前端国际化(i18n,internationalization 的缩写,i 和 n 之间夹着 18 个字母)是让 Web 应用面向多地区用户的核心能力,也是产品出海、合规与转化率提升的第一道门槛。很多团队一开始以为「把文案抽成字典、按语言切换就行」,上线后才发现:德语的复合词能把按钮撑爆、阿拉伯语要从右往左排、货币和日期格式千差万别、时区一错用户看到的全是「昨天」、SEO 还因为缺少 hreflang 被搜索引擎判为重复内容。把 i18n 当成「翻译问题」来处理的团队,几乎都会在第三个语言上线时返工。本文从浏览器原生的 Intl API 讲起,一步步落地一套可维护的多语言前端架构。
一、先吃透浏览器原生的 Intl API
与其自己拼字符串,不如直接用浏览器内置的 Intl 对象。它覆盖了数字、货币、日期、时区、复数、列表等几乎所有本地化规则,且跟随用户操作系统的区域设置,无需任何第三方依赖。把格式化交给标准库,是 i18n 最稳的第一步。
// 同一份数据,按 locale 输出本地化格式,无需手写判断
const price = 1234.5;
new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(price); // ¥1,234.50
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price); // 1.234,50 €
new Intl.DateTimeFormat('ja-JP', { dateStyle: 'long' }).format(new Date()); // 2026年10月17日
// 复数规则:英语 one/other,阿拉伯语有 zero/one/two/few/many/other
const pr = new Intl.PluralRules('en-US');
pr.select(0); // "zero"
pr.select(1); // "one"
pr.select(2); // "other"
不同地区的格式差异远比想象中大,下面这张表能直观说明为什么不能偷懒用模板拼接:
| 维度 | zh-CN | en-US | de-DE | ja-JP |
|---|---|---|---|---|
| 货币 | ¥1,234.50 | $1,234.50 | 1.234,50 € | ¥1,235 |
| 日期 | 2026/10/17 | 10/17/2026 | 17.10.2026 | 2026/10/17 |
| 复数(1) | 1 件商品 | 1 item | 1 Artikel | 1 件 |
| 列表连接 | A、B、C | A, B, and C | A, B und C | A、B、C |
二、文案与复数:别再手写 if-else
真正的难点不是「你好」怎么翻,而是带变量的句子。比如「购物车里有 N 件商品」,N=0/1/多 在每种语言里的形态都不一样,法语里 0 和 1 还共享一种形态。直接用 Vue 3 生态的 vue-i18n 或 i18next,它们都支持 ICU MessageFormat:
// locale/zh-CN.json
{ "cart": "购物车里有 {count, plural, =0 {0 件商品} one {# 件商品} other {# 件商品}}" }
// locale/en-US.json
{ "cart": "Your cart has {count, plural, =0 {no items} one {# item} other {# items}}" }
// i18next 初始化(React 示例)
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
i18n.use(initReactI18next).init({
lng: navigator.language.split('-')[0] || 'zh',
fallbackLng: 'en',
resources: {}, // 字典见下方懒加载
});
占位符统一用 {name} 形式,配合 CI 里的 i18n lint(如 i18next-scanner + 翻译缺失检测),能避免「翻译把占位符挪丢导致页面崩溃」这类低级事故。
三、时区与日历:别拿服务器时间当本地时间
时间是最容易被 i18n 忽略的雷区。务必遵循一条铁律:服务端统一存 UTC,展示时再按用户时区格式化。前端用 Intl.DateTimeFormat 的 timeZone 选项做转换,而不是信任浏览器默认时区或服务器时区。相对时间(「3 小时前」)用 Intl.RelativeTimeFormat,地区名、语言名用 Intl.DisplayNames 本地化:
// 同一时刻,按用户时区展示,而非服务器时区
new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai', dateStyle: 'medium', timeStyle: 'short'
}).format(new Date('2026-10-17T09:00:00Z')); // 2026年10月17日 17:00
// 相对时间:自动选单位(秒/分/时/天)
new Intl.RelativeTimeFormat('zh-CN').format(-3, 'hour'); // 3 小时前
// 把语言代码变成用户看得懂的名字
new Intl.DisplayNames('zh-CN', { type: 'language' }).of('ja'); // 日语
注意:时区名(如 Asia/Shanghai)要用 IANA 标准名,不要自己维护「+8」这样的偏移量——夏令时会让它瞬间出错。
四、架构落地:字典怎么组织、怎么加载
字典建议按语言拆文件,键名用嵌套结构,并支持按 locale 懒加载,避免把所有语言都打进首屏包:
// 按 locale 动态加载字典,首屏只下载当前语言
async function loadLocale(lng) {
const mod = await import(`../locales/${lng}.json`);
i18n.addResourceBundle(lng, 'translation', mod.default);
await i18n.changeLanguage(lng);
}
// 目录结构
// locales/
// zh-CN.json
// en-US.json
// ja-JP.json
切换语言时记得同步写入 localStorage 与 <html lang> 属性,保证刷新后不丢状态,也利于无障碍与 SEO。
五、方向与布局:RTL 语言(阿拉伯语 / 希伯来语)
阿拉伯语、希伯来语是从右往左书写的,全球有数亿用户。仅翻转文字不够,整个布局都要镜像:导航、间距、图标箭头方向都需要随之反转。正确做法是给 <html> 设置 dir="rtl",CSS 一律使用逻辑属性(logical properties)而非物理方向,这样同一套样式在 LTR/RTL 下都能自动适配:
<html lang="ar" dir="rtl">
/* 用 logical 属性,RTL 下自动镜像,无需额外写一份样式 */
.card {
padding-inline: 16px;
margin-inline-start: 8px;
border-inline-start: 3px solid #3b82f6;
}
/* 箭头图标也要反向:用 scaleX(-1) 或切换图标 */
.next-arrow { transform: scaleX(-1); }
六、SEO 与路由:hreflang 与多语言 URL
多语言站点最容易被搜索引擎误判为重复内容。必须在 <head> 里为每个语言版本声明 alternate 链接,并保留一个 x-default 兜底页:
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh/guide" />
<link rel="alternate" hreflang="en" href="https://example.com/en/guide" />
<link rel="alternate" hreflang="ja" href="https://example.com/ja/guide" />
<link rel="alternate" hreflang="x-default" href="https://example.com/" />
如果是静态多语言站点,可以用 Nginx 反向代理 根据 Accept-Language 做内容协商,把用户导向对应语言目录:
map $http_accept_language $lang {
default en;
~*zh zh;
~*ja ja;
}
server {
location / {
root /var/www/site/$lang;
try_files $uri $uri/ /index.html;
}
}
七、翻译协作:从 JSON 字典到专业 TMS
当语言超过两三种、翻译人员不止开发者时,纯手工维护 JSON 会迅速失控。关键纪律有两条:一是键名要有结构(如 namespace.page.element,例如 cart.checkout.submit),禁止用整句中文当 key(否则改文案就要改代码);二是禁止字符串拼接(「您有」+n+「条消息」在多数语言里语法不成立)。管理方式可随规模升级:
| 方式 | 代表 | 适用阶段 |
|---|---|---|
| 内联 JSON | 手动维护 locales/*.json | 1-3 种语言、开发者自译 |
| gettext / PO | 标准 .po 文件 + 工具 | 需要译员用专业编辑器 |
| 云 TMS | Crowdin / Lokalise / 飞书多语言 | 多语言、多人协作、机器翻译预填 |
云 TMS 的价值在于把「开发者改文案 → 自动提翻 → 译员翻译 → 合并回仓库」串成流水线,并支持机器翻译预填、上下文截图、术语库一致性,避免同一个词在不同页面被翻成两种样子。
八、上线前测试:伪本地化(Pseudolocalization)
等翻译真到位再测试往往太晚。伪本地化是在开发期用「假语言」跑一遍:把每个字符映射成带装饰的变体(如 a→ⓐ)、整体拉长约 30%、加上前后缀标记。这样无需真实翻译,就能提前暴露三类问题:文本被截断(拉长后溢出)、方向/布局在长文本下崩坏、以及占位符 {name} 在翻译中被误改。它是 i18n 质量门里性价比最高的一环。
九、后端也要跟上:多语言内容的存储
前端做本地化,后端存储多语言内容时同样有讲究。字符集必须统一 PostgreSQL 的 UTF-8(含 client_encoding 与数据库、表、列的编码一致),排序规则(collation)按语言选择,避免「明明存了中文却按错误规则排序/比较」。多语言字段常见三种建模:
| 方案 | 结构 | 适用场景 |
|---|---|---|
| 字段后缀 | title_zh / title_en 多列 | 语言少、查询简单 |
| JSONB | title JSONB {‘zh’:’…’,’en’:’…’} | 语言多、需灵活扩展 |
| 独立翻译表 | content + content_i18n(lang,body) | 强一致性、需审计 |
十、常见坑与排查清单
落地 i18n 时,下面这些坑几乎必踩,提前规避能省下大量返工:
| 现象 | 根因 | 修复方式 |
|---|---|---|
| 中文变乱码 | 文件/接口非 UTF-8 | 全链路 UTF-8 + <meta charset=”utf-8″> |
| 占位符错位 | 翻译挪动 {name} | ICU 占位符 + CI lint 强制校验 |
| 时间全错一天 | 误用服务器时区 | 存 UTC,展示用 timeZone 格式化 |
| RTL 布局崩坏 | 用了 left/right | 改用 padding/margin-inline 等逻辑属性 |
| hreflang 被忽略 | 漏 x-default | 补全 alternate 链与 sitemap |
| 首屏体积暴涨 | 打包全部语言 | 按 locale 懒加载字典 |
| 译文风格不一 | 无术语库 | 引入 TMS + 术语一致性检查 |
十一、小结
前端国际化是一套「前端 + 后端 + SEO + 协作流程」的协同工程:用原生 Intl 处理格式与时区、用 ICU/i18next 管文案与复数、用逻辑属性兼容 RTL、用 hreflang 讨好搜索引擎、用伪本地化在开发期拦截缺陷,再让后端以 UTF-8 与合理建模承接多语言数据,最后用 TMS 把翻译流水线和代码解耦。把它当成架构问题而不是翻译问题,多语言扩展才会真正轻松。




