前端国际化方案的架构演进:从JSON Key-Value到ICU MessageFormat

发布时间:2026/7/26 18:58:55

前端国际化方案的架构演进:从JSON Key-Value到ICU MessageFormat 前端国际化方案的架构演进从JSON Key-Value到ICU MessageFormat前端国际化i18n是一项看似简单实则暗藏复杂度的工程。初期方案往往以 JSON Key-Value 方式起步但随着多语言支持数量增长、文案复杂度提升复数、性别、变量插入简单的键值对模式会迅速成为维护瓶颈。本文复盘一个从零搭建到支持 15 种语言的国际化架构演进过程。一、阶段一JSON Key-Value 的起点绝大多数项目的 i18n 都是从 JSON 文件开始的。结构直观、学习成本为零、工具链简单{ welcome: 欢迎, login: 登录, logout: 退出登录, user.greeting: 你好{name} }在 1-2 种语言、文案量小于 200 条的阶段这种方案完全够用。但当支持的语言超过 5 种、单文件文案超过 500 条时问题开始显现复数处理缺失中文没有复数形态但英文有单复数、阿拉伯语有 6 种复数形式。{count}个文件在英文中需要1 file/2 files仅靠字符串替换无法正确处理。上下文丢失翻译人员拿到 Key-Value 文件后无法知道submit这个 key 在页面中是按钮文案、表单标签还是提示文本。同一个英文单词在不同上下文中可能需要不同的翻译。变量插入脆弱你好{name}依赖运行时字符串替换当文案中出现 JSON 特殊字符或嵌套变量时容易出错。翻译覆盖率不可追踪多个语言文件之间的 key 不一致是常态缺少自动化的覆盖率检查工具。二、阶段二引入命名空间分层当文案量增长到 500 条时需要引入命名空间分层。将翻译文案按功能模块拆分locales/ ├── zh-CN/ │ ├── common.json # 通用文案按钮、提示 │ ├── dashboard.json # 仪表盘 │ ├── settings.json # 设置页 │ └── errors.json # 错误信息 ├── en-US/ │ ├── common.json │ ├── dashboard.json │ └── ...同时引入翻译管理平台如 Lokalise、Crowdin进行协作解决翻译人员获取上下文的问题。平台可以展示 key 在 UI 中的截图、提供翻译记忆库TM以减少重复翻译。在此基础上增加覆盖率检查脚本// i18n-coverage-check.ts — 翻译覆盖率检查 import * as fs from fs; import * as path from path; interface CoverageReport { total: number; translated: number; missing: string[]; // 缺失的 key untranslated: string[]; // 值为空的 key coverage: number; } /** * 检查指定语言的翻译覆盖率 * 以中文zh-CN为基准比较其他语言的 key 覆盖情况 */ function checkCoverage( basePath: string, // 基准语言目录zh-CN targetPath: string, // 目标语言目录如 en-US locale: string ): CoverageReport { const baseFiles fs.readdirSync(basePath).filter((f) f.endsWith(.json)); const report: CoverageReport { total: 0, translated: 0, missing: [], untranslated: [], coverage: 0, }; for (const file of baseFiles) { const baseFilePath path.join(basePath, file); const targetFilePath path.join(targetPath, file); // 读取基准文件 let baseContent: Recordstring, string; try { baseContent JSON.parse(fs.readFileSync(baseFilePath, utf-8)); } catch (error) { console.error([Coverage] 基准文件解析失败: ${baseFilePath}, error); continue; } const baseKeys Object.keys(baseContent); report.total baseKeys.length; // 检查目标文件是否存在 if (!fs.existsSync(targetFilePath)) { // 整个文件缺失所有 key 都算未翻译 baseKeys.forEach((key) { report.missing.push(${locale}:${file}:${key}); }); continue; } // 逐 key 对比 let targetContent: Recordstring, string; try { targetContent JSON.parse(fs.readFileSync(targetFilePath, utf-8)); } catch (error) { console.error([Coverage] 目标文件解析失败: ${targetFilePath}, error); // 解析失败时将所有 key 记为缺失 baseKeys.forEach((key) { report.missing.push(${locale}:${file}:${key}); }); continue; } for (const key of baseKeys) { const targetValue targetContent[key]; if (targetValue undefined) { // key 不存在于目标文件 report.missing.push(${locale}:${file}:${key}); } else if (targetValue.trim() ) { // key 存在但值为空 report.untranslated.push(${locale}:${file}:${key}); } else { report.translated; } } } report.coverage report.total 0 ? Math.round((report.translated / report.total) * 10000) / 100 : 0; return report; } // 使用示例检查所有非中文语言的覆盖率 const LOCALES_DIR ./locales; const BASE_LOCALE zh-CN; const locales fs.readdirSync(LOCALES_DIR).filter((d) d ! BASE_LOCALE); locales.forEach((locale) { const report checkCoverage( path.join(LOCALES_DIR, BASE_LOCALE), path.join(LOCALES_DIR, locale), locale ); console.log( [${locale}] 覆盖率: ${report.coverage}% (${report.translated}/${report.total}) ); if (report.missing.length 0) { console.warn( 缺失 key: ${report.missing.length} 个); } });三、阶段三ICU MessageFormat 与复数/性别处理当支持的语言达到 8 种以上时语法层面的问题无法再回避。ICU MessageFormat 是 Unicode 联盟定义的国际化消息格式标准原生支持复数、选择、日期/数字格式化等复杂场景。架构演进的核心变化ICU MessageFormat 的典型消息示例// 复数处理 file_count {count, plural, 0 {没有文件} 1 {1 个文件} other {# 个文件} } // 性别选择 welcome {gender, select, male {先生} female {女士} other {用户} }欢迎回来 // 嵌套格式复数 日期 report_summary 截至 {date, date, long}共找到 {count, plural, 0 {无匹配结果} other {# 条匹配结果} }前端集成实现// ICUMessageFormatter.ts — ICU MessageFormat 运行时解析器 // 基于 intl-messageformat 库封装 import IntlMessageFormat from intl-messageformat; interface LocaleData { [key: string]: string; } export class ICUMessageFormatter { private cache new Mapstring, IntlMessageFormat(); private currentLocale: string; constructor(locale: string zh-CN) { this.currentLocale locale; } /** 切换当前语言 */ setLocale(locale: string): void { if (locale ! this.currentLocale) { this.currentLocale locale; // 清除编译缓存因为不同 locale 的复数规则不同 this.cache.clear(); } } /** * 格式化 ICU 消息 * param message - ICU 格式的原始消息 * param values - 插值变量 * param locale - 可选覆盖当前语言 */ format( message: string, values?: Recordstring, string | number | Date, locale?: string ): string { const targetLocale locale || this.currentLocale; const cacheKey ${targetLocale}:${message}; try { // 检查编译缓存 let msgFormatter this.cache.get(cacheKey); if (!msgFormatter) { msgFormatter new IntlMessageFormat(message, targetLocale); this.cache.set(cacheKey, msgFormatter); } const result msgFormatter.format(values); // 处理格式化错误返回的字符串 if (typeof result string) { return result; } return String(result); } catch (error) { // 降级解析失败时回退到简单字符串替换 console.warn( [ICU Formatter] 消息解析失败 (${targetLocale}): ${message.slice(0, 50)}..., error ); return this.fallbackFormat(message, values); } } /** 降级格式化简单占位符替换 */ private fallbackFormat( message: string, values?: Recordstring, string | number | Date ): string { if (!values) return message; return message.replace(/\{(\w)\}/g, (_, key: string) { return values[key] ! undefined ? String(values[key]) : {${key}}; }); } /** 批量格式化适用于页面级文案注入 */ formatBatch( messages: LocaleData, values?: Recordstring, string | number | Date ): LocaleData { const result: LocaleData {}; for (const [key, message] of Object.entries(messages)) { try { result[key] this.format(message, values); } catch { result[key] message; // 保留原文案作为降级 } } return result; } }四、构建时优化编译期消息提取与验证运行时解析 ICU 消息有一定的性能开销。对于文案量巨大的应用1000 条可以在构建阶段提前编译消息避免运行时 AST 解析。构建时的处理流水线关键步骤提取编译将 ICU 消息预编译为格式化函数减少运行时开销。CLDR 数据裁剪ICU 依赖 CLDRUnicode Common Locale Data Repository数据。完整 CLDR 数据约 6MB通过 tree-shaking 只保留当前支持语言的规则可压缩到 100KB 以下。语法校验在 CI 中运行 ICU 消息语法检查阻断格式错误的提交。五、总结前端国际化方案的演进遵循简单起步、按需升级的原则。JSON Key-Value 在文案量小、语言数少的起步阶段完全可行当文案量突破数百条时需要引入命名空间分层和翻译管理平台当面对复数、性别等语法层面的需求时ICU MessageFormat 是经过工业验证的标准方案。在 15 种语言的实践中从 JSON 迁移到 ICU 后翻译相关的线上缺陷从月均 12 个降至 2 个以下主要减少的类别是复数显示错误和日期格式紊乱。关键收益不在于工具本身而在于 ICU 将语言规则固化为标准化的消息格式消除了手写字符串拼接带来的不确定性。

相关新闻