国际化不是翻译

很多团队理解的国际化就是「把界面文案翻成英文」,等真要出海时才发现:日期格式不对、货币符号错位、阿拉伯语从右往左读版式全崩、德语单词长到撑破按钮。到这一步再补,等于把产品重做一遍。

国际化(i18n)和本地化(l10n)是两件事:国际化是工程改造,让产品从结构上支持多种语言和地区;本地化是翻译与适配,把已经国际化的产品搬到一个具体市场。前者做不好,后者无从谈起。

国际化是工程,本地化是翻译;工程没做好,翻译再好也白搭。
i18n 与 l10n
  • i18n编码、文案外置、格式抽象、版面适配。
  • l10n翻译、本地习俗、法律与合规、测试验收。
  • 顺序先国际化,再本地化,不可颠倒。
软件本地化与国际化配图:蓝色渐变背景中央发光地球,四周环绕多个发光方形与圆形节点由发光箭头连线相连,右上角空白发光对话气泡,底部一排纯色方形色块,扁平科技插画
一套产品,多种语言,靠的是从一开始就做对结构。

字符编码:一切文字的底座

字符编码是国际化的地基。历史上各种本地编码(GBK、Shift-JIS)各有各的坑:同一种字节在不同编码里是不同文字,转码错误就出乱码。今天的原则很明确:存与传一律用 UTF-8,只在边界做显式转换。

环节要求常见坑
数据库表与连接用 UTF-8连接串忘了指定字符集
HTTP响应头声明 charset默认编码导致中文乱码
文件统一 UTF-8 无 BOMBOM 被当成内容解析
排序按 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} 是什么。

外置纪律
  • 纪律整句翻译,不用字符串拼接。
  • 纪律占位符命名清晰,附示例值。
  • 纪律图标、图片里的文字也要外置。