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.submit、message.success。别用 submitButton、successMessage 这种平铺的。层次化命名,后期维护时一眼就能找到。
重构第二步:处理动态内容
字符串里经常有变量。比如“你还有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 就行。
重构第四步:处理日期与数字格式
日期格式也是国际化的一部分。中文是“2025年3月15日”,英文是“March 15, 2025”。
我建议用 Intl.DateTimeFormat 和 Intl.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 文件后,发现有些地方还是硬编码。为什么?因为有些字符串是拼接出来的。比如:
const msg = '用户' + name + '登录成功';
这种拼接,你搜“登录成功”是搜不到的。因为字符串被拆开了。所以,重构时要特别注意拼接字符串的地方。
另一个坑是:不要用字符串作为键。比如:
// 错误做法
t('操作成功');
为什么?因为哪天中文文案改成“保存成功”,你所有引用 t('操作成功') 的地方都得改。键应该是稳定的,比如 message.success。文案变了,只改 JSON 文件就行。
t() 引用。这样永远不会出现“代码里写了,JSON 里没有”的情况。
总结
国际化重构,说白了就是一件事:把字符串从代码里赶出去。让代码只关心逻辑,让语言文件只关心文案。两者通过键来连接。
这样做的好处,不只是支持多语言。哪怕你只做中文版,维护起来也轻松得多。产品经理改文案?改 JSON 文件就行,不用动代码。测试同学发现错别字?改 JSON 文件就行,不用重新部署。
嗯,这就是我理解的“重构”的意义——不是炫技,是让代码更好维护。
公众号:蓝海资料掘金营,微信deep3321