MF2 随 CLDR 47 转正:数据模型、MF1 语法对照与迁移验证
MessageFormat 2.0(现称 Unicode MessageFormat,MF2)在 CLDR 47(2025 年 3 月发布)从 Final Candidate 升级为 Stable,规范作为 TR35 的一部分发布,稳定性政策也写进了规范性内容。CLDR 47 已并入 ICU 77。
规范状态与实现状态不是一回事。ICU4J 的 com.ibm.icu.message2、ICU4C 的 icu::message2::MessageFormatter 自 ICU 76 起仍是 tech preview,官方文档明确标注 "Technical Preview API",函数集合会随规范调整;JavaScript 侧的 messageformat 4.0 实现了 MF2 语法,并提供 ECMA-402 提案 runtime API 的 polyfill。
- 规范仓库:https://github.com/unicode-org/message-format-wg
- 站点与交互式示例:https://messageformat.unicode.org/
- ICU 文档 MF2 页:https://unicode-org.github.io/icu/userguide/format_parse/messages/mf2.html
- npm:https://www.npmjs.com/package/messageformat
消息分两类
MF2 分 simple message 与 complex message。
simple message 只有一个 pattern,首字符有约束,首尾空白有意义并计入文本。
complex message 以带点前缀的关键字(声明)或带双花括号的 quoted pattern 开头,结构是「若干声明 + complex body」,首尾空白无意义。complex body 要么是一个 quoted pattern {{ ... }},要么是 matcher。
声明有三种:
.input {$count :number}把外部变量注解成带函数的值.local $x = {|表达式|}定义局部变量.match $count开始 matcher,后面跟至少一个 selector,再跟若干 variant
matcher 的四条语义约束
matcher 是否合法由数据模型判定,不满足即抛 Data Model Error:
- Variant Key Mismatch:每个 variant 的 key 数量必须等于 selector 数量
- Missing Fallback Variant:至少有一个 variant 的所有 key 都是
* - Missing Selector Annotation:每个 selector 必须是变量,且直接或间接引用一个带函数的声明
- Duplicate Variant:variant 的 key 列表必须唯一(字面量按字符串值比较,不按写法)
错误分两层,这点在迁移时经常被混在一起:
| 层级 | 触发条件 | 性质 |
|---|---|---|
| Syntax Error | 不满足文法 | 解析期 |
| Data Model Error | 满足文法但不满足上述语义 | 语法合法、语义不合法 |
第二类容易漏,因为它不会在解析阶段暴露。
例子
.input {$count :number}
.match $count
0 {{ No items. }}
one {{ 1 item. }}
* {{ {$count} items. }}
标记:This is {#bold}important{/bold}.,MF2 不赋予标记语义,如何渲染由开发者决定。
字面量:My name is {|John Doe|}.,用成对竖线包裹;也常出现在选项值里:year=|2-digit|。
转义:\{、\} 表示字面的花括号。MF2 用反斜杠转义,不再是 MF1 的单引号。
函数:
{$ts :time}
{$val :number skeleton=(.####)}
{(123456789.531) :number}
第三行是编译期常量。函数以 : 前缀,后接 identifier 与若干 option;option 形式是 identifier = (literal | variable),同名 option 重复会报 Duplicate Option Name,option 顺序无关。
稳定级别
CLDR 47 明确:默认函数中只有 :string、:number、:integer 三个是 stable,其余(含 datetime 等)为 Draft。实现者与使用方都要按 Draft 对待,升级时给 Draft 函数留回归用例。
与 MF1 的四处硬差异
| 维度 | MF1 | MF2 |
|---|---|---|
| 占位符 | {0} / {name} | 变量必须带 $:{$name},不再有位置参数 |
| 复数 | 内联语法 {n, plural, one{...} other{...}} | .input 声明 + .match matcher + {{}} variant,靠 variant key 匹配;没有 # 特殊符号,数量自己写 {$count} |
| 转义与字面量 | 单引号包裹字面文本,'' 表示单引号 | 反斜杠转义 \{、\},字面量用 |...| 包裹 |
| 兜底 | other 是类别 | * 是兜底 key;缺失直接是 Data Model Error,不是运行时降级 |
另外 MF1 的 select 在 MF2 里统一并入 .match,按字面量 key 匹配(函数用 :string)。
bidi 与测试
CLDR 47 修订了 MF2 的 Default Bidi Strategy,增加对 bidi 隔离符与标记的支持。直接后果是格式化输出里可能插入隔离控制字符,字符串全等断言、长度断言、快照测试都可能失败。
处理方式是先明确比对口径:按码点比对,还是只比对可见文本。具体插入哪些字符随实现与策略配置不同,需在自己的 ICU 版本上实测,不要照搬别处的期望值。
迁移检查清单
- 运行时版本:Java/C++ 侧走 message2 API 仍是 tech preview,API 与函数集合会变。不要和 MF1 的
MessageFormat混在同一个资源文件里,两条链路并存,不是互相替换的关系。 - 前端:
messageformat4.0 提供 MF2 语法与转换工具,可从 MF1 语法转换,但转换后要人工检查 matcher 的 fallback 与 literal。 - 选择器:选择器函数必须在声明里注解,否则触发 Missing Selector Annotation。
- 函数依赖:只依赖 stable 的
:string/:number/:integer;用到 Draft 函数要在升级时留回归用例。 - 错误收集:Syntax Error 与 Data Model Error 分开收集,后者是语法合法但语义不合法,最容易漏。