多语言
Castor 的界面支持简体中文(zh-CN)、English(en-US)和日本語(ja-JP)。用户在顶栏的语言切换器中选择语言,选择会保存在浏览器中;首次访问时按浏览器语言自动匹配,匹配不到时使用英文。
核心约定:中文原文就是 key
代码里直接写中文原文,中文原文同时作为翻译 key:
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):
{
"导入用户": "Import users",
"删除用户 {{name}}?": "Delete user {{name}}?"
}哪些文案需要 t()
公共组件会自动翻译传给它们的字符串属性,直接写中文、补上译文即可:
PageHeader、Panel的标题DataTable的列titleFormFields的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 中查找译名,找不到时显示数据库中的名称:
{
"system_users": "Users",
"system_roles": "Roles"
}新增菜单时要在 en-US.json 和 ja-JP.json 中都添加对应的 code,前端测试会检查 seed-rbac.ts 中的每个菜单编码都有英文和日文译名。
后端报错翻译
后端代码继续抛中文报错:
throw new ServiceError('用户名已存在')前端的 request.ts 会在每个请求上带 Accept-Language 头。后端在响应发出前,按该头把 JSON 响应中的 error、message 以及导入错误行的 error_rows[].reason 翻译成对应语言。没有带受支持语言的请求(如 curl、API Token 客户端)按英文返回。没有登记译文的文案原样返回中文,不会报错。
新增的报错或提示文案要在 apps/api/src/i18n/messages.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 | 模板字符串中包含中文 |
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 下的注释。
新增一个页面时的清单
- 界面文字写中文原文。
- JSX 文本、原生属性、带变量的文案用
t()。 - 在页面目录下创建
locales/en-US.json和locales/ja-JP.json。 - 新菜单在
apps/web/src/locales/menus/的两个文件中添加译名。 - 后端新增的报错在
apps/api/src/i18n/messages.ts登记。 - 运行
node apps/web/scripts/i18n-scan.mjs <页面目录>,确认 0 个问题。
