国际化不是翻译
很多团队理解的国际化就是「把界面文案翻成英文」,等真要出海时才发现:日期格式不对、货币符号错位、阿拉伯语从右往左读版式全崩、德语单词长到撑破按钮。到这一步再补,等于把产品重做一遍。
国际化(i18n)和本地化(l10n)是两件事:国际化是工程改造,让产品从结构上支持多种语言和地区;本地化是翻译与适配,把已经国际化的产品搬到一个具体市场。前者做不好,后者无从谈起。
国际化是工程,本地化是翻译;工程没做好,翻译再好也白搭。
- i18n编码、文案外置、格式抽象、版面适配。
- l10n翻译、本地习俗、法律与合规、测试验收。
- 顺序先国际化,再本地化,不可颠倒。
字符编码:一切文字的底座
字符编码是国际化的地基。历史上各种本地编码(GBK、Shift-JIS)各有各的坑:同一种字节在不同编码里是不同文字,转码错误就出乱码。今天的原则很明确:存与传一律用 UTF-8,只在边界做显式转换。
| 环节 | 要求 | 常见坑 |
|---|---|---|
| 数据库 | 表与连接用 UTF-8 | 连接串忘了指定字符集 |
| HTTP | 响应头声明 charset | 默认编码导致中文乱码 |
| 文件 | 统一 UTF-8 无 BOM | BOM 被当成内容解析 |
| 排序 | 按 Unicode 规则比较 | 二进制比较导致排序错乱 |
还有一个容易被忽略的点:emoji 和生僻字。它们可能需要按码点而非字节去截断,按字节截断字符串会把一个字符切成两半,存库直接报错或存进垃圾数据。字符串处理一律按「字符」为单位,不按字节。
// 按码点安全截断,而不是按字节
function safeSlice(text, max) {
return Array.from(text).slice(0, max).join("");
}
// 示例:safeSlice("你好世界", 2) 返回 "你好"
文案外置:把字符串赶出代码
国际化最核心的一步是文案外置:界面上所有用户可见的字符串,都从代码里挪到资源文件,代码只引用一个键。这样翻译时不动代码,加语言时不动代码。
// 资源文件示例(示意)
{
"welcome": "欢迎回来",
"order.total": "订单总额:{amount}",
"error.timeout": "请求超时,请重试"
}
// 代码里只引用键,不写死文案
t("welcome");
t("order.total", { amount: formatCurrency(total) });
文案外置有两条纪律:一是不要拼接句子。「你购买了{n}件商品」比「你购买了」+ 数字 +「件商品」三块拼起来安全得多,因为不同语言的语序完全不同;二是占位符要有名字,别用裸数字当占位符,翻译人员根本不知道 {1} 是什么。
- 纪律整句翻译,不用字符串拼接。
- 纪律占位符命名清晰,附示例值。
- 纪律图标、图片里的文字也要外置。
数字、日期与时区:格式化的陷阱
同一时刻,在不同地区会写成完全不同的样子:日期有年-月-日、月/日/年、日/月/年;数字有千分位分隔符差异,还有小数点用点还是逗号的区别;时区更是重灾区——存的是本地时间还是 UTC,显示时要不要按用户时区换算,全都没说清就必然出错。
- 存储一律用 UTC,显示时再按用户时区转换。
- 日期时间用标准格式化 API,不手写拼接。
- 金额永远用十进制字符串或整数分存储,不做浮点运算。
- 相对时间(3 分钟前)用库处理,不同语言表达完全不同。
| 区域 | 日期示例 | 数字示例 |
|---|---|---|
| 中国 | 2026-10-28 | 1,234.56 |
| 美国 | 10/28/2026 | 1,234.56 |
| 德国 | 28.10.2026 | 1.234,56 |
| 日本 | 2026年10月28日 | 1,234.56 |
规矩只有一句:格式是显示层的事,不是数据层的事。数据层只管标准化存储,显示层按 locale 格式化,两边彻底分开。
从右到左语言与版面适配
阿拉伯语、希伯来语等从右到左(RTL)语言,不只是文字方向反了:整个版面都要镜像——导航栏、图标顺序、滚动方向、进度条、时间轴,全都反过来。
适配 RTL 的关键是不写死左右:用逻辑属性替代物理方向。CSS 里用 margin-inline-start 而不是 margin-left,用 flex 的 row 而不是死排方向;JavaScript 里用逻辑方向 API 判断,而不是写死 left。
/* 用逻辑属性,RTL 下自动镜像 */
.card {
margin-inline-start: 1rem;
padding-inline-end: 0.5rem;
border-inline-start: 2px solid #1E5FA8;
}
/* 物理属性会写死方向,RTL 下就错位了 */
图标也要选对称的或可翻转的:箭头、翻页按钮这类方向性图标,RTL 下要能镜像。做不到就把图标做成方向自适应,而不是每种语言各做一套。
本地化测试:别等上线再发现
国际化的代码写完了,还要验:伪本地化是成本最低的验证方式——把英文文案替换成加长的、带重音的伪文本,立刻暴露两件事:文案有没有真的外置、界面能不能容纳长文本。
// 伪本地化思路(示意)
// 把每个字符串拉长三分之一并加特殊字符
function pseudoLocalize(text) {
return "[" + text.split("").map(c => c + "̂").join("") + "]";
}
// 界面用伪文本跑一轮,就能发现溢出与写死文案
正式的本地化测试还包括:真实翻译跑一遍关键流程、RTL 版面走查、时区与夏令时切换验证、不同地区数字与货币格式核对。测试要覆盖最短语言和最长语言:德语、芬兰语通常很长,日语、韩语可能很短,界面都要能容纳。
让国际化成为默认习惯
国际化做得好不好,取决于它是不是默认习惯,而不是有没有专门的国际版本。几条可以立刻落地的动作:
- 新写代码默认文案外置,哪怕是单语言产品。
- 存时间一律 UTC,显示时再转换。
- 布局用逻辑属性,少写死左右方向。
- Code Review 时检查写死的文案和格式。
- 每个里程碑跑一次伪本地化。
今天多做的一分结构,是明天出海时省下的一整个重写周期。
市场会变,产品会变,但一套从结构上支持多语言的产品,永远比临时补出来的国际化版本走得更远。让产品说世界的语言,从默认习惯开始。