For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/stage/unstaged/proposal-intl-messageformat.md.
  • 简体中文
  • Intl.MessageFormat ?

    提案概览
    提案速览

    该提案为 MessageFormat 2.0(MF2)消息引入了原生解析器和格式化器,以应对因专有且受限的消息格式化规范而带来的 Web 本地化挑战。它通过 Intl.MessageFormat API 提供共享运行时,支持包含变量、复数规则的简单和复杂消息,可格式化为字符串或部件,并支持自定义消息函数。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    Intl.MessageFormat

    状态

    提案负责人:Eemeli Aro (Mozilla/OpenJS Foundation), Ujjwal Sharma (Igalia)

    前提案负责人:Daniel Minor (Mozilla)

    阶段:1

    演示文稿

    动机

    本提案旨在让 Web 的本地化变得更加容易,从而提升 Web 对使用各种语言的用户的开放性和可访问性。 目前,本地化依赖于一系列大多是专有的消息格式化规范, 这些规范在功能上有所限制,并且/或者给翻译人员带来工作上的挑战。 此外,本地化通常需要在应用的运行时或渲染过程中解析这些自定义格式。

    为了帮助解决这一问题,我们引入了 Intl.MessageFormat,作为 MessageFormat 2.0(又称“MF2”)消息的原生解析器和格式化器。 MF2 是一个目前正在 Unicode 联盟下开发的规范,并得到了广泛的行业支持。 这将允许使用 MF2 消息来本地化网站, 从而能够使用行业标准的工具和流程来实现 Web 的本地化。

    除了设计为对开发者和翻译人员都易于使用的语法之外, MF2 还定义了一个消息数据模型,可用于表示以任何现有语法定义的消息。 这使得 Intl.MessageFormat 可以在现有系统和流程中使用, 为所有用户提供一个共享的消息格式化运行时。

    使用场景

    主要使用场景是,在给定消息源文本、区域设置(locale)和其他选项, 以及可选的一组运行时值的情况下,检索和解析本地化文本(即“消息”)。

    综合来看,这使得从最简单到最复杂的任何消息, 都可以由开发者定义、翻译成任意数量的语言区域,并展示给用户。

    例如,考虑一条相对简单的消息,如

    您有 3 条新通知

    在实践中,这需要考虑任意数量的通知, 以及当前区域设置的复数规则。 使用 MF2 语法,可以将其定义为:

    .match {$count :number}
    0   {{You have no new notifications}}
    one {{You have {$count} new notification}}
    *   {{You have {$count} new notifications}}

    完整消息的某些部分会针对每种情况显式重复, 因为这使得翻译人员处理该消息时显著更容易。

    在代码中,使用下面提议的 API,可以这样使用:

    const source = ... // string source of the message as above
    const mf = new Intl.MessageFormat('en', source);
    const notifications = mf.format({ count: 1 });
    // 'You have 1 new notification'

    由于大多数消息不需要多个变体, 这些消息当然也得到所提议 API 的支持:

    // A plain message
    const mf1 = new Intl.MessageFormat('en', 'Hello!');
    mf1.format(); // 'Hello!'
    
    // A parametric message, formatted to parts
    const mf2 = new Intl.MessageFormat('en', 'Hello {$place}!');
    const greet = mf2.formatToParts({ place: 'world' });
    /* [
      { type: 'text', value: 'Hello ' },
      { type: 'string', source: '$place', value: 'world' },
      { type: 'text', value: '!' }
    ] */

    更复杂的使用场景和用法模式在 API 描述中说明。

    API 描述

    尽管 MF2 规范仍由工作组制定中, 这里展示的 API 代表了其当前的共识。 特别是,MessageData 的确切形态仍在工作组中讨论。

    本提案向 ECMAScript 引入一个新的原生对象(primordial),即 Intl.MessageFormat。 下面的其他 interface 描述旨在表示普通对象。

    MessageData

    MessageData 接口由 Unicode MessageFormat 工作组开发的 MF2 数据模型 定义。 它包含针对特定区域设置的单个消息的解析表示。

    type MessageData = PatternMessage | SelectMessage;
    
    interface PatternMessage {
      type: 'message';
      declarations: Declaration[];
      pattern: Pattern;
    }
    
    interface SelectMessage {
      type: 'select';
      declarations: Declaration[];
      selectors: Expression[];
      variants: Variant[];
    }

    消息数据模型的完整精确定义由其 JSON Schema 定义 给出。

    MessageFormat

    Intl.MessageFormat 构造函数为一条 source 消息、一个或多个区域设置标识符, 以及一个可选的 MessageFormatOptions 对象创建一个 MessageFormat 实例。 如果 source 参数使用字符串, 它将被解析为消息的 MF2 语法表示。

    如果 source 包含 MF2 语法或数据模型错误,调用构造函数可能会抛出错误。

    interface MessageFormat {
      new (
        locales: string | string[] | undefined,
        source: MessageData | string,
        options?: MessageFormatOptions
      ): MessageFormat;
    
      format(
        values?: Record<string, unknown>,
        onError?: (error: Error) => void
      ): string;
    
      formatToParts(
        values?: Record<string, unknown>,
        onError?: (error: Error) => void
      ): MessagePart[];
    
      resolvedOptions(): ResolvedMessageFormatOptions;
    }
    构造函数选项与 resolvedOptions()

    MessageFormatOptions 包含用于创建 MessageFormat 实例的配置选项。 ResolvedMessageFormatOptions 对象包含在构造 MessageFormat 实例期间解析后的选项。

    由于消息可能包含解析为与整个消息具有不同方向性(即从左到右与从右到左)的字符串的占位符, bidiIsolation 选项定义了一种策略, 通过这些策略,这些部分将在输出中相互隔离,以避免溢出效应。 默认的 'compatibility' 策略将在所有已知不匹配消息方向性的表达式边界处包含 Unicode 隔离码点。 'none' 策略不提供任何双向隔离。

    默认情况下,消息的方向性由与第一个区域设置对应的文字(script)决定, 但可以通过 dir 覆盖。 其 "auto" 值对应于方向性未知的消息, 此时方向由第一个强方向性字符决定。

    用户自定义的消息格式化和选择函数可以通过 functions 选项定义。 这些函数允许自定义函数处理任何数据类型。 此类函数可以在消息中被引用, 然后以参数和选项的解析值进行调用。

    interface MessageFormatOptions {
      bidiIsolation?: 'compatibility' | 'none';
      dir?: 'ltr' | 'rtl' | 'auto';
      functions?: { [key: string]: MessageFunction };
      localeMatcher?: 'best fit' | 'lookup';
    }
    
    interface ResolvedMessageFormatOptions {
      bidiIsolation: 'compatibility' | 'none';
      dir: 'ltr' | 'rtl' | 'auto';
      functions: { [key: string]: MessageFunction };
      localeMatcher: 'best fit' | 'lookup';
    }
    format(values?, onError?)

    与其他 Intl 格式化器一样,format() 返回一个字符串。 该方法具有以下可选参数:

    • values 为消息的变量引用提供变量值。
    • onError 定义了一个错误处理器,如果消息解析或格式化失败,将调用该处理器。 如果未定义 onError, 将针对每个错误发出警告,并对相应的消息部分使用回退表示

    为了确定 format() 方法返回的值 res, 消息首先被解析为一个 MessageValue 实例列表。 从空字符串 res 开始,对于每个 MessageValue mv

    1. msgDir 为消息的基本方向。
    2. bidiIsolationbidiIsolation 选项的解析值。
    3. dirmv.dir
    4. strval 为调用 mv.toString() 的结果。
    5. 如果调用失败或 strval 不是字符串:
      1. strval 设置为 {mv.source} 的拼接。
      2. dir 设置为 "auto"
    6. bidi 为调用 ApplyBidiIsolation(bidiIsolation, msgDir, dir){ start: string, end: string } 结果。
    7. bidi.startstrvalbidi.end 追加到 res 的末尾。

    ApplyBidiIsolation 抽象操作将当前的双向隔离策略以及消息和部分的方向作为参数。 它将据此确定 startend 为 Unicode 码点序列, 如有必要,这些码点会将各部分相互隔离。 对于默认的 "compatibility" 策略,结果匹配以下 TS 类型:

    type BidiIsolation =
      | { start: ''; end: '' }
      | {
          start: '\u2066' | '\u2067' | '\u2068'; // LRI | RLI | FSI
          end: '\u2069'; // PDI
        };
    formatToParts(values?, onError?)

    为了将消息格式化为非字符串目标, 提供了 formatToParts() 方法,返回一个 MessagePart 对象数组。 该方法具有以下可选参数:

    • values 为消息的变量引用提供变量值。
    • onError 定义了一个错误处理器,如果消息解析或格式化失败,将调用该处理器。 如果未定义 onError, 将针对每个错误发出警告,并对相应的消息部分使用回退表示

    为了确定 formatToParts() 方法返回的值 res, 消息首先被解析为一个 MessageValue 实例列表。 从空数组 res 开始,对于每个 MessageValue mv

    1. msgDir 为消息的基本方向。
    2. bidiIsolationbidiIsolation 选项的解析值。
    3. dirmv.dir
    4. parts 为调用 mv.toParts() 的结果。
    5. 如果调用失败或 parts 不是数组:
      1. parts 设置为 [{ type: "fallback", source: mv.source }]
      2. dir 设置为 "auto"
    6. bidi 为调用 ApplyBidiIsolation(bidiIsolation, msgDir, dir){ start: string, end: string } 结果。
    7. 如果 bidi.start 不是空字符串:
      1. { type: 'bidiIsolation', value: bidi.start } 追加到 res
    8. 对于每个 partparts
      1. part 追加到 res
    9. 如果 bidi.end 不是空字符串:
      1. { type: 'bidiIsolation', value: bidi.end } 追加到 res

    MessageValue

    在格式化消息时, 消息的选择器和占位符首先各自解析为中间的 MessageValue 表示。 这可以看作是一个具有属性和方法的不可变对象, 尽管其 JavaScript 表示仅对自定义函数可用。

    interface MessageValue {
      type: string;
      locale: string;
      dir: 'ltr' | 'rtl' | 'auto';
      source: string;
      options?: { [key: string]: unknown };
      selectKeys?: (keys: string[]) => string[];
      toParts?: () => MessagePart[];
      toString?: () => string;
      valueOf?: () => unknown;
    }
    
    type MessagePart =
      | { type: 'text'; value: string }
      | {
          type: 'bidiIsolation';
          value: '\u2066' | '\u2067' | '\u2068' | '\u2069'; // LRI | RLI | FSI | PDI
        }
      | ({
          type: string;
          source: string;
          locale?: string;
          dir?: 'ltr' | 'rtl' | 'auto';
        } & (
          | { value?: unknown }
          | { parts: Array<{ type: string; value: unknown; source?: string }> }
        ));

    MessageValue 是一个具有字符串 type、字符串 locale 标识符, 以及标识其来源的不透明字符串 source 的对象。 所有其他字段都是可选的; 它们决定该值如何在 MF2 表达式中使用。

    为了能够用作格式化的占位符, 该对象(或其原型链)必须包含一个返回字符串的 toString 方法, 以及一个返回 MessagePart 数组的 toParts 方法。 此方法的所有内置实现都返回恰好包含一个值的数组, 但用户自定义函数可以返回任意数量的部分,或者不返回。

    除了对应于字面值的部分之外, 每个 MessagePart 必须包含 MessageValue 中的 typesource。 它还可以包含一个字符串 locale 标识符, 并可选地包含任何类型的显式 value 或其自身的 parts 序列。

    为了能够用作变体选择器, MessageValue 对象必须包含一个 selectKeys 方法。 当使用字符串键数组调用时, 它必须返回一个元素是这些键的子集的数组。 返回的键将被视为与选择器匹配,并按优先顺序排列。

    在消息中,表达式的值可以赋给消息局部变量, 这样的变量可以用作另一个表达式的输入参数, 或用作选项值。 某些函数(包括默认的 number)接受 具有 valueOf 方法和 options 字段以及 typelocale 作为输入的对象。 这些也可以定义在返回的对象上。

    字面文本

    模式中表达式之外的文本始终是字面值。 虽然其解析后的值永远不会以 JS 形式呈现, 但为简单起见,可以认为它具有以下解析后的值:

    interface MessageText {
      type: 'text';
      source: string;
      locale: string;
      dir: 'ltr' | 'rtl' | 'auto';
      toParts(): [MessageTextPart];
      toString(): string;
    }
    
    interface MessageTextPart {
      type: 'text';
      value: string;
    }

    对于 MessageTexttoString() 返回的值以及 toParts() 返回的对象的 value 字段 对应于文本来源。 其 locale 始终与消息的基本区域设置相同。

    表达式

    表达式用作选择器和模式占位符。 局部变量声明可以将表达式的值赋给局部变量, 从而允许同一表达式在多个地方使用,并可能具有不同角色。

    表达式可以具有以下三种形式之一:

    • 一个操作数(要么是字面值,要么是变量引用)。
    • 一个带有注解的操作数。
    • 一个没有操作数的注解。

    使用 : 前缀的注解的解析可以通过构造函数的 functions 选项自定义, 该选项接受 MessageFunction 函数值,当注解名称(不带 :)与 functions 键对应时应用这些函数值。

    标记

    除了表达式之外,占位符也可以是标记; 即对应于 HTML 元素或其他标记语法的内容。 与表达式不同,标记不接受位置输入参数, 并且其解析不能通过 functions 选项自定义。

    标记占位符有三种不同形式:

    • 用于非文本内容(如内联图像)的"standalone"(独立)标记,
    • 启动标记跨度的"open"(开始)标记,以及
    • 结束标记跨度的"close"(结束)标记。

    标记使用的语法与 XML 有些相似, 但使用花括号 {} 代替尖括号 <>, 并且对于"standalone"和"open"使用 # 作为前缀:{#img /}{#b}{/b}

    标记占位符不要求成对或干净地嵌套; 在格式化器内部,每个占位符仅被单独考虑, 任何更高级别的验证都由调用者负责。

    标记占位符不能用作选择器。 在 format() 中,所有标记都被忽略,每个标记都被格式化为空字符串。 在 formatToParts() 中,每个标记占位符被格式化为单个部分:

    interface MessageMarkupPart {
      type: 'markup';
      kind: 'open' | 'standalone' | 'close';
      source: string;
      name: string;
      options?: { [key: string]: unknown };
    }

    该部分的 type 始终为 "markup", 其 kind"open""standalone""close" 之一。 name 与标记的名称匹配, 不带 #/ 前缀或后缀。 source 与标记占位符的 name 匹配, 并带有适当的 #/ 前缀和后缀。

    options 对应于占位符中包含的选项的解析后字面值和变量值。 例如,当使用 formatToParts({ baz: 13 }) 格式化 {#open foo=42 bar=$baz} 时, 格式化后的部分的 options 将是 { foo: '42', bar: 13 }。 对于具有变量引用值的选项, 如果解析后的值是具有 valueOf() 方法的对象,则使用返回的值。 options 仅支持"open"和"standalone"标记占位符, 并且永远不会包含在"close"标记占位符中。

    MessageFunction

    从根本上说,消息是通过将值拼接在一起形成的。 为了支持使用用户自定义的格式化选项处理用户自定义的值类型, 以及其他需求, 可以通过构造函数的 functions 选项提供用户提供的消息函数, 以补充或替换默认函数。

    type MessageFunction = (
      msgCtx: MessageFunctionContext,
      options: { [key: string]: unknown },
      input?: unknown
    ) => MessageValue;
    
    interface MessageFunctionContext {
      locales: string[];
      dir: 'ltr' | 'rtl' | 'auto';
      source: string;
    }

    msgCtx 值定义了表达式被解析时的上下文, 包含整个消息的 localesdir, 以及表达式的 source 回退字符串表示。

    inputoptions 值按如下方式构造:

    • 如果该值是消息语法中定义的字面值, 则该值是其 string 值。
    • 如果该值是引用局部变量声明的变量, 则该值是声明表达式解析得到的 MessageValue
    • 否则,该值是引用外部值的变量, 其类型和值即为外部值的类型和值。

    由于函数选项通常由字面值设置, 并且由于 MF2 将所有字面值视为字符串, 因此在用作输入或选项值时,应支持数字和布尔值的 JSON 字符串表示。 每个函数都需要从它们的字符串表示中单独解析这些值。

    如果函数依赖区域设置, 它应该接受一个名为 locale 的额外选项键, 该键解析为覆盖消息基本区域设置的字符串。 此选项值应始终解析为数组,或者 (如果是单个字符串)解析为以逗号分隔的 BCP 47 区域设置标识符列表。

    默认函数

    提供了两个常用的消息函数 numberstring 作为起点, 并作为不带注解的占位符的处理器, 例如 {$foo} 之类的变量引用或 {|the bar|} 之类的字面值。

    变量引用通过首先查找与其名称匹配的局部变量声明来解析, 然后在 values 参数中查找。 字面值总是解析为字符串。

    由于选择器表达式必须具有注解,或者包含的变量引用必须引用同一消息中带有注解的变量声明, 因此未注解的表达式只需要在可格式化占位符中考虑。

    如果占位符表达式包含不带注解的变量引用, 并且该变量解析为数字或 bigint 值或 Number 实例, 则它将解析为以该数值作为输入且不带选项调用 number 函数的结果。

    如果占位符表达式将解析为字符串值或 String 实例, 则它将解析为以该值作为输入且不带选项调用 string 函数的结果。

    否则,未注解的值解析为以下形态:

    interface MessageUnknownValue {
      type: 'unknown';
      source: string;
      locale: string;
      dir: 'ltr' | 'rtl' | 'auto';
      toParts(): [MessageUnknownPart];
      toString(): string;
      valueOf(): unknown;
    }
    
    interface MessageUnknownPart {
      type: 'unknown';
      source: string;
      value: unknown;
    }

    对于 MessageUnknownValuetoString() 方法使用 String() 的等价方式格式化该值, 而 valueOf() 返回原始值。

    如果 MessageFormat 实例的构造函数选项包含覆盖默认 numberstring 函数的 functions 值, 则将视情况调用这些函数,而不是默认函数。

    number

    接受以下任何一种作为输入:

    • 数字或 bigint。 直接用作 value
    • 具有返回数字或 bigint 的 valueOf() 方法的对象, 然后将其用作 value
    • 数字的 JSON 字符串表示,以支持 {42 :number} 之类的字面值。 value 通过对字符串调用 JSON.parse() 的等价方式确定, 并断言其返回数字或 bigint。

    如果未使用此类输入调用,或在确定 value 时发生错误, 则返回回退值。

    在内部,构造一个字符串 locales 数组,如 Intl.NumberFormat 和 Intl.PluralRules 构造函数所使用的。 这将按顺序包括:

    1. 表达式的 "locale" 选项设置的任何区域设置。
    2. 输入的区域设置(如果它是对象并且具有字符串或字符串数组属性 "locale")。
    3. 消息的基本区域设置或区域设置链。

    为了确定数字的格式化和选择选项, 创建一个新的空 options 对象。 如果输入是一个具有对象值属性 "options" 的对象, 则用其值扩展 options 对象。 然后,为表达式的每个选项("locale" 除外)设置一个原始值:

    • 对于每个选项值,如果它是对象,则将其强制转换为字符串。
    • 对于 useGrouping,将 "true""false" 转换为其对应的布尔值。
    • 对于 roundingIncrementminimumIntegerDigits(minimum|maximum)(Fraction|Significant)Digits, 将字符串值解析为非负整数。

    返回具有以下形态的值:

    interface MessageNumber {
      type: 'number';
      source: string;
      locale: string;
      dir: 'ltr' | 'rtl' | 'auto';
      options: Intl.NumberFormatOptions & Intl.PluralRulesOptions;
      selectKeys(keys: string[]): string[];
      toParts(): [MessageNumberPart];
      toString(): string;
      valueOf(): number | bigint;
    }
    
    interface MessageNumberPart {
      type: 'number';
      source: string;
      locale?: string;
      dir?: 'ltr' | 'rtl' | 'auto';
      parts: Intl.NumberFormatPart[];
    }

    MessageNumber 用作选择器时(调用其 selectKeys() 方法), 与值精确数字匹配的键将优先于与值的复数类别匹配的键 (zeroonetwofewmanyother 中的一些,具体取决于区域设置)。

    MessageNumber 被格式化时, 调用其 toString() 将返回与调用以下代码对应的字符串

    new Intl.NumberFormat(locales, options).format(value);

    并且调用其 toParts() 方法将返回一个包含单个对象成员的数组, 其中 parts 将对应于调用以下代码的结果

    new Intl.NumberFormat(locales, options).formatToParts(value);
    string

    接受任何输入,并使用 String() 解析任何非字符串值。 对于无输入,将其值解析为空字符串。 出错时,解析为回退值。

    仅接受 locale 选项作为消息区域设置的覆盖。

    返回具有以下形态的值:

    interface MessageString {
      type: 'string';
      source: string;
      locale: string;
      dir: 'ltr' | 'rtl' | 'auto';
      selectKeys(keys: string[]): [] | [string];
      toParts(): [MessageStringPart];
      toString(): string;
      valueOf(): string;
    }
    
    interface MessageStringPart {
      type: 'string';
      source: string;
      locale?: string;
      dir?: 'ltr' | 'rtl' | 'auto';
      value: string;
    }

    MessageString 用作选择器时, 返回的数组最多包含一个条目, 如果其中一个键与值精确字符串匹配。

    回退值

    MessageFunction 调用可能抛出错误, 或者 format()formatToParts() 调用可能抛出错误。 发生这种情况时, 错误会被捕获,并调用用户提供的 onError 处理器。 如果未定义此类处理器, 默认行为是发出警告而不是抛出错误。 这允许始终提供某种格式化表示。

    在这种情况下,对该值使用回退表示:

    interface MessageFallback {
      type: 'fallback';
      locale: 'und';
      dir: 'auto';
      source: string;
      toParts(): [MessageFallbackPart];
      toString(): string;
    }
    
    interface MessageFallbackPart {
      type: 'fallback';
      source: string;
    }

    当解析包含"reserved"(保留)或"private-use"(私有使用)注解的 MF2 表达式时, 也使用此表示。

    MessageFallbacksource 对应于 MessageValuesource。 当 MessageFallback 被格式化为字符串时, 其值为左花括号 {source 值和右花括号 } 的拼接。

    对比

    MF2 规范是基于从现有系统中汲取的经验教训而制定的, 包括 ICU MessageFormatFluent

    Firefox 中 Fluent 的实现主要依赖于 DOM 中的声明式语法, 但它确实提供了一个 API,用于在不用于本地化 DOM 时直接从 Fluent 检索消息。

    实现

    Polyfill/transpiler 实现

    MessageFormat 2.0 规范仍在开发中。 本提案的 polyfill 实现可在 messageformat 项目下获得。