81、重构与国际化:重构硬编码字符串,支持多语言

说实话,我见过太多项目,一开始都想着“先做出来再说”。结果呢?等产品要出海了,才发现代码里到处都是中文提示、英文报错,混在一起。改起来?那叫一个酸爽。

今天咱们聊的,就是怎么把那些硬编码的字符串抽出来,让应用能优雅地支持多语言。这活儿不复杂,但细节特别多。我踩过的坑,今天全抖出来。

为什么硬编码字符串是毒瘤?

你想想看,一个按钮叫“提交”,另一个地方叫“Submit”。哪天产品经理说,法语里“提交”要改成“Soumettre”。你怎么办?满世界搜“提交”和“Submit”?

更可怕的是,有些字符串藏在业务逻辑里。比如:

if (status === 'success') {
    showMessage('操作成功');
}

哪天UI要改成“保存成功”,你得改代码。哪天要支持英文,你得再改一遍。这不是重构,这是给自己挖坑。

核心问题就一个: 字符串和代码逻辑耦合在一起。解耦,就是重构的第一步。

重构第一步:抽离字符串

我个人习惯,先建一个 locales 目录。里面按语言分文件:

locales/
├── zh-CN.json
├── en-US.json
└── fr-FR.json

每个文件里,用键值对存字符串。比如 zh-CN.json

{
  "button.submit": "提交",
  "message.success": "操作成功",
  "error.network": "网络异常,请稍后重试"
}

en-US.json

{
  "button.submit": "Submit",
  "message.success": "Operation successful",
  "error.network": "Network error, please try again later"
}

然后代码里,再也不写死字符串了。改成这样:

// 之前
showMessage('操作成功');

// 之后
showMessage(t('message.success'));

这个 t() 函数,就是国际化库的核心。它会根据当前语言,从对应的 JSON 文件里取字符串。

关键点: 键的命名要有层次感。比如 button.submitmessage.success。别用 submitButtonsuccessMessage 这种平铺的。层次化命名,后期维护时一眼就能找到。

重构第二步:处理动态内容

字符串里经常有变量。比如“你还有3条未读消息”。直接写死?那3换成5怎么办?

我建议用模板占位符。比如:

{
  "message.unread": "你还有 {count} 条未读消息"
}

代码里:

t('message.unread', { count: 3 });

这样,不同语言的语序可以自由调整。比如英文:

{
  "message.unread": "You have {count} unread messages"
}

你看,占位符的位置完全由语言文件决定。代码不用动。

小技巧: 占位符命名用驼峰,别用数字。比如 {count}{0} 好读一百倍。我在项目中见过用 {0}{1} 的,三个月后没人知道哪个是哪个。

重构第三步:处理复数与性别

这个坑我踩过。中文里“1条消息”和“3条消息”都是“条”。但英文里是“1 message”和“3 messages”。

很多国际化库支持复数规则。比如:

{
  "message.unread": "你有 {count} 条未读消息",
  "message.unread_plural": "You have {count} unread messages"
}

但更优雅的方式是用 ICU MessageFormat。比如:

{
  "message.unread": "你有 {count, plural, one {# 条未读消息} other {# 条未读消息}}"
}

英文版:

{
  "message.unread": "You have {count, plural, one {# unread message} other {# unread messages}}"
}

这样,不同语言的复数规则完全由语言文件控制。代码里只需要传 count 就行。

注意: 不是所有语言都只有“单数/复数”两种。阿拉伯语有6种复数形式。俄语有4种。所以,别自己写复数逻辑,用成熟的库。

重构第四步:处理日期与数字格式

日期格式也是国际化的一部分。中文是“2025年3月15日”,英文是“March 15, 2025”。

我建议用 Intl.DateTimeFormatIntl.NumberFormat。它们是浏览器原生支持的,不用额外引入库。

const date = new Date('2025-03-15');
const formatter = new Intl.DateTimeFormat('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
});
console.log(formatter.format(date)); // 2025年3月15日

数字格式也一样:

const formatter = new Intl.NumberFormat('de-DE', {
    style: 'currency',
    currency: 'EUR'
});
console.log(formatter.format(1234.56)); // 1.234,56 €

你看,代码里完全不用关心格式细节。语言和地区决定了输出。

核心知识体系

下面这张图,是我对国际化重构核心逻辑的总结。你看一眼,心里就有谱了。

国际化重构核心逻辑 国际化重构 字符串抽离 动态内容处理 复数与性别 日期与数字格式 JSON 文件 t() 函数 占位符 模板 ICU 格式 复数规则 Intl API 地区格式 目标:代码与显示完全解耦

避坑指南

我曾经在一个项目里,把字符串抽离到 JSON 文件后,发现有些地方还是硬编码。为什么?因为有些字符串是拼接出来的。比如:

const msg = '用户' + name + '登录成功';

这种拼接,你搜“登录成功”是搜不到的。因为字符串被拆开了。所以,重构时要特别注意拼接字符串的地方。

另一个坑是:不要用字符串作为键。比如:

// 错误做法
t('操作成功');

为什么?因为哪天中文文案改成“保存成功”,你所有引用 t('操作成功') 的地方都得改。键应该是稳定的,比如 message.success。文案变了,只改 JSON 文件就行。

我的习惯: 每次新增一个字符串,先在 JSON 文件里定义好键,再在代码里用 t() 引用。这样永远不会出现“代码里写了,JSON 里没有”的情况。

总结

国际化重构,说白了就是一件事:把字符串从代码里赶出去。让代码只关心逻辑,让语言文件只关心文案。两者通过键来连接。

这样做的好处,不只是支持多语言。哪怕你只做中文版,维护起来也轻松得多。产品经理改文案?改 JSON 文件就行,不用动代码。测试同学发现错别字?改 JSON 文件就行,不用重新部署。

嗯,这就是我理解的“重构”的意义——不是炫技,是让代码更好维护。


公众号:蓝海资料掘金营,微信deep3321