Skip to content

多语言 ​

Castor 的界面支持简体中文(zh-CN)、English(en-US)和日本語(ja-JP)。用户在顶栏的语言切换器中选择语言,选择会保存在浏览器中;首次访问时按浏览器语言自动匹配,匹配不到时使用英文。

核心约定:中文原文就是 key ​

代码里直接写中文原文,中文原文同时作为翻译 key:

tsx
const { t } = useTranslation()

t('保存')
t('共 {{count}} 条', { count })
  • 中文不需要翻译文件:找不到译文时直接显示 key 本身,也就是中文。
  • 英文和日文译文写在 JSON 文件中,格式为“中文 → 译文”。

这样写代码时不需要为每句文案起 key 名,代码读起来也和界面一致。

前端 ​

译文文件 ​

位置内容
页面目录下的 locales/en-US.json、locales/ja-JP.json该页面的文案,例如 apps/web/src/modules/admin/pages/users/locales/
apps/web/src/locales/en-US.json、ja-JP.json公共文案
apps/web/src/locales/menus/en-US.json、ja-JP.json菜单名,按菜单 code 翻译

所有 locales/*.json 在构建时合并到同一个命名空间(locales/menus/ 下的菜单文件单独放在 menu 命名空间)。同一个目录下的 en-US.json 和 ja-JP.json 必须包含相同的 key。

页面译文示例(locales/en-US.json):

json
{
  "导入用户": "Import users",
  "删除用户 {{name}}?": "Delete user {{name}}?"
}

哪些文案需要 t() ​

公共组件会自动翻译传给它们的字符串属性,直接写中文、补上译文即可:

  • PageHeader、Panel 的标题
  • DataTable 的列 title
  • FormFields 的 label、placeholder、options、rules 中的文案
  • FilterSelect、SegmentedTabs、StatusBadge、StatCard、RowActions、ConfirmAction、FormDialog 等组件的文案属性
  • toast.success('固定中文') 这类固定文案

以下情况必须用 t() 包裹:

  • JSX 中直接书写的中文文本
  • 原生元素的 aria-label、title、placeholder
  • 带变量的文案:写成 t('删除用户 ?', { name }),不要用中文模板字符串
  • 图表坐标轴、图例等其他显示渠道

演示内容不翻译

示例数据、示例文档等演示内容属于数据而不是界面文案,用注释标出,扫描时会跳过:

  • 单行:在上一行写 // i18n-ignore-next-line
  • 整个文件:在文件中写 i18n-ignore-file 注释

菜单名翻译 ​

菜单名存在数据库里(中文),前端按菜单 code 在 apps/web/src/locales/menus/<lang>.json 中查找译名,找不到时显示数据库中的名称:

json
{
  "system_users": "Users",
  "system_roles": "Roles"
}

新增菜单时要在 en-US.json 和 ja-JP.json 中都添加对应的 code,前端测试会检查 seed-rbac.ts 中的每个菜单编码都有英文和日文译名。

后端报错翻译 ​

后端代码继续抛中文报错:

ts
throw new ServiceError('用户名已存在')

前端的 request.ts 会在每个请求上带 Accept-Language 头。后端在响应发出前,按该头把 JSON 响应中的 error、message 以及导入错误行的 error_rows[].reason 翻译成对应语言。没有带受支持语言的请求(如 curl、API Token 客户端)按英文返回。没有登记译文的文案原样返回中文,不会报错。

新增的报错或提示文案要在 apps/api/src/i18n/messages.ts 登记英文和日文译文:

ts
// Exact messages
export const MESSAGES = {
  '用户名已存在': { 'en-US': 'Username already exists', 'ja-JP': 'ユーザー名は既に存在します' },
}

// Messages built from template literals: regex on the Chinese text, $1… in the translation
export const PATTERNS = [
  { re: /^菜单编码 (.+) 已存在$/, 'en-US': 'Menu code $1 already exists', 'ja-JP': 'メニューコード $1 は既に存在します' },
]

以上为示意,实际条目按模块分组写在 messages.ts 中。

导入导出表头保持中文

导入导出文件的表头不随界面语言变化,始终为中文。引用表头的报错信息中,表头部分也保持中文。

扫描与测试守卫 ​

前端扫描 ​

apps/web/scripts/i18n-scan.mjs 扫描三类问题:

问题含义
missing中文字符串在任何 locales/*.json 中都没有英文或日文译文
jsx-text中文直接写在 JSX 文本中,没有经过 t()
template模板字符串中包含中文
bash
node apps/web/scripts/i18n-scan.mjs                                  # 扫描整个 src,有问题时退出码为 1
node apps/web/scripts/i18n-scan.mjs src/modules/admin/pages/users    # 只扫描某个目录(相对 apps/web)
node apps/web/scripts/i18n-scan.mjs --json src/modules/admin/pages/users   # 输出 JSON

新页面的扫描结果必须为 0 个问题。

测试 ​

测试检查内容
apps/web/test/i18n.test.ts每个 locales/ 目录的 en-US 与 ja-JP key 相同;同一个 key 在不同文件中的译文不冲突;seed-rbac.ts 中的每个菜单编码都有英文和日文译名;整个前端源码扫描无问题
apps/api/test/i18n-messages.test.ts解析后端源码,找出所有可能返回给用户的中文文案,检查都已在 messages.ts 中登记

这些测试包含在 pnpm test 和 pnpm verify 中,漏写译文会导致门禁失败。

代码注释必须用英文 ​

所有代码注释一律使用英文,包括前端、后端、脚本、测试以及 scaffold 生成的代码。界面文案仍然写中文原文作为 key。

注释中需要引用代码里真实存在的中文字符串(如报错文案、翻译 key)时,把它放在引号或反引号中。apps/web/test/english-comments.test.ts 会检查 apps/api、apps/web、apps/mcp 和 docs/templates 下的注释。

新增一个页面时的清单 ​

  1. 界面文字写中文原文。
  2. JSX 文本、原生属性、带变量的文案用 t()。
  3. 在页面目录下创建 locales/en-US.json 和 locales/ja-JP.json。
  4. 新菜单在 apps/web/src/locales/menus/ 的两个文件中添加译名。
  5. 后端新增的报错在 apps/api/src/i18n/messages.ts 登记。
  6. 运行 node apps/web/scripts/i18n-scan.mjs <页面目录>,确认 0 个问题。

Released under the MIT License.