为什么你的国际化方案总是「差点意思」?
很多团队在项目初期为了快速上线,直接把中文硬编码在组件里,等到需要支持多语言时,才发现要改的地方遍布整个代码库。更麻烦的是,翻译文案散落在各个文件,产品经理改个文案,开发要全局搜索替换,一不小心就漏掉或改错。
我们需要的不仅仅是一个 t('key') 函数,而是一套可维护、可扩展、自动化的国际化工作流。本文将带你从零搭建一套生产级的前端国际化方案,基于 React + i18next,但核心思想适用于任何框架。
第一步:选择合适的技术栈
前端国际化库有很多,比如 react-intl、vue-i18n、i18next 等。我们选择 i18next 因为它:
- 框架无关,支持 React、Vue、原生 JS
- 功能全面:插值、复数、上下文、嵌套
- 生态丰富:语言检测、后端加载、缓存
- 社区活跃,文档详尽
安装核心依赖:
npm install i18next react-i18next i18next-browser-languagedetector i18next-http-backend
第二步:设计翻译文件的组织结构
不要把所有翻译放在一个巨大的 JSON 里,那样难以维护。推荐按「命名空间 (namespace)」拆分,通常按页面或功能模块划分:
public/locales/
en/
common.json
home.json
user.json
zh/
common.json
home.json
user.json
每个文件内部使用嵌套结构,便于管理:
// public/locales/zh/home.json
{
"title": "欢迎回来,{{name}}",
"description": "今天是个好日子",
"actions": {
"save": "保存",
"cancel": "取消"
}
}
💡 注意:key 的命名要有意义,避免 text1、text2 这种。推荐使用语义化的路径,如 home.actions.save。
第三步:初始化 i18next 实例
创建一个 i18n.js 配置文件:
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpBackend from 'i18next-http-backend';
i18n
.use(HttpBackend) // 从服务器加载翻译文件
.use(LanguageDetector) // 检测用户语言
.use(initReactI18next) // 绑定 React
.init({
fallbackLng: 'zh', // 默认语言
supportedLngs: ['zh', 'en'],
ns: ['common', 'home', 'user'], // 命名空间列表
defaultNS: 'common',
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json', // 加载路径
},
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
caches: ['localStorage', 'cookie'], // 缓存用户选择
},
interpolation: {
escapeValue: false, // React 已经处理了 XSS
},
react: {
useSuspense: true, // 配合 Suspense 使用
},
});
export default i18n;
在入口文件引入:
// index.js
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
import './i18n';
import App from './App';
ReactDOM.render(
<Suspense fallback={<div>Loading...</div>}>
<App />
</Suspense>,
document.getElementById('root')
);
第四步:在组件中使用翻译
使用 useTranslation Hook:
import React from 'react';
import { useTranslation } from 'react-i18next';
function HomePage() {
const { t, i18n } = useTranslation('home'); // 指定命名空间
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
};
return (
<div>
<h1>{t('title', { name: '张三' })}</h1>
<p>{t('description')}</p>
<button>{t('actions.save', { ns: 'common' })}</button>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('zh')}>中文</button>
</div>
);
}
💡 注意:当使用多个命名空间时,可以在 useTranslation(['home', 'common']) 中传入数组,然后通过 t('actions.save', { ns: 'common' }) 或 t('common:actions.save') 来指定。
第五步:处理复数与动态变量
i18next 内置了强大的复数处理,只需在翻译文件中使用 one、other 等后缀:
// public/locales/en/home.json
{
"itemCount": "You have {{count}} item",
"itemCount_plural": "You have {{count}} items"
}
// public/locales/zh/home.json
{
"itemCount": "你有 {{count}} 个物品"
}
使用:
<p>{t('itemCount', { count: 5 })}</p>
i18next 会根据语言自动选择正确的复数形式(中文没有复数变化,所以只写一个即可)。
第六步:自动化提取与翻译工作流
手动维护翻译文件容易遗漏。推荐使用 i18next-parser 自动扫描代码中的 t() 调用,提取 key 并生成 JSON 文件。
安装:
npm install i18next-parser --save-dev
创建配置文件 i18next-parser.config.js:
module.exports = {
locales: ['zh', 'en'],
output: 'public/locales/$LOCALE/$NAMESPACE.json',
input: ['src/**/*.{js,jsx,ts,tsx}'],
sort: true,
createOldCatalogs: false,
keepRemoved: false,
lexers: {
js: ['JavascriptLexer'],
jsx: ['JsxLexer'],
default: ['JavascriptLexer'],
},
defaultValue: (locale, namespace, key) => {
// 中文环境使用 key 作为默认值,英文环境留空
return locale === 'zh' ? key : '';
},
};
在 package.json 中添加脚本:
{
"scripts": {
"i18n:extract": "i18next-parser"
}
}
运行 npm run i18n:extract,它会扫描代码并更新 JSON 文件。
💡 注意:提取的 key 默认使用 t('key') 中的字符串。如果使用动态 key,如 t(variable),解析器无法识别,需要手动维护或使用 t('key', { defaultValue: '...' }) 并提供注释。
第七步:与翻译平台集成
对于大型项目,推荐将翻译文件同步到专业平台(如 Crowdin、Lokalise)。以 Crowdin 为例:
- 在 Crowdin 创建项目,上传源语言文件(如
zh) - 配置 GitHub 集成,自动同步
- 翻译完成后,Crowdin 会生成 PR 合并到仓库
这样,产品经理可以直接在平台上修改文案,翻译团队协作翻译,开发只需合并 PR。
第八步:优化加载性能
默认情况下,i18next 会一次性加载所有命名空间。对于大型应用,可以按需加载:
import { useTranslation } from 'react-i18next';
function UserPage() {
const { t } = useTranslation('user', { useSuspense: false });
// 当组件挂载时,i18next 会自动加载 user 命名空间
return <div>{t('profile')}</div>;
}
也可以预加载关键命名空间:
i18n.loadNamespaces(['common', 'home']);
常见坑与解决方案
- XSS 风险:i18next 默认转义 HTML,如果必须渲染 HTML,使用
组件:
“jsx import { Trans } from 'react-i18next'; “
- 语言切换后组件不更新:确保使用
useTranslationHook 或withTranslationHOC,它们会订阅语言变化。
- 日期、货币格式化:使用
IntlAPI 或 i18next 的格式化插件:
“javascript import i18next from 'i18next'; i18next.services.formatter.add('currency', (value, lng) => { return new Intl.NumberFormat(lng, { style: 'currency', currency: 'USD' }).format(value); }); “
- SEO 与 SSR:如果使用 Next.js,需要在服务端初始化 i18n 并传递语言。参考
next-i18next库。
总结
一套好的国际化方案应该具备:
- 清晰的资源组织:按命名空间拆分,语义化 key
- 自动化工具链:提取、翻译、同步一体化
- 性能优化:按需加载,避免阻塞渲染
- 团队协作:产品、翻译、开发各司其职
按照本文的步骤,你可以快速搭建起生产级的国际化工作流。下一步,可以探索更高级的主题,如:
- 使用 TypeScript 为翻译 key 提供类型提示
- 结合 CI/CD 自动检查未翻译的 key
- 实现伪翻译(pseudo-localization)测试 UI 布局
希望这篇文章能帮你少走弯路,让国际化不再是难题。