国际化(i18n)和本地化(l10n)实战
如果你的网站需要支持多语言,就需要做国际化和本地化。这两个概念经常混用,但含义不同:
- i18n(Internationalization):使产品能适应不同语言和地区
- l10n(Localization):为特定语言和地区做适配
这篇文章以 React 项目为例,聊聊多语言支持的实际实现。
项目结构
src/
i18n/
index.ts # i18n 配置
locales/
zh-CN.json # 简体中文
en-US.json # 英文
ja-JP.json # 日文
方案选型
react-i18next
最成熟的 React i18n 方案:
pnpm add react-i18next i18next i18next-browser-languagedetector
// src/i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import zhCN from './locales/zh-CN.json';
import enUS from './locales/en-US.json';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
'zh-CN': { translation: zhCN },
'en-US': { translation: enUS },
},
fallbackLng: 'zh-CN',
interpolation: {
escapeValue: false, // React 已经处理了 XSS
},
detection: {
order: ['localStorage', 'navigator', 'htmlTag'],
},
});
使用方式
import { useTranslation } from 'react-i18next';
function LoginPage() {
const { t } = useTranslation();
return (
<div>
<h1>{t('login.title')}</h1>
<input placeholder={t('login.username')} />
<input placeholder={t('login.password')} />
<button>{t('login.submit')}</button>
</div>
);
}
翻译文件
// zh-CN.json
{
"login": {
"title": "登录",
"username": "用户名",
"password": "密码",
"submit": "登录",
"forgot_password": "忘记密码?"
}
}
// en-US.json
{
"login": {
"title": "Sign In",
"username": "Username",
"password": "Password",
"submit": "Sign In",
"forgot_password": "Forgot password?"
}
}
带变量的翻译
{
"post": {
"comment_count": "{{count}} 条评论",
"published_at": "发布于 {{date}}"
}
}
t('post.comment_count', { count: 42 }) // "42 条评论"
t('post.published_at', { date: '2025-01-15' }) // "发布于 2025-01-15"
复数处理
不同语言的复数规则不同(中文不分单复数,英语分,俄语有三种形式):
{
"post": {
"comment_count_one": "{{count}} comment",
"comment_count_other": "{{count}} comments"
}
}
i18next 会根据当前语言自动选择正确的复数形式。
切换语言
import { useTranslation } from 'react-i18next';
function LanguageSwitcher() {
const { i18n } = useTranslation();
return (
<select
value={i18n.language}
onChange={(e) => i18n.changeLanguage(e.target.value)}
>
<option value="zh-CN">中文</option>
<option value="en-US">English</option>
<option value="ja-JP">日本語</option>
</select>
);
}
Next.js 的 i18n
如果用 Next.js,有内置的 i18n 方案:
// next.config.js
module.exports = {
i18n: {
locales: ['zh-CN', 'en-US', 'ja-JP'],
defaultLocale: 'zh-CN',
localeDetection: true,
},
};
// pages/blog/[id].tsx
import { useRouter } from 'next/router';
export default function BlogPost() {
const router = useRouter();
const { locale } = router;
return <div>当前语言: {locale}</div>;
}
App Router 下用 next-intl:
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
export default async function LocaleLayout({
children,
params: { locale },
}) {
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
本地化不只是翻译
除了文本翻译,本地化还包括:
日期和时间
new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(new Date('2025-01-15'));
// "2025年1月15日"
new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(new Date('2025-01-15'));
// "January 15, 2025"
数字和货币
new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
}).format(1234.56);
// "¥1,234.56"
new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY',
}).format(1234.56);
// "¥1,235"
相对时间
new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' }).format(-1, 'day');
// "昨天"
镜像布局
阿拉伯语和希伯来语是从右到左(RTL)书写的:
[dir="rtl"] .sidebar {
margin-left: 0;
margin-right: 1rem;
}
用 CSS 逻辑属性可以自动处理:
.sidebar {
margin-inline-start: 1rem; /* LTR: margin-left, RTL: margin-right */
}
不要过度国际化
如果你的网站只面向中文用户,不需要做国际化。国际化增加了维护成本——每新增一个功能,翻译文件就要同步更新。
建议:
- 确定目标用户和语言
- 先做好一套语言的质量
- 需要的时候再扩展其他语言
- 用翻译管理平台(如 Crowdin、Lokalise)而不是手动维护 JSON 文件
国际化不是技术难点,难点在于翻译质量和维护成本。技术上 react-i18next 已经很成熟了,开箱即用。
ReactTypeScriptNext.js
返回首页