本文目录导读:

开源项目的本地化与国际化分离,通常遵循代码与翻译内容解耦的原则,核心思路是:国际化(i18n)负责机制,本地化(l10n)负责内容,以下是具体的分离策略和实践方法:
核心分离原则
- 国际化(i18n):在代码中通过抽象层(如
i18n()函数、ICU MessageFormat)标记所有需要翻译的字符串,并定义语言环境(locale)和区域规则(日期、数字格式),这部分由开发者负责。 - 本地化(l10n):将翻译内容(JSON/YAML/PO 文件)与代码分离,由翻译人员、社区或自动化工具(如 Crowdin、Transifex)独立维护,这部分不涉及代码逻辑。
具体实施步骤
步骤1:代码中只保留语言标识符(Key)
-
不要硬编码显示文本,而是使用 Key 或 ID。
-
示例(JavaScript/React):
// 错误:直接写入中文 <button>保存</button> // 正确:使用国际化函数 <button>{i18n('save_button')}</button>
步骤2:将翻译内容放到独立的资源文件中
-
每一门语言一个文件,与代码仓库分离(或位于独立的
locales/目录)。 -
文件结构示例:
locales/ en-US.json zh-CN.json ja-JP.json src/ components/ utils/i18n.js (不包含任何翻译文本) -
en-US.json:{ "save_button": "Save" } -
zh-CN.json:{ "save_button": "保存" }
步骤3:在 CI/CD 或构建时合并
- 开发环境:使用动态加载语言文件(按需加载)。
- 生产环境:通过构建工具(Webpack、Vite)将翻译文件打包成独立 chunk,或预编译为静态资源。
步骤4:为翻译贡献者提供隔离的工作流
- 使用翻译管理系统(TMS)(如 Crowdin、Lokalise、Weblate):
- 开发者只上传 Key 和默认语言(如英文)的字符串。
- 翻译人员在 TMS 上协作,翻译完成后自动生成对应语言文件,并提交 PR 到仓库。
- 避免翻译人员直接修改代码:他们只接触
.po、.json或.xliff文件,不涉及业务逻辑。
常见工具与框架的实现方式
| 语言/框架 | 国际化方法 | 本地化文件格式 | 推荐分离方式 |
|---|---|---|---|
| React/Next.js | react-i18next, next-intl |
JSON / YAML | 使用 locales/ 目录,按语言分文件夹 |
| Vue | vue-i18n |
JSON / YAML | 通过 messages 选项动态导入 |
| Angular | @angular/localize |
XLIFF / JSON | CLDR 数据 + 独立翻译文件夹 |
| Python/Django | gettext, django-i18n |
.po / .mo |
在 locale/ 目录下,每个语言独立的文件夹 |
| Rust | rust-i18n, fluent |
.ftl (Fluent) |
使用 fluent-resources 分离 |
| Kotlin/Android | Android Resources | strings.xml |
按 values-zh、values-ja 目录隔离 |
高级分离:区域规则与翻译内容分离
除了文本翻译,还需要分离:
- 日期/时间格式(短、长、含时区)
- 数字/货币格式(千分位、小数位、货币符号位置)
- 复数规则(零、一、多)
- 方向(RTL/LTR)(阿拉伯语、希伯来语)
推荐做法:
使用 CLDR(通用区域数据仓库)或 ICU 消息格式 来处理规则,而不是在翻译文件中硬编码格式。
避免的陷阱
- ❌ 在翻译文件中包含 HTML 标签或逻辑(应使用组件级抽象)。
- ❌ 将 Key 命名为描述性文本(如
"Welcome to the app"),应使用无歧义 ID(如welcome_app_message)。 - ❌ 在代码中动态拼接翻译(如
i18n(t) + i18n(s)),应使用参数化消息(i18n("greeting", {name}))。
| 职责 | 由谁负责 | 输出 |
|---|---|---|
| 国际化(i18n) | 开发者 | 代码设施、Key 定义、格式规则 |
| 本地化(l10n) | 翻译人员 / 社区 | 语言资源文件(JSON/PO/FTL) |
| 构建与发布 | CI/CD 系统 | 按语言打包、动态加载或独立产物 |
核心目标:修改翻译时无需修改代码,修改代码时无需修改翻译。