Skip to main content

国际化方案设计与选型

国际化不只是翻译文本。语种和地区是两个独立维度,写死 left/right 的样式在 RTL 下会全错位,而把这两件事想清楚的团队并不多。

一、语言和地区是两个维度​

语言标签遵循 BCP 47,形如 en-US、pt-BR、zh-CN:前半段是语言,后半段是地区。规范大小写是语言小写、地区大写(en-US 而不是 en-us),虽然 API 会做规范化,但存进数据库、传给后端时保持规范写法能省掉一堆对不上的问题。

把 en-US 简化成 en 是最常见也最贵的一个设计错误。同一个英语界面:

  • 美国显示美元、MM/DD/YYYY;
  • 英国显示英镑、DD/MM/YYYY;
  • 印度显示卢比,而且数字分组是 12,34,567 这种三位两组的形式。

同一个阿拉伯语界面,在海湾国家与北非的税率、工作日、货币也都不一样。地区决定的是格式与业务规则,语言决定的是文字。

const day = new Date('2026-01-01') // 示例统一用 2026-01-01

new Intl.DateTimeFormat('en-GB', { dateStyle: 'short' }).format(day)
// 01/01/2026
new Intl.DateTimeFormat('en-US', { dateStyle: 'short' }).format(day)
// 1/1/2026
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(1234.5)
// 1.234,50 €
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(1234.5)
// $1,234.50

这类错误的可怕之处在于:它在只支持一种地区时完全看不出来,直到接了第二个币种或第二个地区才集中爆发——那时候要改的是数据结构,不是文案。

判断规则很简单:只要产品涉及货币、税率、日期格式、地址格式中的任何一项,就必须存完整的语言+地区标签。

语言(Language)文字与书写方向复数规则、标点习惯地区(Region)货币、税率、法定节假日日期格式与数字分组两者独立组合:en-US 与 en-GB 语言相同、日期与数字写法不同;ar 还要额外处理 RTL 排版。
图:语言与地区是两个维度——只按语言做,地区差异一定会漏

二、运行时库怎么选​

按四个能力维度拆开看,而不是比谁的名气大:

① 复数规则。 英语只有单复两档;俄语有四档;阿拉伯语有六档。如果方案只支持 count === 1 ? a : b,接阿拉伯语时必然出错。

new Intl.PluralRules('ar').select(3) // 'few'
new Intl.PluralRules('ru').select(3) // 'few'
new Intl.PluralRules('en-US').select(3) // 'other'

Unicode CLDR 把复数归为 zero / one / two / few / many / other 六类,Intl.PluralRules 直接返回类别,你只需为用到的类别准备文案。

② 插值与格式化。 日期、数字、货币一律交给 Intl,不要自己拼字符串——分隔符、符号位置、分组方式全都因地区而异。带变量与复数的复杂句子用 ICU MessageFormat 表达,不要拼接翻译后的字符串:拼接出来的句子语序在多数语言里都是错的。

// ICU MessageFormat(来自 intl-messageformat 库):变量与复数一起表达,语序交给翻译
import { IntlMessageFormat } from 'intl-messageformat'
const msg = new IntlMessageFormat('{count, plural, one {# 条评论} other {# 条评论}}', 'zh-CN')
msg.format({ count: 3 }) // 3 条评论
// 阿拉伯语有 six 类复数,翻译自行决定每种类别的文案,无需改代码

③ 加载策略。 全量打包所有语言会让首屏背上一堆用不到的文案;按语言包异步加载是默认选择。

④ 框架集成度。 路由、服务端渲染、语言切换能不能自然配合,往往比 API 好不好用更影响落地成本。

多数业务用成熟库 + Intl 就够了,真正需要自研的场景很少。补充一点:Intl 用的是运行时内置的 ICU 数据,不需要额外打包语言数据;想知道目标环境支持哪些时区和货币,可以用 Intl.supportedValuesOf() 查询。

顺带一提,Date 长期存在的时区与可变性问题正在被解决——ECMAScript 2026 已经把 Temporal 纳入标准,它提供了不可变、时区明确的时间对象。截至 2026 年 10 月各运行时的支持度仍在推进中,涉及复杂时间计算的新代码值得关注它。

三、文案怎么从代码里抽出来​

手工维护 key 一定会漏。正确做法是自动化:用扫描工具从源码里提取文案,生成带唯一 key 的条目,并自动带上上下文注释——同一个「Submit」在按钮上和错误提示里可能是两种译法,没有注释的译者只能猜。

几条实践:

  • key 用语义化的层级命名(checkout.buttons.placeOrder),不要用原文做 key——原文一改,key 就失效,历史翻译全部作废;
  • key 保持稳定,翻译记忆工具才能复用已有译文;
  • 不要拼接翻译片段,整句交给 ICU 模板;
  • 提取要进 CI,让「新增了未提取的硬编码文案」能被发现。

四、子目录、子域名与参数​

三种常见做法,各有代价:

子目录(/en/、/de/) —— 多数场景的默认选择。设置简单,域名权重集中,配合 hreflang 标注对搜索引擎友好。

子域名(en.example.com) —— 适合各语言独立运营、需要部署隔离的团队。代价是域名权重被分散,SEO 上有小幅损失。

国家顶级域(example.de) —— 适合在各市场已有本地实体的全球品牌,维护成本最高。

路由策略还会影响缓存:语言进了路径,就意味着每种语言一份缓存;如果语言只存在于 Cookie 或请求头,CDN 缓存就必须按对应维度做区分,否则会串。

语言放进路径是最稳的一种,服务端据此选语言包,首屏不会闪:

// middleware:未带语言前缀时,按 Accept-Language 协商后重定向
const supported = ['en', 'zh', 'ar']
const lang = negotiate(request.headers.get('accept-language'), supported) ?? 'en'
return Response.redirect(new URL('/' + lang + pathname, request.url))

// 协商失败要有明确兜底,不能因为 header 缺失就返回 404

五、和 SSR/SSG 怎么配合​

服务端要按 locale 出内容。 客户端渲染的应用如果先出默认语言再切换,用户会看到一次闪烁——这在内容型页面上很难接受。

静态站点要为每种语言预生成一份,而不是运行时切换。

语言包要按需加载。 全量打进首屏是最常见的体积浪费,尤其是文案量大、语言多的站点。做法是先渲染一个最小的骨架,语言包到位后再填充;或者把语言包按路由拆分,只加载当前页需要的部分。

检测顺序要固定:URL 参数 → 用户已保存的偏好 → Cookie / localStorage → 浏览器语言 → 默认语言。检测结果要持久化,否则用户每次回来都要重选一次。

日期与数字的格式化一律交给 Intl,自己拼字符串迟早出错:

const d = new Date('2026-01-01')   // 本文示例统一用 2026-01-01

new Intl.DateTimeFormat('ar').format(d) // 阿拉伯语:不同日历体系
new Intl.DateTimeFormat('en-US').format(d) // 1/1/2026
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(1234.5)

// 时区要显式指定,否则服务端与客户端可能算出不同结果
new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai' }).format(d)

六、地区差异带来的额外工作量​

翻译只是其中一环。日期、数字、货币、复数、排序规则、书写方向都要跟着变,这些比翻译更容易出错,而且错了不会有人报上来。

语言标签用两个字母也不够——地区决定货币与格式,缺了 region 迟早要返工,而且改的是数据结构。

客户端切换语言同样不够:服务端渲染时就要按 locale 出内容,否则首屏会闪一次默认语言。

复数规则更不能自己判断等于不等于 1——CLDR 有六类,俄语四档、阿拉伯语六档,用 Intl.PluralRules。

国际化最容易返工的一步,是把语种当成了地区的全部——货币、格式、税率都跟着地区走。

i18next 的能力边界看 i18next 官方文档,复数、插值、嵌套这几块的取舍很影响选型。