博客 · 工程实践 / 产品工程 · 2026-10-03
国际化(i18n)实战:从硬编码文案到全球可用
i18n 不是「把文案翻译一下」——时区算错会跨天、复数规则会翻出翻译腔、写死的按钮宽度会让德语文案溢出。这篇按「结构 → 格式化 → 布局 → 链路 → 验收」给一条可落地的路径。
一、先分清 i18n 与 l10n:结构先行
很多团队把国际化理解成「上线前找翻译把文案过一遍」,结果改一处文案要翻二十个文件、时间格式全靠各写各的。国际化(i18n)是让代码"具备多语言能力"的工程结构;本地化(l10n)才是针对具体语言的适配。正确顺序:先把结构搭好(文案外置、格式化统一、布局弹性),翻译随时可以进来;反过来则全是返工。
i18n 是一次性的工程结构,l10n 是可重复的运营流程——先修路,再跑车。
· · ·
二、文案外置与 key 设计
所有用户可见文案进资源文件或翻译平台,代码只引用 key。两个关键决策:key 怎么起名(语义化如 checkout.submit 便于重组,原文做 key 便于查找——团队选一种坚持);绝不拼接句子——不同语言语序不同,拼接必然翻译腔。
| 场景 | ✗ 反例 | ✓ 做法 |
|---|---|---|
| 问候语 | "你好," + name + "!" | 整句 + 占位符:hello: "你好,{name}!" |
| 数量文案 | n + "条消息" + (n > 1 ? "s" : "") | 复数规则引擎按语言处理(见下一节) |
| 单位拼接 | price + "元/月" | 整句 + 金额格式化:per_month: "每月 {price}" |
三、格式化的四件套:别自己造轮子
- 日期时间:数据库存 UTC,按用户时区渲染;「跨天」这类业务判断用时间戳比较,不要先格式化再比字符串
- 数字与货币:用平台国际化 API(如 Intl.NumberFormat)处理千分位与货币符号;金额以最小单位整数存储,避免浮点误差
- 复数与语序:英语只有单复数两态,阿拉伯语有六态——交给规则引擎(如 ICU MessageFormat),别用三元表达式硬编码
- 姓名与地址:不要假设「姓在前」或地址从小到大,用结构化字段加各地区的显示模板
// 用标准 API,而不是手写格式化(示意)
new Intl.DateTimeFormat(locale, { dateStyle: "long" }).format(date);
new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount);
// ICU 消息(复数规则由引擎处理)
// "{count, plural, =0 {没有消息} one {# 条消息} other {# 条消息}}"
四、布局与书写方向:给文字留「膨胀余量」
- 长度膨胀:同一句话德语比英语长约三成——按钮、标签、表单一律弹性布局,别写死宽高;截断要有合理策略(省略号 + 悬浮全称)
- RTL 书写方向:阿拉伯语、希伯来语从右向左——用逻辑属性(start/end)而不是 left/right,图标、进度条、边距都要镜像;数字与代码保持 LTR
- 排版细节:中日韩没有空格断词,需要按字符断行;西文注意连字符与避免孤字
五、工程链路:提取 → 翻译 → 构建 → 运行时
- 提取:用 lint 规则拦截硬编码文案;CI 里检查资源文件与代码引用的一致性(缺失 key 直接失败)
- 翻译:接入翻译平台,给译者提供上下文(界面截图、所属模块、字数上限)——「Book」是书还是预订,全靠上下文说清
- 构建:语言包按需加载,首屏只带当前语言;新增语言不改业务代码
- 运行时:语言优先级 = 用户手动选择 > 账号设置 > 浏览器偏好 > 默认语言;回退链(如 zh-HK → zh-Hant → zh → en)与缺失 key 的兜底展示要显式定义
六、测试与验收:让问题在发布前现形
- 伪本地化:把文案自动加长、加乱字符跑一遍界面——十分钟找出大部分写死宽度与截断问题
- 截图回归:关键页面在目标语言下截图对比,翻译更新一目了然
- 边界用例:跨天、夏令时切换、货币零小数(日元)与三位小数(第纳尔)、极端长姓名
七、五个常见坑
- 拼接句子:不同语言语序不同,拼接的文案永远是翻译腔——整句 + 占位符才是正解
- 写死宽高:英语下刚好,德语下溢出,RTL 下错位——布局为文字留余量
- 本地时间存库:服务器换时区或用户跨时区,时间全线漂移——存 UTC、显示时再转换
- 复数硬编码:
n > 1的三元表达式撑不过第二种语言——交给规则引擎 - 翻译缺上下文:同一个词在不同界面含义不同,没有截图与模块信息,译者只能猜
速查卡
| 场景 | 做法 |
|---|---|
| 用户可见文案 | 全部外置为 key;杜绝拼接,整句 + 占位符 |
| 时间处理 | 存 UTC;渲染用 Intl / 各语言库按用户时区 |
| 金额处理 | 最小单位整数存储;显示交给货币格式化 API |
| 复数与语序 | ICU 规则引擎,不用 if / 三元硬编码 |
| 布局适配 | 弹性布局留膨胀余量;逻辑属性支持 RTL |
| 翻译质量 | 给上下文:截图、模块、字数限制;截图回归验收 |
| 快速自检 | 伪本地化跑一遍界面,专找溢出与截断 |
写在最后
国际化的本质不是翻译,而是把「只对一种语言成立」的假设从代码里清出去:文案外置、格式交给标准库、布局留余量、时区存 UTC。结构做对了,新增一种语言就只是加一份资源文件——而不是一次重构。
下一步 · 搜一遍代码里的用户可见硬编码文案
从按钮与错误提示开始——顺手把「n > 1」这类复数判断换成规则引擎,两件小事就能暴露大部分结构问题。系列下一篇候选:A/B 实验与数据驱动决策、性能预算实践、结构化日志规范。
评论(0)