国际化

国际化把语言、区域、时区、复数规则和书写方向变成显式输入,让同一套产品逻辑为不同地区生成正确界面。

难度 进阶 时长 标准深度约 13分钟
版本 Node 24
what

国际化(internationalization,i18n) 把区域设置、消息、格式和书写方向从业务逻辑中分离出来,使同一套软件能适配不同语言与地区。

trap

只翻译界面文字还不够;拼接句子、猜测货币或时区、硬编码复数规则、只把布局翻转一半,都会产生看似正常却含义错误的界面。

fix

保留原始数据,以已解析的区域设置为显式输入,用完整消息和 Intl 格式化输出,并用真实区域设置与伪本地化测试整个渲染路径。

是什么,为什么存在

国际化 是一组设计约束:程序把可翻译消息、区域格式、书写方向和文化规则当作输入,而不是散落在业务代码中的常量。它通常缩写为 i18n,因为英文单词的首尾字母之间有 18 个字母。国际化完成后,新增地区通常只需增加数据和配置,不必复制业务流程。

本地化(localization,l10n) 是针对某个市场填充并验证这些输入的过程,包括翻译、术语选择、日期格式、数字格式和版式检查。i18n 建立可适配的边界,l10n 交付某个地区可用的结果。两者不能互换:架构支持多语言,不代表任一翻译已经准确。

区域设置(locale) 不是语言名称的别名。zh-Hans-CN 同时表达语言、书写系统和地区,而 en-USen-GB 虽然语言相同,日期、拼写和产品约定仍可能不同。用户所在位置也不能可靠地推断其语言、货币或时区。

国际化最重要的边界位于数据与呈现之间。订单金额应保存为数值和 ISO 4217 货币代码,事件应保存为明确的时间语义,界面再依据区域设置生成文本。若把 $1,234.5004/09/2026 当作业务数据,后续代码既难以换地区,也可能错误解析原值。

你会在语言切换、服务端渲染、邮件、账单、搜索排序、无障碍名称和从右到左(right-to-left,RTL)布局中遇到这套边界。即使产品目前只有一种语言,显式保存单位、货币和时区也能避免把展示约定误当成数据事实。

工作原理

一条可预测的国际化路径先解析请求,再选择产品支持的区域设置。程序随后加载对应消息目录,把原始值交给区域敏感的格式化器,并同时设置文档语言与基础方向。业务规则只产生消息键和结构化参数,不负责拼接最终句子。

区域设置可能来自账号偏好、URL、客户端存储或请求头。产品要规定清晰的优先级,并让用户的明确选择高于自动检测;服务端还要把解析结果传给客户端,避免水合时突然换语言。无法匹配时使用一个确定的回退值,而不是依赖运行机器的默认环境。

解析结果不是一个能回答所有问题的全局字符串。语言、书写系统和地区可以来自区域设置;时区、货币、计量单位和一周起始日可能来自用户或业务上下文。把这些值分别建模,才能表达「中文界面、欧元结算、巴黎时区」这样的正常组合。

国际化库负责消息目录和消息语法,运行时负责数字、日期、列表、显示名称、分段和排序等底层能力。JavaScript 的 Intl API 依据运行时携带的 Unicode 区域数据工作;它不翻译产品文案,也不知道订单应使用哪种货币。

区域设置标识符

Web 平台通常使用 BCP 47 语言标签,例如 enpt-BRzh-Hant-TW。子标签可能包含语言、书写系统、地区、变体和 Unicode 扩展,因此不要靠固定位置的 split('-') 结果推断含义。先验证并规范化,再按产品支持列表匹配。

Intl.getCanonicalLocales() 会验证标签并返回规范形式,Intl.Locale 则提供 languagescriptregion 等结构化字段。规范化不等于协商:把 en-us 变成 en-US,并没有决定应用究竟支持 enen-US 还是两者。

区域设置标签可以携带日历或数字系统等扩展,但这仍不等于业务默认值。例如,ar-EG 不应自动决定订单货币,因为用户可能查看以美元计价的商品。格式化函数应分别接收区域设置、货币和时区。

消息选择与回退

消息目录用稳定的语义键连接代码和译文,例如 cart.items。键应描述用途,不应直接使用整段英文源文;源文修改后,语义键仍可稳定,而翻译工具也能单独跟踪内容变化。相同英文词在按钮、名词和法律文本中可能需要不同键。

动态值通过具名占位符进入完整消息。译者可以调整词序,也能在支持 ICU MessageFormat 的系统中按复数和语法性别选择完整分支。把 t('youHave') + count + t('items') 拆成片段,会把英语词序强加给其他语言。

回退链应短、确定并可观测。常见策略是精确区域设置、较宽的语言或书写系统目录、最后的产品默认语言;具体顺序属于产品契约。开发和持续集成应把缺键报告为错误,生产环境即使显示回退文本也应记录遥测。

译文也是输入数据。若目录允许富文本,应把经过验证的占位组件插入消息结构,而不是把翻译字符串直接交给 innerHTML。变量值继续按普通文本转义,链接目标和允许的标签由代码控制。

数字、时间与复数

Intl.NumberFormat 接收数值和格式选项,能够输出十进制数、百分比、单位与货币。货币代码决定计价单位,区域设置决定常见的符号位置和分隔方式;两者缺一不可。格式化后的文本用于显示,不应再被解析回金额。

Intl.DateTimeFormat 需要明确的日期值和展示时区。同一瞬间在上海与纽约可能落在不同日期,因此服务端和客户端若使用不同默认时区,就可能生成不同 HTML。只有日历日期而没有时刻含义的数据,应采用单独的数据模型,不能随意当成 UTC 午夜。

Intl.PluralRules 返回 zeroonetwofewmanyother 复数类别(plural category) 。类别由区域规则和数字共同决定,不是把 count === 1 翻译成几种语言。类别只负责选择消息分支,实际数字仍要单独格式化。

创建格式化器时应明确所需选项,并在一个渲染范围内复用相同配置。不要宣称某种缓存一定更快,除非在目标运行时和实际调用模式中测量过。正确性首先取决于输入语义一致,而不是对象创建策略。

语言与书写方向

HTML 的 lang 告诉浏览器和辅助技术内容使用什么语言,dir 指定基础书写方向。两者解决不同问题,切换区域设置时应一起更新根元素。CSS 使用 margin-inline-startpadding-inline-endtext-align: start 等逻辑属性,避免为 RTL 复制整套样式。

基础方向不能解决一段文本内部的双向混排。用户名、订单号或 URL 可能采用与周围句子相反的方向,应使用 <bdi> 或合适的隔离机制限制影响范围。不要在用户输入中手工插入不可见方向字符来修补布局,因为这些字符难以审计和复制。

镜像策略取决于含义。返回箭头和流程方向图标通常需要镜像,播放键、商标、时钟和包含数字的图形通常不应镜像。应让设计系统逐个声明方向行为,而不是对所有图标统一应用 scaleX(-1)

示例

下面四个例子只使用 Node 24 内置的 Intl API,因此不依赖框架或下载的消息库。示例依次处理格式化、复数类别、消息目录和伪本地化,输出均来自本地的 Node v24.14.0。

显式格式化订单值

第一个例子把区域设置、货币和时区作为三个独立参数。固定的 ISO 时间点让服务端和客户端可以得到同一日期,而不是各自读取宿主机默认时区。

format_order.js
const placedAt = new Date("2026-09-04T10:30:00Z");

function formatOrder(locale, currency, timeZone) {
  const date = new Intl.DateTimeFormat(locale, {
    dateStyle: "medium",
    timeZone,
  }).format(placedAt);
  const total = new Intl.NumberFormat(locale, {
    style: "currency",
    currency,
  }).format(1234.5);
  return `${Intl.getCanonicalLocales(locale)[0]} | ${date} | ${total}`;
}

console.log(formatOrder("en-us", "USD", "America/New_York"));
console.log(formatOrder("de-DE", "EUR", "Europe/Berlin"));
console.log(formatOrder("zh-cn", "CNY", "Asia/Shanghai"));
en-US | Sep 4, 2026 | $1,234.50
de-DE | 04.09.2026 | 1.234,50 €
zh-CN | 2026年9月4日 | ¥1,234.50

Intl.getCanonicalLocales() 把输入标签规范化,所以输出使用 en-USzh-CN。它没有改变货币或时区;这些值由调用方明确提供,避免从语言或地理位置猜测。

观察复数类别

第二个例子不编造阿拉伯语译文,只观察运行时针对同一组数字选择的类别。法语中的 0 属于 one,阿拉伯语还会使用 zerotwofewmany,因此二分条件不能替代区域规则。

plural_categories.js
const counts = [0, 1, 2, 3, 11, 100];

for (const locale of ["en", "fr", "ar"]) {
  const rules = new Intl.PluralRules(locale);
  const selections = counts.map(
    (count) => `${count}=${rules.select(count)}`,
  );
  console.log(`${locale}: ${selections.join(", ")}`);
}
en: 0=other, 1=one, 2=other, 3=other, 11=other, 100=other
fr: 0=one, 1=one, 2=other, 3=other, 11=other, 100=other
ar: 0=zero, 1=one, 2=two, 3=few, 11=many, 100=other

消息目录必须为目标区域可能返回的类别提供分支,并始终保留 other。不要把类别名称展示给用户;它只是消息选择的内部键。

用完整消息承载语法

这个最小目录按语言保存完整消息,并把数字作为具名参数替换。示例把目录回退和区域格式化分开:de-DE 没有目录,因此回退到英文文案,但数字仍按请求区域设置格式化。

message_catalog.js
const catalogs = {
  en: {
    "cart.items": { one: "{count} item", other: "{count} items" },
  },
  zh: {
    "cart.items": { other: "{count} 件商品" },
  },
};

function formatMessage(locale, key, values) {
  const language = new Intl.Locale(locale).language;
  const message = catalogs[language]?.[key] ?? catalogs.en[key];
  const category = new Intl.PluralRules(locale).select(values.count);
  const template = message[category] ?? message.other;
  const count = new Intl.NumberFormat(locale).format(values.count);
  return template.replace("{count}", count);
}

for (const locale of ["en-GB", "zh-CN", "de-DE"]) {
  console.log(`${locale}: ${formatMessage(locale, "cart.items", { count: 2 })}`);
}
en-GB: 2 items
zh-CN: 2 件商品
de-DE: 2 items

生产系统应使用能解析消息语法、验证占位符并报告缺键的成熟库,而不是扩展这个小函数。这个例子的边界仍然适用:业务代码传递键和类型明确的值,消息层决定语序与分支。

用伪本地化暴露布局假设

伪本地化(pseudo-localization) 在不等待真实译文的情况下改变字符并扩张文本。下面的简化转换器保留 {amount}{name} 占位符,方便测试键名是否泄漏、容器是否截断以及代码是否错误依赖英文原文。

pseudo_locale.js
const accents = {
  a: "à", e: "ë", i: "ï", o: "ø", u: "ü",
  A: "Å", E: "Ë", I: "Ï", O: "Ø", U: "Ü",
};

function pseudoLocalize(message) {
  const parts = message.split(/(\{[a-zA-Z][\w]*\})/g);
  const transformed = parts.map((part) => {
    if (/^\{.*\}$/.test(part)) return part;
    return part.replace(/[aeiouAEIOU]/g, (letter) => accents[letter]);
  });
  return `[${transformed.join("")}~~~]`;
}

console.log(pseudoLocalize("Pay {amount} now"));
console.log(pseudoLocalize("Hello, {name}"));
[Pày {amount} nøw~~~]
[Hëllø, {name}~~~]

这个正则只适合示范简单占位符,不能安全处理嵌套的 ICU 消息。真实项目应在消息解析后的语法树上转换,或使用消息工具链提供的伪区域设置,避免破坏复数与选择分支。

陷阱

把语言、地区和用户位置当成同一个值

拼接可翻译句子

把格式化字符串当作存储格式

用回退语言掩盖目录缺陷

只翻转文本,不检查双向内容

AI 时代

添加阿拉伯语之类的 RTL 区域设置很适合作为端到端的智能体任务。智能体可以新增 ar 目录,与现有区域设置比较消息键、占位符类型和复数分支,接入选定的回退策略,并让代表性页面使用 RTL 方向和双向订单编号。产品选择仍需明确记录:例如,缺失法律文案可以阻止发布,而缺失后台标签可以使用指定的回退语言。确定这些选择后,智能体可以把它们写进目录校验和渲染测试,让后续区域设置遵守同一契约。

深入 区域设置协商是产品策略

区域设置协商是产品策略

区域设置协商把一个有顺序的请求列表映射到产品真正支持的目录。HTTP Accept-Language 可以表达偏好和权重,但账号设置或带语言前缀的 URL 往往更明确。应用应先决定输入优先级,再使用经过验证的匹配实现;不要让不同页面各写一套猜测规则。

规范化、扩展和匹配是三个不同步骤。规范化统一标签形式,Intl.Locale.prototype.maximize() 可以根据区域数据补充可能的书写系统与地区,而匹配只允许返回已部署的区域设置。补充出的可能值是算法结果,不是用户身份事实,不能写回账号偏好。

支持列表应是允许列表,不是目录路径模板。把未验证的请求标签直接插入 import() 或文件路径,会把拼写错误变成加载异常,也可能扩大路径遍历和意外模块加载的攻击面。先匹配到内部已知 ID,再由固定映射定位资源。

Unicode 扩展可以表达日历、排序规则或数字系统偏好,例如标签中的 u-ca-*u-nu-*。应用必须决定哪些扩展受支持,并把不支持的选项安全地降级。不能因为标签语法有效,就假定所有下游库都会保留或理解扩展。

回退更像一张受控图,而不是不断删除标签尾部的字符串循环。zh-Hant-HK 可能按产品内容策略回退到 zh-Hant,再回退到默认语言;这种关系应由目录配置表达。构建时检查回退图无环、终点存在,并让运行时记录最终命中的目录。

关注点输入负责方
用户偏好账号、URL、请求头产品策略
标签规范化BCP 47 标签标准库
支持项匹配请求列表、部署目录国际化层
展示时区用户或业务上下文领域模型
交易货币订单或报价领域模型

服务端与客户端的一致性

服务端渲染应把已解析的区域设置、时区和消息目录版本作为页面状态传给客户端。客户端若重新读取浏览器默认值,首次水合可能改变文本、节点数量或方向,引起警告和可见跳动。后续用户切换则应作为一次明确的状态转换执行。

缓存键必须包含真正影响响应的国际化维度。若 HTML 因区域设置而不同,缓存至少要区分已解析的区域设置,而不能只依赖原始请求头;若时区也影响首屏,它同样属于缓存契约。反过来,不影响输出的完整请求头不应直接制造无界缓存变体。

语言切换可能需要异步加载目录。提交新状态前先验证目录完整性,并避免较早请求晚到后覆盖较新的用户选择。根元素的 langdir、消息目录和格式化上下文应以同一个已确认的区域设置一起更新。

消息目录是一份接口

消息键和占位符共同构成代码与译文之间的接口。删除键、改名或改变参数类型都应像 API 变更一样经过迁移。只比较目录是否包含相同键还不够,还要检查每个消息引用的参数名、复数变量和允许的富文本槽位。

占位符名称应表达领域含义,例如 {itemCount}{dueDate}{customerName}{value1} 或按位置编号的参数会迫使译者回看源码,也容易在语序变化时接错值。参数类型应在提取元数据或类型定义中可见,使日期不会被当成已经格式化的任意字符串。

复数消息必须覆盖目标区域设置可能产生的类别,并提供 other=0 这样的精确数字分支表达产品文案规则,zero 则是区域语法类别,两者不是同一个条件。审查时要分别测试精确分支、类别分支和回退分支。

富文本消息应保留译者调整整句结构的能力,同时限制可执行内容。安全的模型通常让代码提供一组已知组件槽位,解析器只允许消息引用这些槽位。译文不能决定任意标签、事件处理器或链接协议,参数值也不获得绕过转义的特权。

目录发布需要版本一致性。代码若先部署新键而旧目录仍被 CDN 缓存,会出现短暂缺键;目录先删除旧键也会破坏尚未更新的客户端。可以采用内容哈希、兼容窗口或原子清单,让代码和资源明确声明彼此兼容的版本。

契约部分构建时检查运行时处理
消息键各目录集合一致记录缺键与回退
占位符名称和类型一致拒绝缺失的必需值
复数分支覆盖可能类别始终保留 other
富文本槽位只引用允许组件转义文本参数
资源版本清单与代码兼容原子切换已验证目录

翻译上下文与所有权

一个键需要说明出现位置、受众、字符限制和参数含义。没有上下文的 open 可能是动词、形容词或状态,机器翻译与人工翻译都只能猜。截图可以辅助理解,但文字化的语义说明更容易进入版本控制和自动检查。

产品团队拥有消息意图和参数契约,语言专家拥有目标语言表达,工程团队拥有加载、验证和安全边界。自动翻译可以生成候选文本,不能证明法律术语、礼貌程度或术语一致性正确。高风险流程需要明确的人工语言审查和发布记录。

时间、数字与排序的语义边界

时间数据至少要区分瞬间、日历日期和带地区规则的本地时间。航班在当地 09:00 起飞与日志事件发生于某个 UTC 瞬间不是同一种值。夏令时切换会产生不存在或重复的本地时刻,因此不能只给字符串附上时区名称就认为转换完成。

金额由数值与货币代码共同定义。100 本身不能说明是日元、欧元还是最小货币单位,区域设置也不能补出这个业务事实。格式化选项中的舍入和小数位应服从领域契约,不能仅依据界面看起来整齐来改变结算值。

百分比同样需要明确数据约定。Intl.NumberFormat 的百分比样式会把 0.25 显示为 25%,所以把已经表示 25 的值再传入会得到 2,500% 一类错误。接口应说明输入是比例还是百分点,并在边界测试中覆盖。

Intl.Collator 适合面向人的文本排序与比较,但区域敏感比较不应决定数据库主键、权限或协议标识符是否相等。同一批名称在不同区域设置下可能改变排序顺序。需要稳定分页时,应保存稳定的次级排序键,不能只依赖显示排序结果。

搜索、大小写转换和文本分段也受到语言影响。土耳其语的大小写、组合字符和由多个码点组成的用户可见字符都会击穿 ASCII 假设。字符限制若面向用户,通常应按字素簇或产品定义计数,而不是按 UTF-16 代码单元长度。

双向文本不只是镜像布局

Unicode 双向算法根据字符属性排列混合方向文本,HTML 的 dir 提供段落的基础方向。一个 RTL 页面中的拉丁订单号仍按 LTR 显示,但周围标点可能受到邻近字符影响。隔离动态片段能阻止其方向性改变外部句子的排列。

当动态文本的方向未知时,<bdi> 为片段建立隔离;独立输入或纯文本容器也可以评估 dir="auto" 是否符合产品需求。已知方向的结构则应明确设置,而不是让首个强方向字符偶然决定。复制、选择和屏幕阅读器测试比静态截图更容易暴露边界错误。

CSS 逻辑属性把内联轴和块轴作为布局语义。它们能让间距、边框和定位随书写模式变化,但不会自动修正绝对坐标图、画布绘制或图片中的文字。组件契约应说明哪些视觉元素依赖方向,并为这些元素提供受控变体。

安全评审也要考虑双向控制字符。日志、源代码片段和账号标识中的不可见控制字符可能让视觉顺序与存储顺序不同。不要粗暴删除所有 RTL 字符;应保留合法语言内容,同时在安全敏感标识符和诊断界面中可视化或限制控制字符。

测试国际化契约

测试矩阵应按行为选择代表,而不是为每个语言复制同一条快乐路径。英语能覆盖基本目录,法语能暴露 0 的复数类别,阿拉伯语覆盖 RTL 与多类别复数,简体和繁体中文覆盖书写系统回退,伪区域设置则放大长度和未提取文本问题。

单元测试验证区域解析、回退图、键集合、占位符和格式化输入。集成测试从服务端响应走到客户端水合,确认 langdir、目录版本和时区保持一致。端到端测试再检查语言切换、持久化、键盘顺序、截断和动态双向内容。

对标准库输出做断言时,优先验证语义不变量,例如包含正确货币、复数分支和日期部分。若产品确实要求逐字符快照,就要固定 Node 与 ICU 版本,并把区域数据升级当作可审查变更。否则,合法的标点或空格更新可能造成没有业务意义的失败。

目录测试应故意删除键、改错占位符并模拟资源加载失败,证明告警和回退真的生效。仅测试完整目录无法发现静默失败路径。并发切换测试还要让先发请求后返回,确认过期目录不会覆盖最新选择。

伪本地化不是语言质量审查。它能发现硬编码文本、空间不足和占位符破坏,却不能验证术语、语气、语法或文化含义。发布前仍需目标语言使用者检查关键流程,并让无障碍测试覆盖语言声明、朗读顺序和可访问名称。

失败行为也是接口

无效 BCP 47 标签可能让 Intl 构造函数抛出 RangeError,消息目录加载也可能因网络或版本问题失败。应用应在请求边界验证外部输入,并把用户可恢复的选择错误与部署缺陷区分开。不能用一个宽泛的 catch 把所有失败都改成默认语言。

不受支持的区域设置与语法无效的标签不是同一情况。前者可以按产品回退策略处理,后者通常表示输入或实现错误。日志应保留经过清理的请求标签和失败阶段,但不能把完整请求头或消息参数无条件写入。

格式化调用也需要领域级错误策略。货币代码、时区名称或消息参数不合法时,结账流程不应悄悄省略金额;展示失败可以进入明确的恢复界面,并阻止依赖该值的提交操作。错误文本本身也必须来自一个保证可用的最小目录。

加载中的界面应避免闪现源语言。可以保留上一份完整目录、显示与语言无关的骨架,或在服务端提前提供所需消息;选择取决于产品交互。关键约束是不能把半份新目录与旧格式化上下文组合成可提交页面。

测试要断言失败分类和恢复结果,而不只断言最终出现某段默认文本。这样才能证明缺键、非法标签、资源超时和过期切换分别走到预期路径。可观测性也应使用同一分类,便于判断问题来自内容、输入、网络还是代码。

发布前的最小证据

一项区域设置上线前,应能给出一组可重复的证据。证据既包括自动检查,也包括对真实内容的人工判断;只有「目录文件存在」不构成完成标准。

  1. 所有消息目录通过键、占位符、复数分支和富文本槽位校验。
  2. 固定运行时上的格式化测试覆盖金额、时间边界、百分比和排序策略。
  3. 服务端与客户端对区域设置、时区、目录版本和方向的解析一致。
  4. 真实语言审查覆盖结账、认证、错误、通知和法律文本等关键路径。
  5. RTL、伪本地化、键盘与辅助技术测试没有阻断性问题。

事故诊断需要记录结构化上下文,但不能记录敏感消息参数。可记录请求区域设置、已解析区域设置、目录版本、消息键、回退命中和时区;用户姓名、自由文本和令牌不应随翻译错误进入日志。这样既能定位缺键,也不会把国际化遥测变成新的数据泄漏渠道。

延伸阅读

检查点

4个问题 · 1 道输出预测题 · 1 道找错题

复制为 Markdown 面试题库 在 GitHub 上编辑 报告错误 讲清楚了吗?