我们的系统服务国内与东南亚两地用户,i18n 从” nice to have”变成刚需是在第一个海外客户要求泰文界面那天。好在 JHipster 是我们用过 i18n 完成度最高的脚手架——生成时就内置几十种语言(含简繁中文),前后端一体。这篇讲清它的 i18n 机制全貌、团队的 key 约定、本地化格式处理,以及和升级共存的注意事项。
生成时的语言选项
首次生成时问答会询问基础语言与附加语言:
jhipster
# ? Which *native* language would you like your application to support? Chinese (Simplified) - 中文(简体)
# ? Would you like to enable internationalization support? Yes
# ? Please choose additional languages to support? English, Thai
也可以事后用语言子生成器追加:
jhipster languages th
选择会记入 .yo-rc.json(languages: ["zh-cn", "en", "th"]),这是事实源的一部分——升级重生成时语言集合不丢失。
后端:MessageSource 与 LocaleResolver
生成的后端已经接好 Spring 的标准 i18n 通道。资源文件按 locale 分置:
src/main/resources/i18n/
├── messages.properties # 默认语言(我们设为 zh-cn 内容)
├── messages_en.properties
└── messages_th.properties
LocaleResolver:JHipster 默认采用 AcceptHeaderLocaleResolver——从请求头 Accept-Language 解析,匹配不到回落默认语言。校验错误消息自动本地化:@NotNull 的报错文案取 messages_*.properties 里的对应 key,REST 层的 ExceptionTranslator 统一翻译成当前 locale 的响应:
@RestController
public class OrderResource {
@PostMapping("/api/orders")
public ResponseEntity<OrderDTO> create(@Valid @RequestBody OrderDTO dto) {
// 校验失败时,客户端带 Accept-Language: en 就收到英文错误
}
}
业务消息自己注入 MessageSource:
@Service
public class OrderService {
private final MessageSource messageSource;
public void cancel(Long id, Locale locale) {
// ...
throw new BusinessException(
messageSource.getMessage("error.order.alreadyShipped", new Object[]{id}, locale));
}
}
# messages_zh_cn.properties(JHipster 用下划线命名资源文件)
error.order.alreadyShipped=订单 {0} 已发货,无法取消
# messages_en.properties
error.order.alreadyShipped=Order {0} has been shipped and cannot be cancelled
Locale 的取法我们封装了一个参数解析器:优先 URL 参数(测试与邮件深链可控)、其次 Accept-Language、最后默认值,方便排障时强制指定语言。
前端:i18n JSON 结构与 key 约定
前端翻译文件在 src/main/webapp/i18n/ 下,按语言一目录:
src/main/webapp/i18n/
├── zh-cn/
│ ├── home.json login.json global.json
│ ├── order.json # 我们的业务翻译(按域分文件)
│ └── inventory.json
├── en/
│ └── (镜像同构文件)
└── th/
key 的结构 文件名.key.path:
// zh-cn/order.json
{
"list": "订单列表",
"status": {
"PAID": "已支付",
"SHIPPED": "已发货"
},
"cancelConfirm": "确认取消订单 {{ id }} 吗?"
}
模板与 TS 里的用法:
<span jhiTranslate="order.status.PAID">已支付</span>
// 组件中编程式翻译
constructor(private translateService: TranslateService) {}
alert(this.translateService.instant('order.cancelConfirm', { id: 42 }));
团队 key 约定(血泪换来的):
- 业务域前缀:
order.、inventory.,与前端 feature 目录对齐(第 23 篇),禁止全部挤进 global.json; - 禁止拼接 key:
'order.status.' + status这种动态 key 会让翻译扫描工具和 rename 重构失明,枚举映射用显式表; - 占位符用命名参数:
{{ id }}而不是{0},翻译调整语序时不脆断; - 文案进代码 review:中英对照在 PR 里一眼看完,“先上线后补翻译”的空 key 不许合入。
语言切换器是生成的:navbar 里的下拉组件写好后自动列出启用语言,切换即持久化到 localStorage 并广播 TranslateService 事件。后端语言偏好同步:切换时把选择写入用户 profile,邮件模板渲染时用同一 locale。
日期、数字与货币本地化
界面格式化是 i18n 最容易翻车的部分,规则只有一条:全部交给 Intl,禁止手写格式化。
// 日期:date-fns 按需引入 locale(也减小 bundle,见第 26 篇)
import { format } from 'date-fns';
import { zhCN, enUS } from 'date-fns/locale';
format(order.createdAt, 'PPPpp', { locale: zhCN }); // 2026年8月24日 下午12:03:22
format(order.createdAt, 'PPPpp', { locale: enUS }); // August 24th, 2026 at 12:03:22 PM
// 数字与货币:Intl 内建,零依赖
new Intl.NumberFormat('th-TH').format(1234567.89); // 1,234,567.89
new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(199); // ¥199.00
new Intl.DateTimeFormat('en', { dateStyle: 'medium' }).format(new Date());
时区是独立问题:后端一律 UTC 存储、API 传输 ISO-8601 带偏移,前端按浏览器时区渲染。我们踩过的坑是服务器 JVM 默认时区影响 Date.toString 进日志——统一 -Duser.timezone=UTC 了事(第 28 篇 checklist 有这一项)。
新增语言的完整步骤
以泰文上线为例,五步:
jhipster languages th——生成前端i18n/th/骨架与后端messages_th.properties(生成的是英文底稿);- 翻译采购:生成文件交给翻译供应商,业务 JSON(
order.json等)同步补齐; - 字体与排版检查:泰文有上下附标字符,行高与截断样式要专门过一遍;
- 校验消息覆盖测试:跑一个”全 key 对比”脚本,确保 th 与 zh-cn 的 key 集合一致,缺漏在 CI 里 fail;
- 切换器发布:
.yo-rc.json确认th已在 languages 列表,升级时它会被保留。
RTL(阿拉伯语、希伯来语)一句话:加 ng-rtl 支持或 CSS 逻辑属性(margin-inline-start),layout 用逻辑方向写就有 80% 兼容,但生成组件的个别硬编码 left/right 需要专项清理——好在我们的业务范围暂不涉及,真要上阿语前会先做一次专项评估。
i18n 与升级的共存
i18n 是生成文件占比很高的区域,升级时注意:
- 翻译文件本身是你的资产:
i18n/zh-cn/*.json中业务域文件(order.json 等)不会被整体重生成(在自定义目录约定下),但 JHipster 自身的 home.json、global.json 可能随版本新增 key。升级流程(第 22 篇)跑完后专门 diff 这两个文件,把上游新增 key 补译; - key 被 JHipster 重命名:罕见但发生过(组件重构引起),表现为界面出现裸 key。上线前 e2e 里加一条”遍历主页面断言无裸 key”的检查(第 17 篇的 Cypress 顺手写);
- 缺 key 回落:TranslateService 默认回落到基础语言,配置里显式声明回落链
['zh-cn'],避免出现半泰半中的”混合页面”事故——我们出现过,用户截图比报障快。
小结
- JHipster 前后端 i18n 开箱即用:后端 MessageSource + AcceptHeaderLocaleResolver,前端 JSON + 语言切换器,语言清单是
.yo-rc.json事实源的一部分。 - key 约定四条:域前缀、禁拼接、命名占位符、文案进 review;格式化全部交给 date-fns/Intl。
- 新增语言五步走,key 覆盖对比进 CI 是防漏译的保险丝。
- 升级后专门 diff 生成翻译文件补新 key,e2e 断言无裸 key。
系列导航
- 上一篇:性能调优实战
- 下一篇:生产上线 Checklist
- 相关阅读:locale 按需打包影响 bundle 体积见第 26 篇性能调优;升级 diff 流程见第 22 篇;i18n 相关的坑收录在踩坑全集