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 | 备注 |
|---|---|---|---|---|
| ICU4J | com.ibm.icu.message2 | ICU 76 起 tech preview;ICU 78(2025-10)core API 升为 draft,Data Model API 仍 technology preview | CLDR 48 | ICU4J 78 起最低 Java 11 |
| ICU4C | icu::message2::MessageFormatter | ICU 76 起 tech preview;ICU 78 仍 technology preview | CLDR 47 级别 | ICU 77 起就“还没完全跟上 CLDR 46.1” |
| ICU4X | icu_experimental::messageformat | epic #3028(2023-01 开);PR #7884 落地初始实现 | 以 PR 为准 | 已 vendor message-format-wg 的 JSON conformance suite |
| JS | 包 messageformat 4.x | 4.0.0(2025-11-25)对齐 LDML 48;4.1.0(2026-10)对齐 LDML 48.2/49 | LDML 48.2/49 | TC39 提案的 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.0 | 2025-11-25 | LDML 48 | 正式版;要求 Node.js 20+,支持 require(esm) |
| 4.1.0 | 2026-10 | LDML 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/resource0.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。
接入检查清单
- 确认手里是 MF2 还是 MF1:看包名,
messageformatv4 是 MF2,@messageformat/core/@messageformat/parser是 MF1。 - 锁精确版本(
--save-exact),并在项目里记录对齐的 LDML 版本。 - 检查用到的 function 是否已 finalized:只有 string/number/integer 是 final,currency/percent 在 JS 4.1 才转 Stable,datetime 等仍在
DraftFunctions里。 - 构建期校验:CI 里对消息文件跑
parseMessage+validate,不要留到运行期。 - 运行期
onError必须接。 - 跨语言一致性:后端若是 ICU4J / ICU4C / ICU4X,注意同版本内部也不同步,需按各自实现跑 message-format-wg 的 conformance 数据。
- i18next 生态:WG 记录中有 i18next i18nFormat 插件的 mf2 格式(0.1.1),但 WG 明确标注“未评估”,生产慎用。