ICU Message FormatterLoading saved progress…

ICU Message Formatter

ICU MessageFormat is a string syntax for translatable text: a single message encodes its own variables, plural forms, and category choices, so it renders correctly for any set of values without the caller stitching grammar together by hand. It is the format behind FormatJS, react-intl, and Android string resources, and it exists because real sentences are grammar, not concatenation — "1 item" versus "3 items" is a decision the message itself must make. You implement icuMessageFormatter(message, values), which parses one ICU string and returns the finished text.

Signature

function icuMessageFormatter(
  message: string,                   // an ICU MessageFormat string
  values: Record<string, unknown>,   // a value per named argument
): string;                           // the formatted output

Examples

A plural argument picks a branch from a count, and each # inside the chosen branch becomes that count:

const msg = '{count, plural, =0 {no items} one {# item} other {# items}}';
icuMessageFormatter(msg, { count: 0 }); // 'no items'  (exact =0 wins)
icuMessageFormatter(msg, { count: 1 }); // '1 item'    (one)
icuMessageFormatter(msg, { count: 7 }); // '7 items'   (other, # -> 7)

A select argument picks a branch by matching a string value, falling back to other:

const msg = '{gender, select, male {he} female {she} other {they}}';
icuMessageFormatter(msg, { gender: 'female' }); // 'she'
icuMessageFormatter(msg, { gender: 'droid' });  // 'they'  (no match -> other)

Notes

  • Simple arguments{name} is replaced with values.name. A message with no {...} at all comes back unchanged.
  • plural — try an exact =N {..} branch first (so =0 {no items} beats the categories); otherwise apply the keyword rule, where a count of 1 selects one and every other count selects other. Replace each # in the chosen branch with the count.
  • select — choose the branch whose key equals String(values[arg]); when none matches, use the other branch, which a well-formed select always provides.
  • Nesting — a branch body is itself a message and may hold further arguments, plurals, or selects. Format it recursively, and preserve whitespace inside a branch exactly as written.
  • Balanced braces are the hard part — a nested {..} inside a branch must not be mistaken for the branch's closing brace. Track brace depth instead of splitting on the first } you meet.
  • Out of scope — apostrophe escaping (an ICU literal brace written as an apostrophe pair) and number or date skeletons such as {n, number, percent}. Assume every {...} is a simple argument, a plural, or a select.

FAQ

Why can't a single regex replace handle ICU messages?
A flat regex over {word} has no notion of balanced braces, so a nested {..} inside a plural or select branch fools it into ending the branch early. It also cannot pick a plural or select branch or turn # into the count. You need a recursive parser that tracks brace depth.
How does a plural argument choose its branch?
An exact =N branch is tried first, so =0 {no items} beats the keyword categories. If none matches exactly, the keyword rule applies: a count of 1 selects one and every other count selects other. Inside the chosen branch the literal # is replaced with the count.
What does select do when no branch matches the value?
It falls back to the other branch, which every well-formed select message must provide. The branch is chosen by comparing String(values[arg]) against each branch key, so select works on any stringifiable value, not just numbers.
Loading editor…