1 of 24

Intl.MessageFormat

TC39 Stage 1 Update

Eemeli Aro, Mozilla / OpenJS Foundation

2 of 24

Outline

  • Changes since first presentation in March 2022
  • API design questions
    • How to format compound values?
    • How to format to parts?
    • How to write custom formatters?

3 of 24

interface MessageFormatOptions {

bidiIsolation?:� 'compatibility' | 'none';

dir?: 'ltr' | 'rtl' | 'auto';

functions?:� { [key: string]: MessageFunction };

localeMatcher?: 'best fit' | 'lookup';

}

��type MessageFunction = (

source: string,

locales: string[],

options: { [key: string]: unknown },

input?: unknown

) => MessageValue;

class Intl.MessageFormat {

constructor(

source: MessageData | string,

locales?: string | string[],

options?: MessageFormatOptions

);

format(

values?: Record<string, unknown>,

onError?: (error: Error) => void

): string;

formatToParts(

values?: Record<string, unknown>,

onError?: (error: Error) => void

): MessagePart[];

resolvedOptions():

ResolvedMessageFormatOptions;

}

4 of 24

Changes since first presentation in March 2022

  • Separation of message resource handling into its own proposal
  • Refactored resolveMessage() method into format() and formatToParts()
  • Split the previously merged definitions of:
    • input value with options
    • mostly internal MessageValue
    • output MessagePart, which no longer has any methods
  • Much work on the underlying MF2 spec, including explicit bidi isolation and a MessageData definition
  • Error handling defaults to emitting warnings

5 of 24

interface MessageFormatOptions {

bidiIsolation?:� 'compatibility' | 'none';

dir?: 'ltr' | 'rtl' | 'auto';

functions?:� { [key: string]: MessageFunction };

localeMatcher?: 'best fit' | 'lookup';

}

��type MessageFunction = (

source: string,

locales: string[],

options: { [key: string]: unknown },

input?: unknown

) => MessageValue;

class Intl.MessageFormat {

constructor(

source: MessageData | string,

locales?: string | string[],

options?: MessageFormatOptions

);

format(

values?: Record<string, unknown>,

onError?: (error: Error) => void

): string;

formatToParts(

values?: Record<string, unknown>,

onError?: (error: Error) => void

): MessagePart[];

resolvedOptions():

ResolvedMessageFormatOptions;

}

6 of 24

interface MessageFormatOptions {

bidiIsolation?:� 'compatibility' | 'none';

dir?: 'ltr' | 'rtl' | 'auto';

functions?:� { [key: string]: MessageFunction };

localeMatcher?: 'best fit' | 'lookup';

}

��type MessageFunction = (

source: string,

locales: string[],

options: { [key: string]: unknown },

input?: unknown

) => MessageValue;

class Intl.MessageFormat {

constructor(

source: MessageData | string,

locales?: string | string[],

options?: MessageFormatOptions

);

format(

values?: Record<string, unknown>,

onError?: (error: Error) => void

): string;

formatToParts(

values?: Record<string, unknown>,

onError?: (error: Error) => void

): MessagePart[];

resolvedOptions():

ResolvedMessageFormatOptions;

}

7 of 24

interface MessageFormatOptions {

bidiIsolation?:� 'compatibility' | 'none';

dir?: 'ltr' | 'rtl' | 'auto';

functions?:� { [key: string]: MessageFunction };

localeMatcher?: 'best fit' | 'lookup';

}

��type MessageFunction = (

source: string,

locales: string[],

options: { [key: string]: unknown },

input?: unknown

) => MessageValue;

class Intl.MessageFormat {

constructor(

source: MessageData | string,

locales?: string | string[],

options?: MessageFormatOptions

);

format(

values?: Record<string, unknown>,

onError?: (error: Error) => void

): string;

formatToParts(

values?: Record<string, unknown>,

onError?: (error: Error) => void

): MessagePart[];

resolvedOptions():

ResolvedMessageFormatOptions;

}

8 of 24

interface MessageFormatOptions {

bidiIsolation?:� 'compatibility' | 'none';

dir?: 'ltr' | 'rtl' | 'auto';

functions?:� { [key: string]: MessageFunction };

localeMatcher?: 'best fit' | 'lookup';

}

��type MessageFunction = (

source: string,

locales: string[],

options: { [key: string]: unknown },

input?: unknown

) => MessageValue;

class Intl.MessageFormat {

constructor(

source: MessageData | string,

locales?: string | string[],

options?: MessageFormatOptions

);

format(

values?: Record<string, unknown>,

onError?: (error: Error) => void

): string;

formatToParts(

values?: Record<string, unknown>,

onError?: (error: Error) => void

): MessagePart[];

resolvedOptions():

ResolvedMessageFormatOptions;

}

9 of 24

interface MessageFormatOptions {

bidiIsolation?:� 'compatibility' | 'none';

dir?: 'ltr' | 'rtl' | 'auto';

functions?:� { [key: string]: MessageFunction };

localeMatcher?: 'best fit' | 'lookup';

}

��type MessageFunction = (

source: string,

locales: string[],

options: { [key: string]: unknown },

input?: unknown

) => MessageValue;

class Intl.MessageFormat {

constructor(

source: MessageData | string,

locales?: string | string[],

options?: MessageFormatOptions

);

format(

values?: Record<string, unknown>,

onError?: (error: Error) => void

): string;

formatToParts(

values?: Record<string, unknown>,

onError?: (error: Error) => void

): MessagePart[];

resolvedOptions():

ResolvedMessageFormatOptions;

}

10 of 24

const src = '{Hello {$place}!}';

const mf = new Intl.MessageFormat(src, 'en');

mf.format({ place: 'world' }); // 'Hello world!'

mf.formatToParts({ place: 'world' });

/* [

{ type: 'literal', value: 'Hello ' },

{ type: 'string', source: '$place', value: 'world' },

{ type: 'literal', value: '!' }

] */

11 of 24

const src = `

input {$count :number}

match {$count}

when 0 {You have no new notifications}

when one {You have {$count} new notification}

when * {You have {$count} new notifications}`;

const mf = new Intl.MessageFormat(src, 'en');

mf.format({ count: 0 }); // 'You have no new notifications'

mf.format({ count: 1 }); // 'You have 1 new notification'

12 of 24

How to format compound values?

  • With existing formatters, some “options” are really a part of the value
    • NumberFormat: currency, unit
    • DateTimeFormat: timeZone
  • A MessageFormat user should be able to pass a single argument that can include e.g. a number (42) and a currency code ('EUR')

13 of 24

How to format compound values?

  • Formatters like :number allow as input objects with a valueOf() method returning a number or bigint, with an options property that is merged with message expression options.

const src = '{Your total is {$price :number style=currency}}';

const mf = new Intl.MessageFormat(src, 'en');

const price = new Number(42);

price.options = { currency: 'EUR' };

mf.format({ price }) // 'Your total is €42.00'

14 of 24

How to format to parts?

  • MessageFormat needs to provide a formatToParts() method.
  • A formatted message may include values like numbers that themselves format to parts.
  • A formatted message may include values that should not be stringified, such as DOM elements, React components, or any imaginable objects.

15 of 24

How to format to parts?

  • Let each formatted part to contain either a single value or a sequence of parts. Allow for the values of the top-level or sub-parts to be of any type, but stick to string values where possible in built-in functions.

mf.formatToParts({ price }) /* [

{ type: 'literal', value: 'Your total is ' },

{ type: 'number', source: '$price', parts: [

{ type: 'currency', value: '€' },

{ type: 'integer', value: '42' },

{ type: 'decimal', value: '.' },

{ type: 'fraction', value: '00' } ] }

] */

16 of 24

How to format to parts?

  • Let each formatted part to contain either a single value or a sequence of parts. Allow for the values of the top-level or sub-parts to be of any type, but stick to string values where possible in built-in functions.

const src = '{Have a {cow.png :image alt=Cow}}';

const mf = new Intl.MessageFormat(src, 'en',

{ functions: { image: … } });

mf.formatToParts() /* [

{ type: 'literal', value: 'Have a ' },

{ type: 'image', source: 'cow.png',

value: <img src="cow.png" alt="Cow"> }

] */

17 of 24

How to write custom formatters?

  • MF2 allows for users to define their own functions that are called by name, like :image in the previous.
  • The resolved value of an expression can be assigned to a message-internal variable and then used:
    • as a selector,
    • as a value formatted to a string,
    • as a value formatted to parts,
    • as an input to another function, or
    • as an option value.

18 of 24

How to write custom formatters?

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;

}

  • Include a functions option mapping identifiers to user-defined functions. Require each such function to return a MessageValue object with methods defining its supported use cases.

type MessageFunction = (

source: string,

locales: string[],

options: { [key: string]: unknown },

input?: unknown

) => MessageValue;

19 of 24

How to write custom formatters?

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;

}

  • Include a functions option mapping identifiers to user-defined functions. Require each such function to return a MessageValue object with methods defining its supported use cases.

Selector

Format to parts

Format to string

20 of 24

How to write custom formatters?

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;

}

  • Include a functions option mapping identifiers to user-defined functions. Require each such function to return a MessageValue object with methods defining its supported use cases.

To use as input/option value, MessageValue needs to match the “compound value” interface.

  • Would resolvedOptions() be better than options?
  • Would value be better than valueOf()?

21 of 24

Incubator call TBD

Help me write the spec text for this?

22 of 24

Links

23 of 24

Why is this so difficult?

  • Because localization is deceptively easy.
    • 80% of all UI messages are plain strings
    • 80% of the rest are simple variable replacements
    • If we don’t initially account for all the weirdness* in the remainder, we’ll make choices that will fundamentally limit what can be achieved.

* Including, but not limited to: formatter options, cardinal & ordinal plurals, custom formatters, custom selectors, markup elements, composite values, formatting to parts.

24 of 24

Why is this so difficult?

  • Minimal API for an interpreter and runtime for a new message formatting language
  • User-customisable selectors and formatting functions
  • Multiple types of input and output