案例 MF2 各实现版本不一致:ICU4J 升到 draft、ICU4C 停在 CLDR 47、JS 包 4.1 已到 48.2

2026-10-30 00:03:47

MF2 各实现版本不一致:接入前先对版本与 API 稳定性

MessageFormat 2.0(MF2)的规范状态已经是 Stable,但“规范 Stable”和“四个实现行为一致”是两件事。下面按实现逐个梳理版本、API 稳定性和已知缺口,最后给出 JS 侧的接入代码与排查顺序。

关键链接:

  • 规范(LDML MessageFormat):https://unicode.org/reports/tr35/tr35-messageFormat.html
  • 官网:https://messageformat.unicode.org/
  • JS 库:https://github.com/messageformat/messageformat
  • WG 仓库:https://github.com/unicode-org/message-format-wg
  • ICU4X epic:https://github.com/unicode-org/icu4x/issues/3028
  • TC39 提案:https://github.com/tc39/proposal-intl-messageformat/

规范状态:Stable 的范围有限

CLDR 47(2025-03-13)把 MessageFormat 2.0 从 Final Candidate 升为 Stable,ICU 77 集成了 CLDR 47。

定稿时做了一个关键取舍:为了避免实现方来不及评审 API,只把 :string、:number、:integer 三个 function 标为 finalized,其余 function 一律标成 Proposed/Draft,后续再逐个转正。所以“MF2 已经 Stable”只覆盖语法内核和这三个 function,:datetime、:currency、:percent 等仍处在不同成熟度上。这一条直接决定了后面所有实现的行为差异。

实现矩阵:同版本 ICU 内部也不同步

实现入口进入时间与状态对齐 CLDR备注
ICU4Jcom.ibm.icu.message2ICU 76 起 tech preview;ICU 78(2025-10)core API 升为 draft,Data Model API 仍 technology previewCLDR 48ICU4J 78 起最低 Java 11
ICU4Cicu::message2::MessageFormatterICU 76 起 tech preview;ICU 78 仍 technology previewCLDR 47 级别ICU 77 起就“还没完全跟上 CLDR 46.1”
ICU4Xicu_experimental::messageformatepic #3028(2023-01 开);PR #7884 落地初始实现以 PR 为准已 vendor message-format-wg 的 JSON conformance suite
JS包 messageformat 4.x4.0.0(2025-11-25)对齐 LDML 48;4.1.0(2026-10)对齐 LDML 48.2/49LDML 48.2/49TC39 提案的 polyfill

ICU4J 已经到 CLDR 48,ICU4C 还停在 CLDR 47 级别——同一个 ICU 版本里两个实现不同步,这是最容易忽略的一点。

ICU4X 的初始实现覆盖 parse、validate、format、内置 handlers、format_to_parts,并把 message-format-wg 的 JSON conformance suite vendor 进来跑一致性测试;API 稳定性和工具链仍在 epic 剩余范围内。这里有一个设计层面的冲突:ICU4X 希望编译期就知道 locale 以便裁剪数据,而规范里 u:locale / u:dir 可以是变量引用,locale 只能在运行期确定,和 ICU4X 的静态数据加载设计相抵。后续 4.1 的 JS 实现已按 LDML 48.2/49 删掉 u:locale 的残留提及。(ICU4X 的 PR 编号与合并时间需自行核对。)

结论很直白:同一段 MF2 消息在 ICU4J / ICU4C / ICU4X / JS 四个实现上的行为可能不同,尤其是 function 支持范围。生产接入必须锁定实现+版本,并跑一致性测试。

JavaScript 实现:接入时主要关注的实现

仓库在 messageformat/messageformat,包名 messageformat,Apache-2.0。

版本状态:

版本时间对齐规范变化
4.0.02025-11-25LDML 48正式版;要求 Node.js 20+,支持 require(esm)
4.1.02026-10LDML 48.2/49删掉 u:locale 残留;:currency 与 :percent 从 Draft 升为 Stable

包当前 API 对应 LDML 48.2(2026-03)版本规范。

v4 与 v3 的 API 完全不同,而且包名不同:

  • v3 是 @messageformat/core,那是 ICU MessageFormat(MF1)编译器。要 MF1 就用它。
  • @messageformat/parser(周下载约 190 万)也是 MF1 解析器,不能解析 MF2。这是最常见的踩坑点。

安装并锁精确版本:

npm install --save-exact messageformat

一段能直接跑的最小示例,覆盖构造函数、format、formatToParts 和自定义函数:

import { MessageFormat } from 'messageformat';
import { DraftFunctions, asString } from 'messageformat/functions';

// 1. 内置 Draft function:datetime 仍在 DraftFunctions 里
const mf = new MessageFormat('en', 'Today is {$today :datetime dateStyle=medium}', {
  functions: DraftFunctions
});
console.log(mf.format({ today: new Date('2022-02-02') }));
// 'Today is Feb 2, 2022'

// 2. 最佳努力 + onError
const out = new MessageFormat('en', 'Hello {$user}').format(
  { us3r: 'Bob' },
  (e) => console.log(e.type) // unresolved-var
);
console.log(out); // 'Hello {$user}'

// 3. formatToParts:不必再解析输出字符串
const parts = new MessageFormat('en', '{#b}Hello {$user}{/b}').formatToParts({ user: 'Bob' });
// markup open(source '#b', name 'b') / literal('Hello ') /
// string(source '$user', locale 'en', value 'Bob') / markup close

// 4. 自定义 function
const uppercase = {
  string: (input, context) =>
    DraftFunctions.string(asString(input).toLocaleUpperCase(context.locales))
};
const mm = new MessageFormat(['en'], '{messageformat :uppercase}', {
  functions: { uppercase }
});
console.log(mm.format({})); // 'MESSAGEFORMAT'

构造函数签名是 new MessageFormat(locales, message, options):locales 可以是字符串或数组,第三个参数可传 functions 注入自定义函数。formatter 对象可复用,不必每轮请求重建。

(上面 uppercase 的入参与返回形状请按你所锁版本的 TypeScript 定义核对,DraftFunctions.string 的签名在不同小版本间可能调整。)

工具函数分两组:

  • parseMessage / stringifyMessage:源码 ↔ 数据模型。
  • validate / visit:数据模型校验与遍历。
  • parseCST / messageFromCST / stringifyCST:保留全部字符的 CST。

转换工具链:

  • @messageformat/icu-messageformat-1:MF1 编译到 MF2 formatter
  • @messageformat/fluent:Fluent 编译到 MF2
  • @messageformat/xliff:XLIFF 2 ↔ MF2(ALPHA)
  • @messageformat/resource 0.1.0(2026-10 首发):处理 W3C i18n WG 孵化的 message resources 文本格式,0.x 会有破坏性变更

定位上,messageformat 是 TC39 提案 Intl.MessageFormat 的 polyfill。浏览器原生 API 尚不存在,别直接写 new Intl.MessageFormat(...),用前先探测。

错误分型:先分清哪类错

MF2 的错误要分三层看,排查顺序也按这个来。

语法错误(syntax error):parseMessage 阶段就抛 MessageSyntaxError。

数据模型错误(data model error):语法能过,但模型不合法。parseMessage 只做语法解析,数据模型错误要另外调 validate,抛 MessageDataModelError(继承自 MessageSyntaxError)。

这两类都不可恢复:实现不会给 fallback 输出,直接抛错或失败。

典型例子是 missing-fallback:matcher 必须穷尽,需要一个所有 key 都是 * 的变体。

.match $x
one {{x is singular}}

要补一行 * {{x is plural}} 才能通过 validate。

运行期错误(unresolved-var、function 缺失等)才是“最佳努力”:返回带原样的文本,通过 onError 回调拿到 type。也就是说,如果不接 onError,用户界面上可能直接出现 {$user}。

调试手段:官方 playground 的错误信息比多数实现详细;另有 MF2 language server,可给 VS Code 提供行内诊断和 quick fix。

接入检查清单

  1. 确认手里是 MF2 还是 MF1:看包名,messageformat v4 是 MF2,@messageformat/core / @messageformat/parser 是 MF1。
  2. 锁精确版本(--save-exact),并在项目里记录对齐的 LDML 版本。
  3. 检查用到的 function 是否已 finalized:只有 string/number/integer 是 final,currency/percent 在 JS 4.1 才转 Stable,datetime 等仍在 DraftFunctions 里。
  4. 构建期校验:CI 里对消息文件跑 parseMessage + validate,不要留到运行期。
  5. 运行期 onError 必须接。
  6. 跨语言一致性:后端若是 ICU4J / ICU4C / ICU4X,注意同版本内部也不同步,需按各自实现跑 message-format-wg 的 conformance 数据。
  7. i18next 生态:WG 记录中有 i18next i18nFormat 插件的 mf2 格式(0.1.1),但 WG 明确标注“未评估”,生产慎用。

推荐文章

程序员茄子在线接单