# 04 本地化工作流 (Localization Workflow)

> 项目遵循 **ADR-A1**：所有玩家可见字符串必须通过 `tr("KEY")` 包裹，禁止业务代码中出现裸字符串。

---

## 1. 文件位置

```
translations/
  zh_CN.po   ← 简体中文（权威主档）
  zh_TW.po   ← 繁体中文
  en.po      ← 英文
  ja.po      ← 日文
```

---

## 2. `.po` 文件格式

每条翻译的格式为：

```po
msgid "UI_WAVE"
msgstr "第 %d 波"
```

- `msgid`：代码中 `tr("KEY")` 使用的键
- `msgstr`：该语言显示的文字
- `%d`、`%s`、`%.1f` 等格式占位符必须与代码中 `tr("KEY") % value` 完全对应

---

## 3. 添加新翻译字符串（标准流程）

### Step 1 — 在代码中使用 tr() 键

```gdscript
# ❌ 错误：裸字符串
label.text = "商店已关闭"

# ✅ 正确：通过键引用
label.text = tr("SHOP_CLOSED_MSG")

# 带参数
label.text = tr("UI_WAVE") % current_wave      # → "第 3 波"
label.text = tr("UI_KILLS") % [kills, alive]   # → "击杀: 7  存活: 3"
```

### Step 2 — 在全部四个 `.po` 文件中添加对应条目

**zh_CN.po**（权威档，写游戏实际中文）：
```po
msgid "SHOP_CLOSED_MSG"
msgstr "商店已关闭"
```

**zh_TW.po**（繁体，改繁体字）：
```po
msgid "SHOP_CLOSED_MSG"
msgstr "商店已關閉"
```

**en.po**（英文）：
```po
msgid "SHOP_CLOSED_MSG"
msgstr "Shop Closed"
```

**ja.po**（日文）：
```po
msgid "SHOP_CLOSED_MSG"
msgstr "ショップが閉店しました"
```

### Step 3 — 验证（运行时检查）

在游戏内执行脚本验证无缺键：

```gdscript
var keys = ["SHOP_CLOSED_MSG"]  # 你新加的键
for loc in ["zh_CN","zh_TW","en","ja"]:
    Locale.set_locale(loc)
    for k in keys:
        if tr(k) == k:
            print("缺键！语言: %s  键: %s" % [loc, k])
```

---

## 4. 键名规范

**格式**：`<模块>_<实体>_<含义>`，全大写 + 下划线

| 前缀 | 用途 | 示例 |
|:---|:---|:---|
| `UI_` | 通用 HUD 标签 | `UI_WAVE`, `UI_HP` |
| `SHOP_` | 商店界面 | `SHOP_TITLE`, `SHOP_REROLL` |
| `INV_` | 背包界面 | `INV_TITLE`, `INV_BENCH` |
| `SETTLE_` | 结算屏 | `SETTLE_TITLE`, `SETTLE_STATS` |
| `SETTINGS_` | 设置面板 | `SETTINGS_DIFFICULTY` |
| `STATUS_` | 状态消息 | `STATUS_PURCHASED` |
| `DIFF_` | 难度名称 | `DIFF_BEGINNER`, `DIFF_CHALLENGE` |
| `SPELL_` | 法术名/描述 | `SPELL_SPARK_BOLT_NAME`, `SPELL_SPARK_BOLT_DESC` |
| `CORE_` | Core 名/描述 | `CORE_WAND_BASIC_NAME` |
| `STATUS_EFFECT_` | 状态效果名 | `STATUS_EFFECT_BURN_NAME` |
| `BOSS_` | Boss 名称 | `BOSS_FINAL_NAME` |

---

## 5. 法术 / Core 名称本地化（待完成）

当前法术 `display_name` 字段存储的是裸英/中文字符串（如 `"Spark Bolt"`）。正式发布前需键化。  
> **注意**：修改后须同步更新商店按钮渲染（`combat_s2.gd → _refresh_shop_ui`），将 `spell.display_name` 改为 `tr(spell.display_name)`。

**Step 1 — 修改 `wand_preset.gd`**

```gdscript
# 当前（裸字符串）
n.display_name = "Spark Bolt"
n.description  = "发射一颗期限 4s 的电光飞弹，造成 3 伤害。"

# 修改为（键）
n.display_name = "SPELL_SPARK_BOLT_NAME"
n.description  = "SPELL_SPARK_BOLT_DESC"
```

**Step 2 — 在四语 `.po` 中添加**

```po
# zh_CN.po
msgid "SPELL_SPARK_BOLT_NAME"
msgstr "电花弹"

msgid "SPELL_SPARK_BOLT_DESC"
msgstr "发射一颗快速电花飞弹，造成 3 点闪电伤害。"
```

**Step 3 — 显示时包裹 tr()**

```gdscript
# UI 显示法术名时
btn.text = tr(spell.display_name)
# 法术描述
desc_label.text = tr(spell.description)
```

---

## 6. 添加新语言

**Step 1 — 创建新 `.po` 文件**（以韩语为例）

```
translations/ko.po
```

内容：复制 `en.po`，将 `Language: en` 改为 `Language: ko`，翻译所有 `msgstr`。

**Step 2 — 在 `locale_manager.gd` 中注册**

```gdscript
const LOCALES: Array = ["zh_CN", "zh_TW", "en", "ja", "ko"]  # 追加 "ko"
```

新语言将自动在商店语言切换按钮中循环出现。

---

## 7. 在游戏内切换语言（测试用）

```gdscript
# 切换到英文
Locale.set_locale("en")

# 循环切换（等同商店 🌐 按钮）
Locale.cycle_locale()

# 查看当前语言
print(Locale.get_locale())
```

---

## 8. 提交规范

每次提交涉及玩家可见字符串的改动时，须**同时更新四个 `.po` 文件**。允许以英文或空字符串占位（`msgstr ""`），但 PR 审查时须标注"待翻译"。

---

## 9. 工具推荐

| 工具 | 用途 |
|:---|:---|
| [Poedit](https://poedit.net) | `.po` 文件 GUI 编辑器，支持模糊匹配和自动翻译接口 |
| VSCode + gettext 插件 | 轻量化文本编辑 |
| Godot 内置本地化 | `Project → Project Settings → Localization → Translations` 可预览各语言 |
