
很多前端项目最开始做国际化时,思路都比较简单:
把页面上的中文提取出来
翻译成英文
根据语言切换展示不同文案
但真正上线过多语言项目后就会发现,前端国际化远不只是"替换字符串"。
它还会涉及:
语言包组织
动态文案拼接
日期时间格式
数字和货币格式
复数规则
表单校验提示
后端错误码映射
RTL 布局
多语言协作流程
如果前期设计不合理,后面新增语言时会非常痛苦。
本文主要从工程实践角度,总结一个 Web 项目中 i18n 的常见处理方式。
一、国际化不是翻译,而是工程问题
很多团队第一次做 i18n 时,容易把它当成内容工作:
中文文案 → 英文文案
但在真实项目中,国际化更像是工程问题。
比如下面这个按钮:
<button>提交</button>
替换成语言包之后,看起来很简单:
$t('common.submit')
但问题很快就来了:
key 怎么命名?
语言包怎么拆分?
动态变量怎么处理?
后端错误信息怎么翻译?
多人协作时怎么避免冲突?
缺失翻译时怎么降级?
所以 i18n 的核心不是"翻译文本",而是建立一套可维护的多语言资源管理机制。
二、语言包应该怎么组织?
最常见的语言包结构是按语言拆分:
locales
├── zh-CN.json
├── en-US.json
├── ja-JP.json
└── ko-KR.json
这种方式适合小项目。
但项目变大后,一个 JSON 文件可能会变得非常长,不方便维护。
更推荐按业务模块拆分:
locales
├── zh-CN
│ ├── common.json
│ ├── login.json
│ ├── dashboard.json
│ └── order.json
├── en-US
│ ├── common.json
│ ├── login.json
│ ├── dashboard.json
│ └── order.json
这样做的好处是:
模块边界清晰
多人协作冲突更少
按需加载更方便
定位缺失 key 更快
例如 login.json 可以这样写:
{
"title": "登录",
"emailPlaceholder": "请输入邮箱",
"passwordPlaceholder": "请输入密码",
"submit": "登录",
"forgotPassword": "忘记密码?"
}
对应英文:
{
"title": "Sign in",
"emailPlaceholder": "Enter your email",
"passwordPlaceholder": "Enter your password",
"submit": "Sign in",
"forgotPassword": "Forgot password?"
}
三、key 命名不要只按中文含义来写
很多项目会出现这种 key:
{
"queding": "确定",
"quxiao": "取消",
"tijiao": "提交"
}
这种命名短期能用,但维护性很差。
更推荐使用语义化路径:
{
"common": {
"confirm": "确定",
"cancel": "取消",
"submit": "提交"
}
}
业务页面里的文案可以按模块划分:
{
"order": {
"detail": {
"title": "订单详情",
"payNow": "立即支付",
"cancelOrder": "取消订单"
}
}
}
这种方式有几个优点:
语义清晰
便于搜索
不依赖具体中文文案
方便后续修改
不要用中文拼音做 key,也不要用整句中文做 key。
例如:
$t('订单详情')
看起来方便,但后期文案一改,key 也要跟着变,非常不稳定。
四、动态文案不要用字符串拼接
国际化里最容易踩坑的地方之一,就是动态文案拼接。
比如中文里写:
`你有 ${count} 条未读消息`
很多人会直接翻译成英文:
`You have ${count} unread messages`
看起来没问题。
但如果是更复杂的语言,语序可能完全不同。
所以不要这样写:
$t('message.youHave') + count + $t('message.unread')
应该使用插值:
{
"unreadMessage": "你有 {count} 条未读消息"
}
英文:
{
"unreadMessage": "You have {count} unread messages"
}
代码中:
$t('message.unreadMessage', { count: 5 })
这样每种语言都可以自己决定变量位置。
五、复数规则要单独处理
英文里有明显的单复数:
1 message
2 messages
但中文没有这种变化。
所以如果只按中文思维写语言包,到了英文就容易出问题。
以 Vue I18n 为例,可以使用复数形式:
{
"apple": "no apples | one apple | {count} apples"
}
使用时:
$t('apple', count)
React 项目中如果使用 React Intl,也可以写成:
intl.formatMessage(
{
id: 'message.count',
defaultMessage: '{count, plural, one {# message} other {# messages}}'
},
{ count }
)
复数规则不要自己用 if else 手写。
不同语言的复数规则差异很大,最好交给成熟 i18n 库处理。
六、日期、时间和数字不要手动格式化
很多项目里会写这种代码:
`${year}-${month}-${day}`
在中文环境里可能没问题,但国际化场景下会遇到很多差异:
日期顺序不同
12 小时制和 24 小时制不同
小数点符号不同
千分位符号不同
货币符号位置不同
更推荐使用 Intl API。
日期格式化:
const date = new Date();
const result = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
console.log(result);
数字格式化:
const number = 1234567.89;
const result = new Intl.NumberFormat('en-US').format(number);
console.log(result);
货币格式化:
const price = 1999.99;
const result = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(price);
console.log(result);
不要把日期、金额格式写死在业务代码里。
这些都应该跟随 locale 变化。
七、后端错误码不要直接返回自然语言
很多系统一开始后端会直接返回错误信息:
{
"code": 10001,
"message": "用户不存在"
}
这样在单语言项目里没问题。
但多语言项目里,前端就很难处理。
更合理的方式是后端返回稳定错误码:
{
"code": "USER_NOT_FOUND"
}
前端根据错误码做映射:
{
"error": {
"USER_NOT_FOUND": "用户不存在",
"PASSWORD_INCORRECT": "密码错误",
"TOKEN_EXPIRED": "登录已过期,请重新登录"
}
}
英文语言包:
{
"error": {
"USER_NOT_FOUND": "User not found",
"PASSWORD_INCORRECT": "Incorrect password",
"TOKEN_EXPIRED": "Your session has expired. Please sign in again."
}
}
这样做的好处是:
后端不需要关心具体语言
前端可以统一展示
错误码稳定可追踪
多语言扩展更简单
当然,如果是服务端渲染项目,也可以在服务端根据请求语言返回对应文案。
关键是不要把自然语言错误信息和业务逻辑强绑定。
八、不要忽略布局问题
很多人以为 i18n 只影响文字。
实际上,语言会直接影响 UI 布局。
比如英文文案通常比中文长:
提交
Submit
删除
Delete
账户安全设置
Account security settings
德语、俄语、法语等语言可能更长。
如果按钮宽度写死,很容易出现:
文字溢出
按钮换行
布局错位
弹窗被撑开
表格列宽异常
所以多语言项目里要尽量避免写死宽度。
例如:
.button {
min-width: 96px;
padding: 0 16px;
}
而不是:
.button {
width: 96px;
}
对于标题、菜单、表格列,也要考虑长文本场景。
九、RTL 语言需要提前考虑
如果项目未来要支持阿拉伯语、希伯来语等语言,就要考虑 RTL。
RTL 是 Right To Left,也就是从右到左阅读。
这会影响:
页面布局方向
菜单展开方向
图标位置
输入框对齐
进度条方向
轮播方向
CSS 中可以使用逻辑属性减少适配成本。
例如:
.card {
margin-inline-start: 16px;
padding-inline-end: 20px;
}
而不是:
.card {
margin-left: 16px;
padding-right: 20px;
}
逻辑属性会根据文本方向自动适配。
如果一开始大量写死 left 和 right,后期支持 RTL 会非常痛苦。
十、语言切换需要考虑持久化
用户选择语言后,一般需要持久化。
常见方案:
localStorage
Cookie
URL 参数
用户账号偏好
浏览器默认语言
推荐优先级可以是:
URL 参数 > 用户账号设置 > localStorage > 浏览器语言 > 默认语言
例如:
function getLocale() {
const urlLocale = new URLSearchParams(window.location.search).get('lang');
if (urlLocale) return urlLocale;
const savedLocale = localStorage.getItem('locale');
if (savedLocale) return savedLocale;
return navigator.language || 'zh-CN';
}
对于后台管理系统,通常建议绑定到用户账号偏好。
这样用户换设备登录时,语言设置仍然一致。
十一、缺失翻译要有降级策略
线上项目中,语言包缺失是很常见的问题。
比如新增了一个 key:
$t('order.refundReason')
但英文语言包忘记补了。
如果没有降级策略,页面可能直接显示 key:
order.refundReason
这对用户体验很差。
比较常见的处理方式:
当前语言缺失 → 回退默认语言
默认语言也缺失 → 显示 key 并上报
开发环境可以直接 warning:
console.warn(`[i18n missing]: ${key}`);
生产环境可以上报到日志系统,方便定期扫描缺失翻译。
十二、多语言项目需要协作流程
i18n 最大的难点,往往不是技术实现,而是协作流程。
一个正常多语言需求可能涉及:
产品经理写中文文案
设计师调整 UI
前端提取 key
翻译人员维护语言包
测试验证不同语言页面
后端维护错误码
如果流程不清晰,很容易出现:
语言包漏翻
文案和设计不一致
上线前才发现按钮溢出
错误码没有对应翻译
比较推荐的流程是:
1. 需求阶段确认是否涉及新增文案
2. 前端开发时新增 i18n key
3. 默认语言先补齐
4. 其他语言进入翻译流程
5. 测试阶段开启多语言检查
6. 上线前扫描 missing key
如果团队是跨国协作,需求评审和技术 Demo 也会涉及多语言沟通。这个场景下,可以使用**同言翻译(Transync AI)**这类实时翻译工具辅助会议沟通,比如用实时字幕理解讨论内容,再结合会议总结沉淀需求变更点。
这类工具不应该替代正式语言包翻译,但可以降低跨语言沟通成本,尤其适合远程会议、海外客户需求确认、国际团队技术评审等场景。
十三、一个简单的 i18n 检查清单
最后整理一个多语言项目检查清单。
开发阶段可以逐项确认:
1. 页面文案是否全部使用 i18n key
2. key 命名是否语义化
3. 是否避免字符串拼接
4. 是否处理动态变量
5. 是否处理复数规则
6. 日期、数字、货币是否使用 Intl
7. 后端错误是否使用错误码映射
8. 长文本是否会撑爆布局
9. 是否需要支持 RTL
10. 语言选择是否持久化
11. 缺失翻译是否有降级
12. 上线前是否扫描 missing key
这套清单不复杂,但能避免很多低级问题。
总结
前端国际化不是简单的"翻译页面"。
它更像是一套工程能力,涉及:
资源管理
组件设计
格式化规则
错误码规范
布局适配
协作流程
质量检查
项目越早建立规范,后期支持新语言的成本就越低。
如果只是在上线前临时把中文替换成英文,很容易留下大量技术债。
比较合理的方式是:
从第一版语言包开始,就把 i18n 当成基础工程能力来设计
这样当业务真正走向多地区、多语言、多团队协作时,前端系统才不会被文案和语言问题拖垮。
