CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / JHIPSTER / P-242 · JHipster 项目开发

JHipster 开发 27:国际化与本地化

我们的系统服务国内与东南亚两地用户,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.jsonlanguages: ["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 约定(血泪换来的):

  1. 业务域前缀order.inventory.,与前端 feature 目录对齐(第 23 篇),禁止全部挤进 global.json;
  2. 禁止拼接 key'order.status.' + status 这种动态 key 会让翻译扫描工具和 rename 重构失明,枚举映射用显式表;
  3. 占位符用命名参数{{ id }} 而不是 {0},翻译调整语序时不脆断;
  4. 文案进代码 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 有这一项)。

新增语言的完整步骤

以泰文上线为例,五步:

  1. jhipster languages th——生成前端 i18n/th/ 骨架与后端 messages_th.properties(生成的是英文底稿);
  2. 翻译采购:生成文件交给翻译供应商,业务 JSON(order.json 等)同步补齐;
  3. 字体与排版检查:泰文有上下附标字符,行高与截断样式要专门过一遍;
  4. 校验消息覆盖测试:跑一个”全 key 对比”脚本,确保 th 与 zh-cn 的 key 集合一致,缺漏在 CI 里 fail;
  5. 切换器发布:.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。

系列导航

← Angular 22+ 教程 27:内存泄漏、unsubscribe 与 takeUntilDestroyed 目录 算法 028:算法思想知识体系详解 →
← 返回文章列表