国际化(i18n)

本项目提供一套前后端共享的完整本地化方案,当前支持三种 locale:

  • en
  • zh-Hans
  • zh-Hant

产品默认跟随浏览器语言,但用户也可以在 Settings 中手动覆盖 locale。 该页面同时展示浏览器侧看到的工作区姿态(API base、theme、timezone), 帮助操作员确认本地会话实际采用的上下文。前后端会保持一致, 从而保证 API 错误消息、active-runs 界面与格式化日期都与当前 UI 语言匹配。

设计原则

  • 把支持的 locales 单一事实源放在 locales/manifest.json
  • 文案以稳定翻译 key 存储,而不是在 UI 中内联字符串
  • 优先翻译整句,而不是字符串拼接
  • 用户手动选择 locale 的优先级高于浏览器自动协商
  • 使用 locale-aware 的格式化(Intl、后端翻译错误、Content-Language headers)
  • 把技术标识符(provider.type、model IDs、dataset paths)视为数据,而不是可翻译文案

仓库布局

locales/
├── manifest.json
├── en/
│   ├── frontend.json
│   └── backend.json
├── zh-Hans/
│   ├── frontend.json
│   └── backend.json
└── zh-Hant/
    ├── frontend.json
    └── backend.json
  • manifest.json:共享 locale registry 与 native names
  • frontend.json:通过 react-i18next 渲染的 React UI 文案
  • backend.json:由 FastAPI middleware/helpers 翻译的 API 侧消息

运行时行为

Frontend

  • frontend/src/i18n/index.ts 初始化 i18next
  • frontend/src/app/locale-provider.tsx 解析最终 locale,并同步 <html lang>
  • frontend/src/i18n/locale.ts 包含 locale detection、browser matching、持久化与手动覆盖逻辑
  • frontend/src/api/client.ts 在 API 请求中发送 X-Eval-Locale
  • SSE 请求通过 ?locale= 传递 locale,因为 EventSource 不能发送自定义 headers

Backend

  • backend/src/eval_752/i18n/__init__.py 加载 locales/manifest.json,负责 locale 解析与后端消息翻译
  • LocaleMiddleware 的优先级如下:
    1. X-Eval-Locale
    2. locale query parameter
    3. Accept-Language
    4. manifest.json 中的默认 locale
  • 响应会带上 Content-Language,方便客户端确认实际应用了哪个 locale

新增或更新文案

Frontend 文案

  1. locales/en/frontend.json 中新增稳定 key
  2. 在所有其他 frontend.json 中镜像同一 key
  3. 在 React 组件中使用 useTranslation(),或在非 React 工具 / hooks 中使用 i18n.t(...)
  4. 优先使用插值:
t("runs.newRunForm.created", { id, status });
  1. 日期与数字优先使用 locale-aware 格式化,并尽量复用现有 helpers

Backend 文案

  1. locales/en/backend.json 中新增稳定 key
  2. 在所有其他 backend.json 中镜像同一 key
  3. 在 routes、validators 与后端 response builders 中使用 translate("path.to.key", value=name)
  4. 除非明确需要返回上游原始输出,否则不要新增面向用户的 str(exc) 文本

添加新语言

  1. locales/manifest.json 中添加 locale code 与 native name
  2. 创建 locales/<locale>/frontend.json
  3. 创建 locales/<locale>/backend.json
  4. 先完整复制英文 key 结构,再翻译 value
  5. frontend/src/i18n/resources.ts 中注册前端资源导入
  6. 如果新语言需要特殊浏览器匹配规则,请检查 frontend/src/i18n/locale.ts
  7. 如果后端需要特殊 locale 协商逻辑,请检查 backend/src/eval_752/i18n/__init__.py
  8. 确认该语言会出现在 Settings 中,并验证浏览器匹配行为符合预期

贡献者检查清单

PR 若修改了面向用户的文案,请确认:

  • 更新了全部 locale catalogs,而不仅是英文
  • translation keys 保持稳定;只有语义变化时才重命名
  • 不通过多个 key 拼接一句话
  • 占位符(如 {{name}}{{count}})在各语言中保持一致
  • 验证浏览器跟随模式与手动覆盖模式
  • 至少验证一条由后端翻译的错误或校验路径

测试与验证

修改 i18n 后,请运行相关检查:

cd backend
uv run pytest --cov

cd ../frontend
pnpm typecheck
pnpm test

自动化覆盖目前包括:

  • backend/tests/app/test_i18n.py 中的 locale negotiation tests
  • backend/tests/test_locale_catalogs.py 中的 locale catalog parity checks
  • frontend/src/i18n/locale.test.ts 中的前端 locale helper tests
  • frontend/src/api/client.test.ts 中 API client locale-header 的覆盖

常见陷阱

  • 不要在组件内直接写面向用户的 fallback English
  • 不要本地化持久化标识符或 API enum values,除非它们仅用于展示
  • 若可以用整句 key,就不要围绕标点拼接多个已翻译片段
  • 不要只在 manifest 中加 locale,却没补双份 catalogs 和前端资源注册

另见: