在 2026 年的全球 Web 应用市场中,超过 72% 的用户更倾向于使用母语界面的产品(Common Sense Advisory 数据),而支持多语言的 SaaS 产品平均收入比单语言版本高出 47%。国际化(Internationalization,简称 i18n)早已不是「翻译几个字符串」那么简单——它涉及日期格式、数字分隔符、复数规则、文本方向(RTL)、动态内容翻译、SEO 多语言路由等一整套工程化挑战。如果你正在构建面向全球用户的 Web 应用,i18n 是你绕不过去的基础设施。
本文不是一份「安装 i18next 然后调 $t()」的入门教程,而是从方案选型、ICU 消息格式、工程化架构到性能优化,系统性地讲解如何构建一个生产级的多语言 Web 应用。
🔧 一、i18n 方案选型:三大主流库深度对比
1.1 为什么 string.replace() 不够用
很多开发者的第一反应是用一个 JSON 文件存翻译,然后写个简单的替换函数:
// ❌ 错误写法:手写的简陋 i18n
const messages = {
'zh-CN': { greeting: '你好,{{name}}!你有 {{count}} 条消息。' },
'en': { greeting: 'Hello, {{name}}! You have {{count}} messages.' }
};
function t(key, params) {
let str = messages[currentLang][key];
for (const [k, v] of Object.entries(params)) {
str = str.replace(`{{${k}}}`, v);
}
return str;
}
这种方式在原型阶段可以工作,但一旦进入生产环境就会遇到一系列致命问题:
- ❌ 不支持复数规则:中文没有复数变化,但英文有
1 messagevs2 messages,阿拉伯语有 6 种复数形式 - ❌ 不支持日期/数字格式化:
2026-06-01在中文是「2026年6月1日」,在德文是「01.06.2026」 - ❌ 不支持嵌套和上下文:同一个词在不同语境下翻译不同
- ❌ 不支持翻译文件分包:所有翻译塞在一个文件里,应用越大加载越慢
⚠️ 警告: 如果你的应用只需要支持中英文、且没有复数和日期格式化需求,手写方案勉强可用。但只要需求稍微复杂一点,你就需要一个专业的 i18n 库。
1.2 三大主流方案对比
| 特性 | i18next | Vue I18n (vue-i18n) | FormatJS (react-intl) |
|---|---|---|---|
| 框架 | 通用(React/Vue/Svelte/Vanilla) | Vue 专属 | React 专属 |
| ICU 消息格式 | ✅ 通过插件 | ✅ 原生支持 | ✅ 原生支持 |
| 复数规则 | ✅ 内置 | ✅ 内置 | ✅ 内置 |
| 日期/数字格式化 | ✅ Intl API | ✅ Intl API | ✅ Intl API |
| 命名空间/分包 | ✅ 原生 | ✅ 原生 | ❌ 需手动管理 |
| 翻译文件加载 | ✅ 异步加载 | ✅ 异步加载 | ✅ 异步加载 |
| SSR 支持 | ✅ | ✅ | ✅ |
| TypeScript | ✅ 类型安全 | ✅ 类型安全 | ✅ 类型安全 |
| 生态/插件 | ⭐⭐⭐⭐⭐ 最丰富 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 包大小 | ~18KB (gzip) | ~12KB (gzip) | ~22KB (gzip) |
| 学习曲线 | 中等 | 低 | 中等 |
| 适用场景 | 多框架项目/通用需求 | Vue 项目首选 | React 项目首选 |
⚡ 关键结论: Vue 项目选 Vue I18n,React 项目选 FormatJS,多框架项目或需要最大灵活性选 i18next。三者在核心能力上差异不大,主要区别在于框架集成度和生态。
1.3 ICU 消息格式:i18n 的灵魂
ICU(International Components for Unicode)消息格式是国际化翻译的标准格式,它定义了如何处理变量插值、复数、选择等复杂场景。三大库都支持或部分支持 ICU 格式。
// ✅ 正确写法:ICU 消息格式示例
const icuMessages = {
// 简单插值
greeting: '你好,{name}!',
// 复数规则(英文有 6 种复数形式:zero, one, two, few, many, other)
messageCount: '{count, plural, =0 {没有消息} one {1 条消息} other {{count} 条消息}}',
// 选择规则(根据性别选择不同文案)
userStatus: '{gender, select, male {他在线} female {她在线} other {用户在线}}',
// 日期格式化
publishDate: '发布于 {date, date, long}',
// 数字格式化
price: '价格:{amount, number, ::currency/CNY}',
// 复杂嵌套
orderSummary: '{count, plural, =0 {您没有订单} other {您有 # 个订单,总计 {total, number, ::currency/CNY}}}'
};
📌 记住: ICU 格式中的
#符号在复数上下文中代表复数变量的值。这是一个非常实用的语法糖,避免了重复传参。
🚀 二、生产级 i18n 工程化架构
2.1 翻译文件组织与按需加载
在大型应用中,将所有翻译塞在一个文件里会导致首屏加载大量无用的翻译数据。正确的做法是按**命名空间(Namespace)**分包,并实现异步加载。
// ✅ 正确写法:i18next 命名空间分包 + 异步加载
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
// 使用 HTTP 后端异步加载翻译文件
.use(HttpBackend)
// 自动检测用户语言
.use(LanguageDetector)
// 集成 React
.use(initReactI18next)
.init({
// 支持的语言列表
supportedLngs: ['zh-CN', 'en', 'ja', 'de', 'ar'],
// 回退语言
fallbackLng: 'en',
// 默认命名空间
defaultNS: 'common',
// 需要加载的命名空间列表
ns: ['common', 'auth', 'dashboard', 'settings'],
backend: {
// 翻译文件的加载路径
// {{lng}} 会被替换为语言代码,{{ns}} 会被替换为命名空间
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
interpolation: {
escapeValue: false, // React 已经做了 XSS 防护
},
// 检测顺序:先查 URL 参数,再查 Cookie,再查浏览器设置
detection: {
order: ['querystring', 'cookie', 'navigator'],
lookupQuerystring: 'lang',
lookupCookie: 'i18n_lang',
caches: ['cookie'],
},
});
export default i18n;
翻译文件的目录结构如下:
public/
locales/
zh-CN/
common.json // 通用翻译:按钮、导航、通用文案
auth.json // 认证相关:登录、注册、忘记密码
dashboard.json // 仪表盘页面
settings.json // 设置页面
en/
common.json
auth.json
dashboard.json
settings.json
ja/
common.json
...
// public/locales/zh-CN/common.json
{
"nav": {
"home": "首页",
"about": "关于我们",
"contact": "联系我们",
"login": "登录"
},
"actions": {
"save": "保存",
"cancel": "取消",
"delete": "删除",
"confirm": "确认"
},
"messages": {
"loading": "加载中...",
"error": "操作失败,请重试",
"success": "操作成功",
"noData": "暂无数据"
}
}
// public/locales/en/common.json
{
"nav": {
"home": "Home",
"about": "About Us",
"contact": "Contact",
"login": "Log In"
},
"actions": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"confirm": "Confirm"
},
"messages": {
"loading": "Loading...",
"error": "Operation failed. Please try again.",
"success": "Operation successful",
"noData": "No data available"
}
}
在 Vue 3 中使用 Vue I18n 的分包配置:
// ✅ 正确写法:Vue I18n 异步加载配置
import { createI18n } from 'vue-i18n';
// 仅加载当前语言的 common 命名空间(首屏最小化)
const loadedLanguages = {};
function setI18nLanguage(lang) {
i18n.global.locale.value = lang;
document.querySelector('html').setAttribute('lang', lang);
// 设置文档方向(RTL 语言如阿拉伯语、希伯来语)
document.querySelector('html').setAttribute('dir',
['ar', 'he', 'fa', 'ur'].includes(lang) ? 'rtl' : 'ltr'
);
return lang;
}
export async function loadLanguageAsync(lang) {
// 如果已经加载过该语言,直接切换
if (i18n.global.locale.value === lang) {
return setI18nLanguage(lang);
}
// 如果该语言的翻译文件已加载过,直接切换
if (loadedLanguages[lang]) {
return setI18nLanguage(lang);
}
// 异步加载翻译文件
const messages = await import(`./locales/${lang}/common.json`);
i18n.global.setLocaleMessage(lang, messages.default);
loadedLanguages[lang] = true;
return setI18nLanguage(lang);
}
const i18n = createI18n({
legacy: false, // 使用 Composition API 模式
locale: 'zh-CN',
fallbackLocale: 'en',
messages: {}, // 初始为空,运行时异步加载
});
2.2 复数规则与复杂消息处理
不同语言的复数规则差异巨大,这是 i18n 中最容易出错的地方。
// ✅ 正确写法:处理不同语言的复数规则
import { useTranslation } from 'react-i18next';
function NotificationBell({ count }) {
const { t } = useTranslation('common');
return (
<div className="notification-bell">
<span>🔔</span>
{/* ICU 复数格式:自动根据语言规则选择正确的形式 */}
<span>{t('notifications', { count })}</span>
</div>
);
}
// 翻译文件示例:
// zh-CN: "{count, plural, =0 {没有通知} other {# 条通知}}"
// en: "{count, plural, =0 {No notifications} one {1 notification} other {# notifications}}"
// ja: "{count, plural, other {# 件の通知}}"(日语没有复数变化)
// ar: "{count, plural, =0 {لا إشعارات} one {إشعار واحد} two {إشعاران} few {# إشعارت} many {# إشعاراً} other {# إشعار}}"(阿拉伯语有 6 种形式)
💡 提示: 中文、日文、韩文等东亚语言没有复数变化,所以在
plural规则中只需要other形式。但英文需要one和other,阿拉伯语需要全部 6 种形式。使用 ICU 格式可以自动处理这些差异。
2.3 日期、数字和货币本地化
日期和数字的格式化是 i18n 中最容易被忽视的部分。不同地区的格式差异非常大:
| 格式项 | 中文(zh-CN) | 英文(en-US) | 德文(de-DE) | 阿拉伯语(ar-SA) |
|---|---|---|---|---|
| 日期 | 2026年6月1日 | June 1, 2026 | 1. Juni 2026 | ١ يونيو ٢٠٢٦ |
| 时间 | 14:30 | 2:30 PM | 14:30 | ٢:٣٠ م |
| 数字 | 1,234,567.89 | 1,234,567.89 | 1.234.567,89 | ١٬٢٣٤٬٥٦٧٫٨٩ |
| 货币 | ¥1,234.57 | $1,234.57 | 1.234,57 € | ١٬٢٣٤٫٥٧ ر.س. |
// ✅ 正确写法:使用 Intl API 进行本地化格式化
function formatLocalized(locale) {
const date = new Date('2026-06-01T14:30:00');
const number = 1234567.89;
const currency = 1234.57;
// 日期格式化
const dateFormatter = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
});
console.log('日期:', dateFormatter.format(date));
// zh-CN: 2026年6月1日星期一
// en-US: Monday, June 1, 2026
// de-DE: Montag, 1. Juni 2026
// 相对时间格式化(如「3 分钟前」)
const relativeFormatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
console.log('相对时间:', relativeFormatter.format(-3, 'minute'));
// zh-CN: 3分钟前
// en-US: 3 minutes ago
// de-DE: vor 3 Minuten
// 数字格式化
const numberFormatter = new Intl.NumberFormat(locale);
console.log('数字:', numberFormatter.format(number));
// zh-CN: 1,234,567.89
// de-DE: 1.234.567,89
// 货币格式化
const currencyFormatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency: locale.startsWith('zh') ? 'CNY' : 'USD',
});
console.log('货币:', currencyFormatter.format(currency));
// zh-CN: ¥1,234.57
// en-US: $1,234.57
// 列表格式化(「A、B 和 C」)
const listFormatter = new Intl.ListFormat(locale, { style: 'long', type: 'conjunction' });
console.log('列表:', listFormatter.format(['苹果', '香蕉', '橘子']));
// zh-CN: 苹果、香蕉和橘子
// en-US: apples, bananas, and oranges
}
formatLocalized('zh-CN');
formatLocalized('en-US');
formatLocalized('de-DE');
📌 记住: 始终使用
IntlAPI 而不是手动拼接日期和数字字符串。IntlAPI 是浏览器原生的国际化引擎,它内置了 CLDR(Unicode Common Locale Data Repository)数据,覆盖了全球 700+ 种语言/地区的格式规则。
💡 三、工程化最佳实践与避坑指南
3.1 翻译 Key 的命名规范
翻译 Key 的命名直接影响项目的可维护性。推荐使用层级命名空间 + 语义化命名:
// ❌ 错误写法:扁平的无意义 Key
const badKeys = {
'txt1': '保存',
'txt2': '取消',
'btn_submit': '提交',
'err_msg': '操作失败',
};
// ✅ 正确写法:层级命名空间 + 语义化 Key
const goodKeys = {
// 结构:模块.组件.用途
'common.actions.save': '保存',
'common.actions.cancel': '取消',
'common.actions.submit': '提交',
'auth.login.title': '登录您的账户',
'auth.login.emailPlaceholder': '请输入邮箱地址',
'auth.login.passwordPlaceholder': '请输入密码',
'auth.login.submitButton': '登录',
'auth.login.errors.invalidCredentials': '邮箱或密码错误',
'auth.login.errors.tooManyAttempts': '登录尝试次数过多,请稍后再试',
'dashboard.stats.totalUsers': '总用户数',
'dashboard.stats.activeUsers': '活跃用户',
'dashboard.stats.newUsersToday': '今日新增',
};
命名规范的几个原则:
- ✅ 按模块分组:
auth.、dashboard.、settings. - ✅ 语义化命名:
actions.save而不是btn1 - ✅ 包含上下文:
auth.login.errors.invalidCredentials而不是error1 - ❌ 避免在 Key 中包含语言信息:不要用
zh_greeting这种命名 - ❌ 避免在翻译文本中硬编码变量:不要存
你好,张三,应该存你好,{name}
3.2 RTL(从右到左)布局支持
阿拉伯语、希伯来语、波斯语等语言的文字方向是从右到左(RTL)。这不仅仅是文字方向的问题,整个 UI 布局都需要镜像翻转。
/* ✅ 正确写法:使用 CSS 逻辑属性实现 RTL 兼容 */
.card {
/* 使用逻辑属性代替物理属性 */
margin-inline-start: 16px; /* LTR 时是 margin-left,RTL 时是 margin-right */
padding-inline-end: 24px; /* LTR 时是 padding-right,RTL 时是 padding-left */
border-inline-start: 3px solid #2563eb; /* 左边框/右边框自动切换 */
/* 文本对齐 */
text-align: start; /* LTR 时左对齐,RTL 时右对齐 */
/* Flexbox 自动支持 RTL */
display: flex;
gap: 12px;
/* flex-direction 会根据 dir 属性自动翻转 */
}
/* 浮动和定位也需要逻辑化 */
.sidebar {
float: inline-start; /* LTR 时 float:left,RTL 时 float:right */
inset-inline-start: 0; /* left: 0 / right: 0 自动切换 */
}
⚠️ 警告: 不要在 CSS 中使用
margin-left、padding-right、text-align: left等物理属性。现代 CSS 的逻辑属性(margin-inline-start、padding-inline-end、text-align: start)可以自动适配 LTR 和 RTL 布局,无需为 RTL 写额外的样式覆盖。
3.3 SEO 多语言路由策略
搜索引擎需要知道你的网站有多个语言版本,否则会把不同语言的页面当成重复内容处理。
<!-- ✅ 正确写法:在 HTML head 中声明多语言版本 -->
<head>
<!-- 告诉搜索引擎当前页面的语言替代版本 -->
<link rel="alternate" hreflang="zh-CN" href="https://jsjson.com/zh-CN/tools" />
<link rel="alternate" hreflang="en" href="https://jsjson.com/en/tools" />
<link rel="alternate" hreflang="ja" href="https://jsjson.com/ja/tools" />
<link rel="alternate" hreflang="x-default" href="https://jsjson.com/tools" />
<!-- Open Graph 多语言 -->
<meta property="og:locale" content="zh_CN" />
<meta property="og:locale:alternate" content="en_US" />
<meta property="og:locale:alternate" content="ja_JP" />
</head>
在 Nuxt 3 中使用 @nuxtjs/i18n 模块自动处理 SEO:
// ✅ 正确写法:Nuxt 3 i18n 配置
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: [
{ code: 'zh-CN', iso: 'zh-CN', name: '简体中文', file: 'zh-CN.json' },
{ code: 'en', iso: 'en-US', name: 'English', file: 'en.json' },
{ code: 'ja', iso: 'ja', name: '日本語', file: 'ja.json' },
],
defaultLocale: 'zh-CN',
strategy: 'prefix_except_default', // 默认语言不带前缀:/tools,其他语言带前缀:/en/tools
seo: true, // 自动生成 hreflang 标签
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'i18n_lang',
redirectOn: 'root', // 仅在首次访问时检测
},
},
});
推荐的 URL 结构:
| 策略 | URL 示例 | 适用场景 |
|---|---|---|
| 子目录(推荐) | /zh-CN/tools、/en/tools |
SEO 最友好,共享域名权重 |
| 子域名 | zh.jsjson.com、en.jsjson.com |
不同语言内容差异大时 |
| 查询参数 | /tools?lang=zh-CN |
不推荐,SEO 不友好 |
⚠️ 警告: 永远不要用 Cookie 或浏览器检测来决定显示什么语言,而不提供 URL 级别的语言标识。搜索引擎爬虫不会携带 Cookie,也无法执行 JavaScript 来检测语言。
3.4 翻译管理流程与协作
在团队协作中,翻译管理是一个经常被忽视但极其重要的工程问题:
// ✅ 正确写法:开发阶段的翻译缺失检测
// i18n-missing-key-logger.ts
import i18n from './i18n';
const missingKeys = new Set();
// 拦截缺失的翻译 Key
i18n.on('missingKey', (lngs, namespace, key) => {
const fullKey = `${namespace}:${key}`;
if (!missingKeys.has(fullKey)) {
missingKeys.add(fullKey);
console.warn(`[i18n] Missing translation: ${fullKey} (${lngs.join(', ')})`);
// 开发环境:发送到翻译管理平台
if (import.meta.env.DEV) {
fetch('/api/i18n/missing', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key,
namespace,
languages: lngs,
context: document.location.pathname,
timestamp: Date.now(),
}),
}).catch(() => {}); // 静默失败,不影响用户体验
}
}
});
// 导出缺失的翻译 Key(CI/CD 集成)
export function getMissingKeysReport() {
return Array.from(missingKeys);
}
推荐的翻译管理流程:
- ✅ 开发阶段:开发者在代码中使用翻译 Key,运行时自动检测缺失的 Key
- ✅ CI/CD 阶段:构建时运行翻译完整性检查脚本,缺失翻译则构建失败
- ✅ 翻译阶段:翻译人员在 Crowdin、Lokalise 或 Phrase 等平台上翻译
- ✅ 上线阶段:翻译文件自动同步到 CDN,支持热更新无需重新构建
3.5 性能优化:翻译文件的加载策略
// ✅ 正确写法:翻译文件的缓存与预加载策略
class TranslationLoader {
constructor() {
this.cache = new Map();
this.loading = new Map();
}
async load(lang, namespace) {
const cacheKey = `${lang}:${namespace}`;
// 1. 内存缓存命中
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
// 2. 防止重复加载
if (this.loading.has(cacheKey)) {
return this.loading.get(cacheKey);
}
// 3. 从 localStorage 缓存中读取
const storageKey = `i18n_${cacheKey}_v2`;
const cached = this.getFromStorage(storageKey);
if (cached) {
this.cache.set(cacheKey, cached);
return cached;
}
// 4. 从网络加载
const loadPromise = fetch(`/locales/${lang}/${namespace}.json`)
.then(res => {
if (!res.ok) throw new Error(`Failed to load ${cacheKey}`);
return res.json();
})
.then(messages => {
// 缓存到内存和 localStorage
this.cache.set(cacheKey, messages);
this.saveToStorage(storageKey, messages);
this.loading.delete(cacheKey);
return messages;
})
.catch(err => {
this.loading.delete(cacheKey);
throw err;
});
this.loading.set(cacheKey, loadPromise);
return loadPromise;
}
getFromStorage(key) {
try {
const raw = localStorage.getItem(key);
if (!raw) return null;
const { data, version } = JSON.parse(raw);
// 版本号不匹配则缓存失效
if (version !== this.getVersion()) {
localStorage.removeItem(key);
return null;
}
return data;
} catch {
return null;
}
}
saveToStorage(key, data) {
try {
localStorage.setItem(key, JSON.stringify({
data,
version: this.getVersion(),
timestamp: Date.now(),
}));
} catch {
// localStorage 空间不足时静默失败
}
}
getVersion() {
// 翻译版本号,每次更新翻译时递增
return '2026.06.01';
}
// 预加载用户可能需要的语言
preloadLanguages(langs, namespaces = ['common']) {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
langs.forEach(lang => {
namespaces.forEach(ns => this.load(lang, ns).catch(() => {}));
});
});
}
}
}
💡 提示: 在生产环境中,翻译文件应该放在 CDN 上并设置合适的缓存头(
Cache-Control: public, max-age=86400)。翻译文件更新时,通过修改版本号来清除缓存,而不是依赖短过期时间。
📊 四、方案选型决策树
根据项目实际情况选择最合适的 i18n 方案:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| Vue 3 项目 | Vue I18n + @nuxtjs/i18n | 框架原生集成,Composition API 支持好 |
| React 项目 | i18next + react-i18next | 生态最丰富,灵活性最高 |
| React 项目(偏简单) | FormatJS (react-intl) | Yahoo 出品,API 设计优雅 |
| 多框架/库项目 | i18next | 框架无关,一次学习到处使用 |
| 只需要中文+英文 | vue-i18n 或 react-i18next | 最简配置,无需复杂功能 |
| 需要翻译管理平台 | i18next + Crowdin | Crowdin 对 i18next 支持最好 |
⚡ 总结与推荐
Web 应用国际化是一个系统工程,不是「装个库翻译几个字符串」就能搞定的。以下是核心要点:
- ✅ 选择合适的 i18n 库:Vue 项目用 Vue I18n,React 项目用 i18next 或 FormatJS
- ✅ 使用 ICU 消息格式:正确处理复数、选择、日期和数字格式化
- ✅ 翻译文件按需加载:按命名空间分包,异步加载,减少首屏体积
- ✅ 使用 CSS 逻辑属性:
margin-inline-start代替margin-left,天然支持 RTL - ✅ SEO 多语言路由:使用子目录策略 +
hreflang标签 - ✅ 翻译完整性检查:CI/CD 集成翻译缺失检测,避免未翻译的 Key 上线
- ✅ 性能优化:翻译文件 CDN 分发 + localStorage 缓存 + 空闲时预加载
⚡ 关键结论: 国际化不是事后补丁,而是需要在项目架构设计阶段就考虑的基础设施。越早引入 i18n,后期改造的成本越低。如果你的项目有任何走向海外的可能性,从第一天就用 i18n 的方式组织你的文案。
相关工具推荐: