StringLane

    Type to search titles, sections and body text across all 58 pages.

    Concept

    What Is ICU MessageFormat? Syntax, Plural and Select

    ICU MessageFormat is the syntax for strings that change with a number or a choice. Plural and select syntax, a worked example, and the errors that break it.

    Last updated

    ICU MessageFormat is a syntax for writing one string that changes with a number or a choice, such as {count, plural, one {# file} other {# files}}. At runtime the formatter picks the branch that fits the active locale and value, so "1 file" and "5 files" come from one message, and so do the four forms Ukrainian needs.

    It comes from the International Components for Unicode (ICU) project. Flutter ARB files use it for every plural and select, and i18next reads it through the i18next-icu plugin. If you localize a Flutter or i18next app, you will write it.

    Why do plurals need ICU MessageFormat?#

    You cannot translate plurals by gluing a number to a noun. English has two cardinal forms ("1 item", "2 items"); Ukrainian has four; Arabic has six. Hard-coding "$count items" produces broken grammar in most of the world. An ICU message carries every form in one string, and the runtime picks the right one for the active locale and number.

    How does ICU plural syntax work?#

    {count, plural, =0 {No files} one {# file} other {# files}}
    • count: the variable the message branches on.
    • plural: the message type.
    • =0: an exact-value branch. It matches only that number and is checked before the categories.
    • one, other: plural categories. # is replaced by the number at runtime.

    The categories come from CLDR, the Unicode locale data ICU reads: zero, one, two, few, many, and other. Each language uses a subset. English uses one and other; Ukrainian uses one, few, many, and other; Arabic uses all six. other is always required: it is the fallback when no other branch matches.

    Flutter is the exception to the # rule. Its gen-l10n has no pound token and displays # literally, so in an ARB file write the variable instead: {count, plural, one {{count} file} other {{count} files}}.

    How does ICU select syntax work?#

    select branches on a string value you pass in rather than a number. Grammatical gender is the most common use:

    {gender, select, female {She liked this} male {He liked this} other {They liked this}}

    Unlike plural, select has no fixed set of options: you name the branches yourself, and the value you pass at runtime picks one. A value that matches no branch uses other, which is why select requires an other branch too.

    Example: a select with a plural inside#

    Real strings often need both. ICU's own guidance is to put the select on the outside and write a complete sentence in each branch, so a translator can reorder the words for their language:

    {gender, select,
      female {{count, plural, one {She shared # file} other {She shared # files}}}
      male {{count, plural, one {He shared # file} other {He shared # files}}}
      other {{count, plural, one {They shared # file} other {They shared # files}}}
    }

    Each translation has to carry the same select branches as the source, and each nested plural has to carry the categories its own language needs, which are not English's.

    Where do ICU messages break?#

    The failure that ships most quietly is a translation that keeps English's plural categories. This Ukrainian message, written with {count} so it holds in Flutter too, parses, compiles and renders:

    {count, plural, one {{count} файл} other {{count} файлів}}

    It shows "3 файлів" where Ukrainian says "3 файли", for every count ending in 2, 3 or 4, except those ending in 12, 13 or 14, because those counts belong to few and the message has no few branch. Nothing errors. The correct message carries all four categories:

    {count, plural, one {{count} файл} few {{count} файли} many {{count} файлів} other {{count} файлу}}

    The other common failures:

    • A missing other branch. The message fails to parse, so it never renders as written.
    • Unbalanced braces, usually from editing a nested message by hand.
    • A dropped select branch. A translation that loses female still renders, but that case falls back to other and the reader gets the wrong sentence.
    • # in a Flutter ARB file, displayed literally instead of the number.
    • A placeholder such as {name} that exists in the source but vanished from a translation.

    These are hard to catch by reading raw files, because the error is in the structure of the message, and often in a language you do not read.

    How do you check ICU messages across every locale?#

    StringLane parses every ICU message in every locale of a Flutter ARB or i18next project and reports the failures above: syntax that does not parse, plural categories a language requires but a translation lacks, select branches missing against the source, # in Flutter, and dropped placeholders. In the desktop app they appear as validation badges on each cell as you edit. The free CLI runs the same checks from a terminal or CI, and writes nothing into your repository:

    npx @stringlane/cli@latest check .

    Inspect and Check a Project covers turning that into a CI gate, and the Validation Code Reference lists every check by code.

    Next steps#

    Frequently asked questions

    What is the difference between plural and select in ICU?
    plural branches on a number and uses the CLDR categories for the target language (zero, one, two, few, many, other). select branches on an arbitrary string value you supply, such as a gender or a role, and you define the option names yourself. Both require an other branch as the fallback.
    Why do I need a few or many plural category?
    Because the target language has grammatical forms English does not. Ukrainian and Polish need few and many, Arabic needs all six. Translating one and other alone leaves those forms to fall through to other, which produces grammatically wrong text for whole ranges of numbers.
    Why does # show up literally in my Flutter app?
    Because Flutter's gen-l10n has no pound token. Most ICU runtimes, i18next-icu included, replace # with the number, but a Flutter app displays a literal #. In ARB files, write the plural variable instead: {count} rather than #.
    Is the other branch really always required?
    Yes, for both plural and select. other is the fallback used when no other branch matches, and an ICU parser rejects a plural or select message that does not have one.
    Which localization formats use ICU MessageFormat?
    Flutter ARB files use ICU syntax for every plural and select, and i18next reads ICU messages through the i18next-icu plugin. iOS .stringsdict files, iOS String Catalogs, Android strings.xml and default i18next JSON express plurals in their own structures instead of ICU syntax, though all of them use the same CLDR plural categories.
    The StringLane app showing the key home.welcomeBack, used in three places in Swift files, with four locale cards: English, German and Japanese in place and French flagged PARAM.

    See every locale side by side

    The desktop app flags what is missing or broken on the cell, and saves to your files.

    Download for macOS

    14-day trial, no email