4. 多语言资源管理与国际化基础
电子作业指导系统(e-SOP)通常要在同一套界面里同时服务中文、英文、越南语、泰语等多条产线。如果界面文字被硬编码在页面或组件里,每增加一种语言就要复制一份页面,维护成本会迅速失控。国际化的核心思路是把“界面结构”和“界面文字”彻底分离:结构由代码负责,文字由语言资源负责,运行时再按当前语言把两者拼装起来。
i18n 与 l10n:两个不同阶段的工作
i18n(internationalization,国际化)是“让系统具备支持多语言的能力”,属于工程改造阶段:抽取文案、定义翻译键、建立语言包加载机制、处理日期数字格式。l10n(localization,本地化)是“为某个具体地区产出可用内容”,属于内容交付阶段:把键翻译成目标语言、校对术语、适配当地计量单位与日期习惯。一句话概括:i18n 是一次性的架构能力,l10n 是每种语言重复一次的内容工作。二者关系可用下图表示。
翻译键设计:决定后期维护成本的关键
翻译键(translation key)是代码与译文之间的唯一契约。键一旦发布就被大量引用,改名意味着全量替换,因此键的命名必须在项目初期就定好规则。推荐采用“模块.对象.用途”的点分层级结构,全部小写,用点号分隔,不使用中文、空格和驼峰。
// 推荐:语义清晰、可分层、可批量检索
"sop.step.title" // 作业步骤标题
"sop.step.duration" // 步骤标准工时
"device.status.running" // 设备状态:运行中
"device.status.fault" // 设备状态:故障
"common.button.confirm" // 通用按钮:确认
"common.button.cancel" // 通用按钮:取消
// 不推荐:把中文或整句当键,改文案即改键
"作业步骤标题"
"请点击确认按钮继续"
键设计有三条硬规则。第一,键只描述“位置和用途”,不描述“内容”,例如用 device.status.fault 而不是 device.status.red,因为颜色可能随主题变化。第二,带变量的句子必须使用占位符,且占位符顺序由命名参数决定,不能依赖位置拼接:
// zh-CN.json
"alarm.overTemp": "设备 {device} 温度 {value}℃ 超过上限 {limit}℃"
// en-US.json
"alarm.overTemp": "Device {device} temperature {value}°C exceeds limit {limit}°C"
第三,复数、性别等语言差异不要用 if 判断写在代码里,而应交给 i18n 库的复数规则处理,例如英文 1 step 与 3 steps,中文则无复数变化。若把复数逻辑写死在组件中,新增语言时必然返工。
语言包组织:按模块拆分,按需合并
单文件语言包在小型系统中够用,但 e-SOP 往往包含工单、设备、质检、报表等十几个模块,一个 zh-CN.json 可能膨胀到数千行,多人协作时冲突频繁。更实用的做法是按模块拆分为多个命名空间文件,再由入口统一合并:
locales/
├── zh-CN/
│ ├── common.json // 通用按钮、提示
│ ├── sop.json // 作业指导相关
│ ├── device.json // 设备状态与参数
│ └── quality.json // 质检项与判定
├── en-US/
│ ├── common.json
│ ├── sop.json
│ ├── device.json
│ └── quality.json
└── index.js // 汇总导出
拆分后,键的命名空间与文件名保持一致,例如 sop.json 中的键统一以 sop. 开头。这样翻译人员只需拿到自己负责的模块文件,代码评审时也能快速定位改动范围。合并入口示例:
import commonZh from './zh-CN/common.json';
import sopZh from './zh-CN/sop.json';
import deviceZh from './zh-CN/device.json';
export const zhCN = { ...commonZh, ...sopZh, ...deviceZh };
动态加载策略:首屏轻量,切换按需
把所有语言包一次性打进主包,会让首屏体积随语言数量线性增长。合理的策略是:当前语言的核心模块随首屏加载,其余语言和非核心模块在语言切换或进入对应页面时再异步拉取。判断依据是“用户是否马上需要看到”。
const loaded = {};
async function loadNamespace(lang, ns) {
const key = `${lang}/${ns}`;
if (loaded[key]) return loaded[key];
const res = await fetch(`/locales/${lang}/${ns}.json`);
loaded[key] = await res.json();
return loaded[key];
}
// 进入设备页面时按需加载
await loadNamespace('vi-VN', 'device');
加载失败必须有兜底:先回退到默认语言(如 zh-CN)的同名键,再回退到键本身,避免界面出现空白。同时应缓存已加载的语言包,避免同一语言反复请求。切换语言时,若目标语言包尚未加载,先显示加载态,加载完成后再整体重渲染,而不是逐条替换文本,否则会出现中英混排的中间状态。
把以上四件事落实到位——明确 i18n 与 l10n 的分工、用点分层级设计翻译键、按模块拆分语言包、按需动态加载并做好回退——系统就具备了可持续扩展的多语言能力。后续新增一种语言,只需增加一个语言目录并补齐键值,无需改动任何界面代码。