如何一次性支持38种语言:Proton WebClients 国际化 i18n 体系完整拆解(ttag 翻译工作流指南)
【免费下载链接】WebClientsMonorepo hosting the proton web clients项目地址: https://gitcode.com/gh_mirrors/we/WebClients
Proton WebClients 是 Proton 官方的前端 Monorepo,集中管理了 Mail、Drive、Pass、Calendar、Account 等所有 Web 客户端。它的国际化(i18n)体系用一套轻量方案支撑了38 种语言:开发者在代码里用ttag标记可翻译文本,再用 Monorepo 内置的 packages/i18n/ CLI 工具一键提取、校验翻译文件,最后通过 Crowdin 平台分发给全球翻译者。本文带你从零拆解这套 ttag 翻译工作流的完整链路。
一、i18n 文件布局:每个应用一个 locales 目录
Monorepo 采用"应用自治"策略:每个应用在自己的根目录下维护语言包,而不是堆在一个全局目录里。以 Mail 为例:
| 路径 | 作用 |
|---|---|
applications/mail/locales/*.json | 各语言翻译文件,共 30+ 种语言 |
applications/mail/locales/config/locales.json | 语言代码 → 母语名称的映射(如"fr_FR": "Français"),驱动 UI 语言切换器 |
applications/account/locales/*.json | Account 应用的语言包,覆盖 38 种语言(含ar_SA、kab_DZ等长尾语种) |
打开任意一个语言文件(如 fr_FR.json),能看到统一的三段式结构:
{ "headers": { "language": "fr_FR", "plural-forms": "nplurals=2; plural=(n >= 2);" }, "contexts": { "account": { "Mail forward stopped": ["Le transfert automatique de messages a été arrêté."] } } }- headers:语言元信息,重点是
plural-forms(复数规则)——德语、俄语、波兰语的复数规则各不相同,这一行让翻译引擎按目标语言的语法规则处理数量词; - contexts:按模块分区的键值对。同一个文案在不同模块(如"账户设置" vs"日历邀请")允许有不同的译文,靠 context 命名空间隔离,避免"一词多义"被错误统一。
二、代码中如何标记可翻译文本:ttag 模板字符串
Proton 的 React 项目统一使用 ttag 来标记文本,核心就是一个叫trans的模板字符串:
import { trans } from '@lingui/core'; // 基本用法:模板字符串里的英文即翻译源文本 const msg = trans`Mail forward stopped`; // 带变量:${ name } 会被翻译工具识别为占位符,翻译者不能改动变量名 const msg = trans`{ name } sent you a file`; // 指定 context + 复数形式 const msg = trans({ context: 'account', defaults: { one: '1 other potential leak detected', other: '${ n } other potential leaks detected' }, });几个关键点:
- 源码即模板:
trans的默认参数就是英文原文,提取工具扫描代码即可生成翻译模板,不需要单独维护 key 字典; - 变量受保护:
${ name }这类占位符会被校验脚本提取比对,翻译中变量缺失或数量不一致会直接报错; - context 必填:没有 context 的翻译在 CI 校验阶段会被判定为不合规(见下文)。
在组件库里实际调试时,可以借助 Storybook 预览组件的文案渲染效果(组件库位于 packages/components/):
三、proton-i18n CLI:提取与校验的三条命令
packages/i18n/ 是一个 npm 内部包@proton/i18n,暴露proton-i18n命令行,官方说明见 Readme.md:
$ npx proton-i18n help Available commands: - validate <type> # 校验翻译:检查 context、变量、ttag 格式 - extract <type> # 从代码中提取全部翻译到模板文件1️⃣ extract:如何做到"只提取真正会发布的文案"
提取逻辑见 scripts/extract.sh。它的巧妙之处在于不在源码上提取,而在构建产物上提取:
- 先用 Webpack 构建出
dist包(经过 tree-shaking,被删除的死代码里的文案不会出现); - 用 extract-sourcemaps.mjs 从 sourcemap 还原出实际打进了 bundle 的源码;
- 调用 ttag-cli 执行
ttag extract,输出 gettext 标准的.pot模板文件。
这样翻译平台上永远不会出现"代码已删除、文案还留着"的僵尸词条,模板文件与线上真实内容严格一致。
2️⃣ validate:三重质量门禁
lib/validate.js 用gettext-parser解析模板文件后执行三层检查:
| 检查项 | 实现 | 拦截的问题 |
|---|---|---|
| 无 context 检查 | validateWithoutContext | 翻译者/开发者漏写 context 导致文案串区 |
| 变量匹配检查 | validateVariables | 单数/复数句中${ n }占位符数量不一致 |
| 重复变量检查 | validateContextAndVariables | 同一 context 下两条文案变量结构完全相同却语义不同 |
任意一项失败都会抛出N translations without context!类错误,直接卡住合并。
3️⃣ lint-functions:ttag 写法规范检查
validate --lint-functions会运行 scripts/linter.mjs,检查源码里trans是否使用了正确格式(比如是否误把函数调用结果传进去、模板字符串是否带 context),把问题拦在代码提交阶段而不是翻译阶段。
四、从代码到 Crowdin:翻译工作流全链路
把上面的环节串起来,就是一条完整的自动化流水线:
- 开发者写代码:
trans标记文案 + context; - CI 跑 lint:
lint-functions保证 ttag 写法规范; - 构建后提取:
extract生成po/template.pot; - 上传翻译平台:CLI 通过 .env 中的
I18N_TEMPLATE_FILE识别模板路径,同步到 Crowdin; - 翻译者协作:各语言译员在 Crowdin 上提交
.po译文; - 回写语言包:译文落地为各应用
locales/目录下的 JSON(格式即上文第二段); - 合入前校验:
validate三重门禁确保变量、context 完整。
整个链路里没有任何手工拷贝粘贴的环节,38 种语言 × 10+ 个应用、数以万计的词条,全部靠这套 CLI 保证一致性。
五、如何新增一种语言:三步走
想为某个应用增加新语言(例如vi_VN越南语),步骤非常清晰:
- 建文件:在
applications/<app>/locales/下新建vi_VN.json,headers里填对该语言的plural-forms规则; - 登记语言:在 locales/config/locales.json 中追加
"vi_VN": "Tiếng Việt",语言切换器即刻生效; - 同步词条:在 Crowdin 上创建对应项目,跑
proton-i18n validate确认无缺失 context 后即可发布。
得益于模板文件自动提取 + CI 校验,新增语言的成本基本只剩"翻译"本身。
总结:为什么这套 i18n 体系值得借鉴
Proton WebClients 的国际化方案没有引入沉重的框架,而是抓住了三件事:用ttag模板字符串让"英文原文"成为唯一事实来源(代码、模板、界面永不脱节);基于构建产物提取词条(消灭僵尸文案);用 CLI + CI 把 context、变量校验变成硬性门禁(翻译质量自动化)。对于同样要支持多语言、多应用的 Monorepo 项目,这套 ttag + gettext 的轻量组合是一个可直接抄作业的成熟范式。
- 核心工具源码:packages/i18n/lib/extract.js、packages/i18n/lib/validate.js
- 提取脚本:packages/i18n/scripts/extract.sh
- 语言包示例:applications/mail/locales/fr_FR.json
【免费下载链接】WebClientsMonorepo hosting the proton web clients项目地址: https://gitcode.com/gh_mirrors/we/WebClients
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考