# i18n Guide

MaiBot 现在使用 `JSON + Crowdin + Babel` 的国际化方案，不依赖 gettext 的 `.po/.mo` 运行时。

## 仓库翻译策略

- `zh-CN` 是仓库内唯一的 source language，也是唯一的 GitHub 侧 source of truth。
- 非 `zh-CN` 的目标语言文件是同步产物和可评审输出，不是常规长期编辑面。
- 仓库中已经提交过的目标语言文件，需要先通过一次 bootstrap 同步进 Crowdin，避免 Crowdin 把这些历史翻译当成“未翻译”并用 source 文本导出覆盖。
- bootstrap 完成后，目标语言的常规维护应在 Crowdin 中完成，而不是直接在 GitHub 中持续编辑。
- GitHub Actions 是仓库与 Crowdin 之间唯一允许的同步方式；不要把 Crowdin 的原生 GitHub integration 当作第二条回写路径。
- 翻译回流仍然通过 `l10n_*` pull request 完成，不直接写回 `main` 或 `r-dev`。

## 目录结构

翻译文件位于 `locales/<locale>/*.json`，当前默认语言是 `zh-CN`。

建议按模块拆分文件：

- `core.json`
- `startup.json`
- `config.json`
- `prompts.json`

长 Prompt 模板使用单文件本地化目录：

```text
prompts/
  zh-CN/
    replyer.prompt
    planner.prompt
```

注意：

- `prompts/zh-CN/**/*.prompt` 是唯一中文 source 模板目录。
- 不要把 `zh-CN` 原文整批复制到 `prompts/en-US/` 后直接提交。
- 目标语言 prompt 文件应该由 Crowdin 下载生成；在本地还没有目标文件时，运行时会自动回退到 `zh-CN`。

Dashboard WebUI 的 locale 文件位于 `dashboard/src/i18n/locales/*.json`：

```text
dashboard/src/i18n/locales/
  zh.json
  en.json
  ja.json
  ko.json
```

注意：

- `dashboard/src/i18n/locales/zh.json` 是 dashboard 在 Git 中的 source of truth。
- dashboard 运行时继续使用 `zh` / `en` / `ja` / `ko` 这组短 locale code，但 Crowdin 侧仍把 `zh.json` 当作仓库里的 `zh-CN` source 资产来同步。
- `en.json` / `ja.json` / `ko.json` 是同步产物和可评审输出，不是常规长期手工编辑面。

## 在代码中使用

统一从 [`src/common/i18n/__init__.py`](../src/common/i18n/__init__.py) 导入：

```python
from src.common.i18n import t, tn

logger.info(t("startup.launching_script", script_file=script_file))
logger.info(tn("core.tasks_cancelled", count))
```

可用能力：

- `t(key, locale=None, **kwargs)`：普通翻译
- `tn(key, count, locale=None, **kwargs)`：plural 翻译
- `set_locale(locale)` / `get_locale()`：设置或读取当前默认 locale
- `format_datetime_localized(...)`
- `format_number_localized(...)`
- `format_decimal_localized(...)`

Prompt 模板统一从 [`src/common/prompt_i18n.py`](../src/common/prompt_i18n.py) 加载：

```python
from src.common.prompt_i18n import load_prompt

template = load_prompt("replyer")
rendered = load_prompt("replyer", identity="Mai", bot_name="麦麦")
```

Prompt 加载规则：

- 优先读取 `prompts/<当前 locale>/`
- 找不到时回退到 `prompts/zh-CN/`

## locale 优先级

运行时按以下顺序决定 locale：

1. 显式传入 `locale`
2. 当前上下文中的 locale 覆盖（如使用 `use_locale(...)`）
3. 环境变量 `MAIBOT_LOCALE`
4. 默认值 `zh-CN`

## key 规范

- 使用稳定的点分 key，例如 `startup.env_created`
- 全部小写
- 不要把中文原文直接当 key

## 日常翻译流程

1. 先在 `locales/zh-CN/*.json`、`prompts/zh-CN/**/*.prompt` 或 `dashboard/src/i18n/locales/zh.json` 添加或修改 source 内容。
2. 在代码中用 `t()` / `tn()` / `load_prompt()` 替换硬编码字符串。
3. 运行 `python scripts/i18n_validate.py` 校验结构。
4. 把 source 变更推送到 `main` 或 `r-dev`，或手动触发 [`crowdin-sync.yml`](../.github/workflows/crowdin-sync.yml)。
5. 目标语言翻译在 Crowdin 中完成。
6. GitHub Actions 下载当时 Crowdin 中可用的翻译结果，并通过 `l10n_main` / `l10n_r-dev` pull request 回流到仓库。

对于非 `zh-CN` 的目标 locale：

- 下面这两条是本仓库的 repository-specific 校验策略，不是 Crowdin 默认行为。
- 不要手工把中文 source 文案直接复制进目标语言文件后提交。
- 英文 locale 文件中不应保留中文字符；这类残留会被校验脚本拦截。
- Python 项目后续新增的 `ko` locale 也遵循同样规则。
- `dashboard/src/i18n/locales/en.json` / `ja.json` / `ko.json` 也遵循同样规则。

### 什么时候可以直接改目标语言文件

以下场景才适合直接在 GitHub 中改非 `zh-CN` 文件：

- 需要把仓库里已经存在的历史目标语言文件一次性 bootstrap 到 Crowdin。
- 需要做紧急修复，而且你确认后续会把同样的修改补回 Crowdin，避免下一次同步被覆盖。

除了上面这些例外，不要把目标语言文件当作常规编辑入口。

## 校验脚本

运行：

```bash
python scripts/i18n_validate.py
```

校验内容包括：

- JSON 语法是否合法
- 是否存在重复 key
- 是否存在空字符串 key
- 各语言 key 集合是否与 `zh-CN` 对齐
- dashboard 嵌套 JSON 的 key 集合是否与 `dashboard/src/i18n/locales/zh.json` 对齐
- 占位符集合是否一致
- dashboard i18next `{{placeholder}}` 占位符集合是否一致
- plural 结构是否一致
- 非 `zh-CN` locale 是否直接保留了包含中文字符的 source 文案
- prompt 模板已存在时，其占位符集合必须与 `prompts/zh-CN/` 对齐

对于 prompt 模板：

- 缺少目标 locale 文件只会给 warning，不会阻断，因为运行时有 fallback
- 目标 locale 文件如果存在但占位符漂移，会直接校验失败

## 候选扫描

如果你想继续做下一批迁移，可以运行：

```bash
python scripts/i18n_extract_candidates.py
```

这个脚本会扫描仓库中的 Python 文件，输出仍然包含中文字符串常量的位置，方便人工挑选下一批适合迁移到 i18n 的文案。

## Crowdin

项目根目录的 [`crowdin.yml`](../crowdin.yml) 现在会上传三类 source：

- `locales/zh-CN/*.json`
- `prompts/zh-CN/**/*.prompt`
- `dashboard/src/i18n/locales/zh.json`

对于 Python 项目：

- `locales/zh-CN/*.json` 的目标语言现在也包含韩语，不再排除 `ko`
- `prompts/zh-CN/**/*.prompt` 的目标语言现在也包含韩语，不再排除 `ko`

对于 dashboard：

- Crowdin 下载结果会回写到 `dashboard/src/i18n/locales/en.json`、`ja.json`、`ko.json`
- 其中英文文件名保持为 `en.json`，但在 Crowdin 配置里仍映射到 `en-US`

GitHub Actions 中的 [`crowdin-sync.yml`](../.github/workflows/crowdin-sync.yml) 是日常稳态同步入口：

- push 到 `main` / `r-dev` 时，只有 source 资产（包括 dashboard 的 `zh.json` source 文件）和 `crowdin.yml` 会触发正常上传。
- workflow 运行时会上传 source，并下载当时 Crowdin 中可用的翻译结果。
- 下载结果通过 `l10n_*` pull request 回流，而不是直接写回 `main` / `r-dev`。

[`crowdin-bootstrap.yml`](../.github/workflows/crowdin-bootstrap.yml) 是一次性或例外场景使用的 bootstrap 入口：

- 只能手动触发。
- 会把仓库当前已提交的目标语言文件上传到 Crowdin，用来保留历史翻译。
- 不会作为日常 workflow 持续上传 GitHub 中的目标语言改动。
- 这个 workflow 必须先存在于仓库默认分支后，才会在 GitHub 网页端出现，也才能被 `gh workflow run` 调用。

常用命令：

```bash
python scripts/i18n_validate.py
gh workflow run crowdin-sync.yml --ref main
gh workflow run crowdin-bootstrap.yml --ref main -f base_branch=r-dev -f confirm_bootstrap=yes-bootstrap-current-target-translations
gh run list --workflow crowdin-sync.yml --limit 5
gh pr list --head l10n_r-dev
```

更完整的 GitHub 侧操作说明见 [`docs/github-actions-crowdin-workflow-report.md`](./github-actions-crowdin-workflow-report.md)。

## 当前迁移范围

这一批已经覆盖：

- `bot.py` 启动、重启、退出与协议确认提示
- `src/config` 中第一批配置加载、热重载、校验异常提示
- `src/main.py` 的主要启动链路提示
- `src/common/prompt_i18n.py` 的 locale 感知 Prompt 加载
- `prompts/<locale>/` 的单文件 Prompt 模板结构

仍然可以继续做但这次没有全量翻译的内容：

- 所有 Prompt 模板的高质量英文翻译
- 内部协议字段
- debug-only 文案
