1. 项目概述
国际化(i18n)是现代前端工程中不可或缺的一环。作为经历过多个跨国项目的老前端,我深刻体会到一套健壮的i18n体系对项目可维护性的影响。这次要分享的不仅是简单的多语言切换方案,而是从设计规范到工程化落地的完整体系。
这个方案已经在三个中大型项目中得到验证,支持日均百万级PV的业务场景。核心解决的是传统i18n方案存在的三个痛点:翻译内容散落难以维护、动态内容处理困难、缺乏工程化约束导致的协作混乱。下面我会从设计思路到具体实现,拆解每个关键环节。
2. 核心设计思路
2.1 分层架构设计
我们采用三层结构组织国际化资源:
├── core/ # 核心运行时 ├── locales/ # 语言资源 │ ├── en/ │ ├── zh-CN/ │ └── ja/ └── plugins/ # 构建工具集成这种设计的优势在于:
- 核心逻辑与具体实现解耦,便于适配不同框架
- 语言资源独立目录管理,符合monorepo趋势
- 构建工具通过插件机制接入,不影响业务代码
2.2 资源文件规范
采用YAML作为资源文件格式(相比JSON支持注释和更简洁的结构):
# locales/en/common.yaml buttons: submit: Submit cancel: Cancel errors: required: "{field} is required"关键规范:
- 按业务域划分文件(如common.yaml, product.yaml)
- 使用嵌套结构组织同类词汇
- 变量使用{field}语法标注
- 禁止在key中使用动态拼接
2.3 动态内容处理方案
对于接口返回的动态内容,设计特殊的"@dynamic"命名空间:
# locales/en/dynamic.yaml product: status_@dynamic: 1: Available 2: Sold Out运行时通过特殊处理器转换:
i18n.t(`product.status_${apiStatus}`)3. 工程化实现
3.1 基础运行时实现
核心创建i18n实例的工厂函数:
class I18nCore { constructor(options) { this.locale = options.defaultLocale this.fallbacks = new Map(options.fallbacks) this.resources = new Proxy({}, { get: (target, locale) => { return this.loadLocale(locale) } }) } async loadLocale(locale) { if (!this._cache[locale]) { const res = await import(`../locales/${locale}/*.yaml`) this._cache[locale] = deepMerge(res) } return this._cache[locale] } }关键特性:
- 按需加载语言包
- 支持fallback链(如zh-TW -> zh-CN -> en)
- 基于Proxy的懒加载机制
3.2 Webpack插件实现
开发阶段的关键优化插件:
class I18nWebpackPlugin { apply(compiler) { compiler.hooks.emit.tapAsync('I18nPlugin', (compilation, cb) => { const usedKeys = this.scanSourceFiles() const allKeys = this.loadLocaleKeys() compilation.assets['i18n-report.json'] = { source: () => JSON.stringify({ missing: difference(allKeys, usedKeys), unused: difference(usedKeys, allKeys) }, null, 2), size: () => {...} } cb() }) } }插件功能:
- 构建时扫描未使用的翻译
- 检测代码中的硬编码文本
- 生成翻译覆盖率报告
3.3 CLI工具链
开发配套命令行工具实现:
i18n-cli --scan --locales=src/locales # 输出: # [missing] zh-CN: product.promotion.title # [unused] en: buttons.legacy_confirm核心命令:
- scan:检测代码与资源文件差异
- sync:同步不同语言版本缺失项
- export:导出待翻译的CSV文件
- import:导入翻译平台结果
4. 框架适配层
4.1 React集成方案
创建高阶组件封装上下文:
const I18nProvider = ({ locale, children }) => { const [i18n, setI18n] = useState(createI18n(locale)) useEffect(() => { setI18n(createI18n(locale)) }, [locale]) return ( <I18nContext.Provider value={i18n}> {children} </I18nContext.Provider> ) } const useTranslate = () => { const i18n = useContext(I18nContext) return (key, vars) => i18n.t(key, vars) }4.2 Vue插件实现
通过mixin注入$t方法:
const I18nPlugin = { install(Vue, options) { const i18n = createI18n(options) Vue.mixin({ beforeCreate() { this.$i18n = i18n }, methods: { $t(key, vars) { return this.$i18n.t(key, vars) } } }) } }5. 高级特性实现
5.1 复数处理规则
在资源文件中定义复数形式:
messages: cart_items: one: "{count} item in cart" other: "{count} items in cart"运行时根据Intl.PluralRules处理:
function pluralize(locale, count, variants) { const rule = new Intl.PluralRules(locale).select(count) return variants[rule] || variants.other }5.2 日期/数字格式化
扩展t方法支持自动格式化:
i18n.t('sale.ends', { date: new Date(), price: 2999 }, { date: 'short', price: 'currency' })基于Intl API的格式化器:
const formatters = { currency: new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' }), short: new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'short' }) }6. 性能优化策略
6.1 语言包懒加载
实现分块加载方案:
// 静态声明依赖 const localeMap = { 'en': () => import('./locales/en/core.yaml'), 'zh-CN': () => import('./locales/zh-CN/core.yaml') } // 动态加载逻辑 async function loadLocale(locale) { const [common, product] = await Promise.all([ localeMap[locale](), import(`./locales/${locale}/product.yaml`) ]) return { ...common, ...product } }6.2 内存缓存策略
使用WeakMap缓存实例:
const cache = new WeakMap() function getI18nInstance(locale) { if (!cache.has(locale)) { cache.set(locale, createI18n(locale)) } return cache.get(locale) }7. 测试方案设计
7.1 单元测试重点
验证核心功能:
describe('i18n core', () => { it('should fallback to default locale', () => { const i18n = createI18n({ defaultLocale: 'en', fallbacks: { 'zh-TW': 'zh-CN' } }) expect(i18n.t('buttons.submit')).toEqual('Submit') }) })7.2 E2E测试场景
验证完整流程:
describe('language switch', () => { it('should update page content', async () => { await page.select('#lang-switcher', 'ja') expect(await page.textContent('button')).toEqual('送信') }) })8. 部署与监控
8.1 CI集成检查
在流水线中添加检查:
- name: i18n check run: | npx i18n-cli scan --strict if [ $? -ne 0 ]; then echo "Missing translations detected" exit 1 fi8.2 运行时错误收集
监听翻译缺失错误:
window.addEventListener('i18n:missing', (e) => { trackError({ key: e.detail.key, locale: navigator.language }) })9. 团队协作规范
9.1 开发流程约束
Git工作流要求:
- 禁止在代码中硬编码文本
- 新增功能必须同步更新默认语言资源
- 翻译更新通过独立PR提交
9.2 代码审查要点
重点检查:
- 是否存在未国际化的文本
- 动态key是否符合命名规范
- 变量插值是否安全处理
10. 迁移与升级策略
10.1 旧项目迁移方案
分阶段实施:
- 提取现有文本到资源文件
- 用脚本替换代码中的字符串
- 逐步启用新特性
10.2 破坏性变更处理
通过适配器兼容旧版:
function legacyAdapter(key) { return key.includes('::') ? key.split('::')[1] : key }这套体系在落地过程中最大的体会是:国际化不是简单的文本替换,而是需要从工程架构层面建立完整的工具链和规范。特别是在多人协作项目中,严格的代码约束和自动化检查比技术方案本身更重要。建议在项目初期就引入这套体系,后期迁移的成本会呈指数级增长。