国际化i18n
What — 是什么
国际化(i18n,Internationalization 的缩写)是让应用支持多语言和区域习惯的工程实践,本地化(l10n)是具体语言的适配。
核心概念:
- 翻译管理:JSON 资源文件、键值对提取与替换
- ICU MessageFormat:处理复数、性别、变量插值的标准格式
- 区域格式:日期、数字、货币、排序规则
- RTL 布局:阿拉伯语、希伯来语等从右到左的语言
- 动态切换:运行时切换语言,无需刷新页面
关键特性:
- i18n 是架构层面的准备,l10n 是具体语言的翻译
- 翻译 key 提取而非硬编码是第一步
- 日期/数字格式化使用
IntlAPI,不要手动拼接
Why — 为什么
适用场景:
- 面向多地区用户的 SaaS 产品
- 企业级出海项目
- 多语言内容平台
对比方案:
| 维度 | i18next | react-intl | vue-i18n | next-intl |
|---|---|---|---|---|
| 框架 | 框架无关 | React | Vue | Next.js |
| ICU 格式 | 插件支持 | 原生 | 支持 | 支持 |
| 动态加载 | 支持 | 支持 | 支持 | 支持 |
| TypeScript | 插件 | 支持 | 支持 | 支持 |
| 生态 | 最大 | 成熟 | Vue 官方 | Next.js 首选 |
优缺点:
- ✅ 优点:
- 扩大用户覆盖范围
- 翻译与代码分离,非技术人员可维护
IntlAPI 自动处理区域格式
- ❌ 缺点:
- 增加包体积(翻译文件)
- key 管理需要规范
- RTL 布局需要额外适配
How — 怎么用
React + i18next
配置:
// i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import en from './locales/en.json';
import zh from './locales/zh.json';
i18n.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: { en: { translation: en }, zh: { translation: zh } },
fallbackLng: 'en',
interpolation: { escapeValue: false },
detection: {
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
},
});
export default i18n;
翻译文件:
// locales/zh.json
{
"common": {
"confirm": "确认",
"cancel": "取消",
"delete": "删除"
},
"user": {
"greeting": "你好,{{name}}!",
"unread": "你有 {{count}} 条未读消息",
"unread_plural": "你有 {{count}} 条未读消息"
},
"order": {
"status": "订单状态:{{status}}",
"total": "总计:{{amount}}",
"items": "{{count}} 件商品"
}
}
// locales/en.json
{
"common": {
"confirm": "Confirm",
"cancel": "Cancel",
"delete": "Delete"
},
"user": {
"greeting": "Hello, {{name}}!",
"unread": "You have {{count}} unread message",
"unread_plural": "You have {{count}} unread messages"
},
"order": {
"status": "Order status: {{status}}",
"total": "Total: {{amount}}",
"items": "{{count}} item(s)"
}
}
组件中使用:
import { useTranslation } from 'react-i18next';
function UserPanel({ user }: { user: User }) {
const { t, i18n } = useTranslation();
return (
<div>
<p>{t('user.greeting', { name: user.name })}</p>
<p>{t('user.unread', { count: user.unreadCount })}</p>
<select
value={i18n.language}
onChange={(e) => i18n.changeLanguage(e.target.value)}
>
<option value="zh">中文</option>
<option value="en">English</option>
</select>
</div>
);
}
Vue + vue-i18n
// i18n/index.ts
import { createI18n } from 'vue-i18n';
import zh from './locales/zh.json';
import en from './locales/en.json';
const i18n = createI18n({
legacy: false, // Composition API 模式
locale: localStorage.getItem('locale') || navigator.language.startsWith('zh') ? 'zh' : 'en',
fallbackLocale: 'en',
messages: { zh, en },
});
export default i18n;
<template>
<p>{{ t('user.greeting', { name: user.name }) }}</p>
<p>{{ t('user.unread', { count: user.unreadCount }) }}</p>
<select v-model="locale">
<option value="zh">中文</option>
<option value="en">English</option>
</select>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
// 切换语言时持久化
watch(locale, (val) => localStorage.setItem('locale', val));
</script>
区域格式化
// 数字格式化
const numberFmt = new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
});
numberFmt.format(1234.5); // "¥1,234.50"
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(1234.5);
// "$1,234.50"
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(1234.5);
// "1.234,50 €"
// 日期格式化
new Intl.DateTimeFormat('zh-CN', { dateStyle: 'long' }).format(new Date());
// "2026年5月11日"
new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }).format(new Date());
// "May 11, 2026"
// 相对时间
new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' }).format(-1, 'day');
// "昨天"
new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' }).format(-1, 'day');
// "yesterday"
// 列表格式化
new Intl.ListFormat('zh-CN', { style: 'long' }).format(['张三', '李四', '王五']);
// "张三、李四和王五"
new Intl.ListFormat('en-US', { style: 'long' }).format(['Alice', 'Bob', 'Carol']);
// "Alice, Bob, and Carol"
RTL 布局
/* 使用逻辑属性替代物理方向 */
.card {
/* ❌ 物理方向 */
/* margin-left: 16px; */
/* text-align: left; */
/* ✅ 逻辑方向:自动适配 RTL */
margin-inline-start: 16px;
text-align: start;
padding-inline: 16px;
border-inline-end: 1px solid var(--color-border);
}
/* RTL 专属覆盖 */
[dir="rtl"] .icon-arrow {
transform: scaleX(-1); /* 箭头翻转 */
}
// 切换 RTL
function setDirection(lang: string) {
const rtlLanguages = ['ar', 'he', 'fa', 'ur'];
const dir = rtlLanguages.includes(lang) ? 'rtl' : 'ltr';
document.documentElement.setAttribute('dir', dir);
document.documentElement.setAttribute('lang', lang);
}
Next.js i18n(App Router)
// next.config.ts
const nextConfig = {
i18n: {
locales: ['zh', 'en', 'ja'],
defaultLocale: 'zh',
},
};
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
export default async function LocaleLayout({
children, params: { locale },
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const messages = await getMessages();
return (
<html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 翻译 key 丢失 | 新增 key 未同步到所有语言文件 | CI 中用脚本检查 key 完整性 |
| 日期格式硬编码 | 2026-01-15 直接拼接 | 用 Intl.DateTimeFormat |
| 译文溢出 UI | 德语等比中文长 30%+ | UI 预留弹性空间,测试长文本 |
| 切换语言闪烁 | 全量加载所有翻译 | 按需加载语言包 i18next-http-backend |
| 复数形式错误 | 中文无复数,英文有 | 用 ICU MessageFormat 处理复数 |
最佳实践
- 翻译 key 用点分隔命名空间:
user.greeting、order.status - 日期/数字/货币用
IntlAPI,不要手动格式化 - CSS 用逻辑属性(
margin-inline-start)适配 RTL - CI 检查翻译 key 完整性,防止遗漏
- UI 测试长文本(德语/法语文本比中文长 30%+)
面试题
Q1: i18n 和 l10n 的区别是什么?
i18n(Internationalization)是架构层面的准备——让代码具备多语言能力(提取 key、抽象格式化、支持 RTL);l10n(Localization)是具体语言的适配——翻译文案、调整日期数字格式、适配区域习惯。i18n 是前提,l10n 是落地。
Q2: Intl API 有哪些用途?为什么推荐用 Intl 而非手动格式化?
IntlAPI 处理区域敏感的格式化:Intl.NumberFormat(数字/货币)、Intl.DateTimeFormat(日期)、Intl.RelativeTimeFormat(相对时间)、Intl.ListFormat(列表连接)。推荐使用是因为手动格式化无法覆盖所有区域规则(如德语数字用点作千分位),Intl由浏览器内置,零依赖且规则完整。
Q3: RTL 布局如何适配?
使用 CSS 逻辑属性替代物理方向属性(
margin-inline-start替代margin-left,text-align: start替代text-left);根元素设置dir="rtl"自动翻转布局方向;图标箭头等方向性元素用transform: scaleX(-1)翻转;避免在 CSS 中硬编码左右方向。
Q4: 翻译 key 的管理有哪些最佳实践?
使用点分隔命名空间组织 key(
user.greeting、order.status),避免扁平化命名导致冲突;CI 中用脚本检查所有语言文件的 key 完整性;key 名体现语义而非翻译内容(用user.greeting而非user.hello);按模块拆分翻译文件避免单文件过大。
相关链接: