ICU MessageFormat

The ICU plural format

ICU MessageFormat puts the whole plural inside one message: {count, plural, one {…} other {…}}. The branches are named after CLDR categories, the runtime picks the right one for the locale and the number, and # stands for the count itself. Because the value is self contained, the plural survives any tool that moves the file, which made ICU the de facto interchange encoding.

This page covers the syntax, where it came from, and the same plural written in JSON, YAML and properties files.

Examples

The same plural, three languages

The message grows with the language: English needs two branches, Polish four, Arabic six. Switch the file format to see the encoding is identical everywhere; only the quoting changes.

en · two branches
"items": "{count, plural, one {# item} other {# items}}"
pl · four branches
"items": "{count, plural,
  one {# artykuł}
  few {# artykuły}
  many {# artykułów}
  other {# artykułu}}"
ar · six branches
"items": "{count, plural,
  zero {لا عناصر} one {عنصر واحد}
  two {عنصران} few {# عناصر}
  many {# عنصرًا} other {# عنصر}}"
en
items: "{count, plural, one {# item} other {# items}}"
pl
items: "{count, plural,
  one {# artykuł} few {# artykuły}
  many {# artykułów} other {# artykułu}}"

The braces mean the value must be quoted in YAML; a bare brace would start a map.

en
cart.items={count, plural, one {# item} other {# items}}
pl · continued line
cart.items={count, plural, one {# artykuł} \\
  few {# artykuły} many {# artykułów} other {# artykułu}}

How it works

Selection happens at runtime, by rule

The library formatting the message asks CLDR which category the number belongs to in the current locale, then renders that branch. Polish 22 is few, Polish 25 is many, and the translator never writes an if statement: the categories in the message are the whole contract. Beyond plural, the same syntax offers select for branching on things like grammatical gender, selectordinal for 1st/2nd/3rd style ordinals, and explicit matches like =0 for special casing exact values.

Origin

Where it came from

The lineage starts with Java’s MessageFormat, which shipped with JDK 1.1 in 1997 and could already interpolate and branch on numbers. IBM’s ICU project (International Components for Unicode) carried the idea further and grounded plural selection in CLDR’s category data, replacing per language special cases with one rule set. JavaScript implementations, FormatJS and react-intl among them, brought the syntax to the web, and Flutter adopted it for ARB messages.

Trade offs

Strengths and limits

strength

Self contained

The plural lives in the value, so any pipeline that can move a string can move the whole plural without understanding it.

strength

More than plurals

select, selectordinal and nested arguments cover gender, ordinals and combinations no key convention can express.

limit

Braces are hostile to humans

A translator editing raw ICU can break a message with one brace. Good tooling shows per form fields and reassembles the string.

In locamorph

How locamorph treats ICU

ICU is the default plural format on export. On import, ICU plurals decompose into per form fields for properties and ARB files; in JSON and YAML the message imports whole, every form intact in one string. Messages using select, offset: or explicit =N matches always travel whole, because splitting them would drop meaning.

Further reading

Edit forms, not braces

Import ICU messages and translators get one field per form; exports rebuild the message exactly.