How ICU MessageFormat Plural Rules Work
10 min read · updated August 11, 2026
ICU MessageFormat exists because count === 1 ? "file" : "files" is a rule about English that has been written into the source code of an application that will be translated. The plural syntax moves that decision out of the code and into the message, where each language can answer it differently.
The six categories
CLDR defines six plural categories. No language uses all six for cardinals except Arabic and Welsh; most use two; English uses two and Japanese uses one. The names are labels, not meanings — one does not mean “the number one” and few does not mean “a small number”. Each is defined per language by a rule over the numeric value.
category a language that uses it and what it covers
-------- ---------------------------------------------------------
zero Latvian: n mod 10 = 0, and 11–19
Welsh and Arabic: exactly 0
one English: exactly 1 with no decimals
Russian: 1, 21, 31, 101 — but NOT 11
French: 0 and 1 both
two Arabic: exactly 2 (the dual)
Welsh: exactly 2
Slovene: n mod 100 = 2
few Polish and Russian: n mod 10 in 2–4, excluding 12–14
Arabic: n mod 100 in 3–10
Welsh: exactly 3
many Russian: n mod 10 = 0, 5–9, or n mod 100 in 11–14
Arabic: n mod 100 in 11–99
Welsh: exactly 6
Portuguese: the compact-million case (1000000, 2000000…)
other Every language. The required fallback.The Russian row is the one to read twice. one covers 21 and 101 but not 11, and many covers 11 through 14 as well as 0 and 5 through 9. That is not an ICU convention; it is Russian grammar, which is why двадцать один дом takes a nominative singular and одиннадцать домов takes a genitive plural.
The authoritative data is the CLDR language plural rules chart, published by the Unicode Consortium with each CLDR release. Read the rule for your target language there rather than inferring it.
many category in a CLDR release to handle compact million forms, which means a message authored against an older CLDR is missing a form under a newer one. Treat the table above as the published values at the time of writing and re-check on an ICU or CLDR upgrade.The syntax
{fileCount, plural,
=0 {No files selected}
one {# file selected}
other {# files selected}}fileCountis the argument name;pluralis the format type.=0is an explicit value match. It is tested before the categories and wins outright, which is how you write “no files” without pretending zero is a plural category in English.#is replaced by the argument, formatted with the locale’s number format — so it renders as1,234in English and1.234in German, without a separate call.otheris mandatory. A message without it fails to parse.
Two more features earn their keep. offset:1 subtracts from the value before category selection and before # is rendered, which is how “You and 3 others liked this” is written as one message. And select handles non-numeric branching, usually gender, and nests inside plural.
{likeCount, plural, offset:1
=0 {No one liked this}
=1 {{name} liked this}
one {{name} and # other liked this}
other {{name} and # others liked this}}
{gender, select,
female {{count, plural, one {She has # message}
other {She has # messages}}}
male {{count, plural, one {He has # message}
other {He has # messages}}}
other {{count, plural, one {They have # message}
other {They have # messages}}}}Four things that are not obvious
- Decimals change the category. CLDR rules read several operands, not just the value:
iis the integer part,vthe number of visible fraction digits,fthe fraction digits themselves. Englishonerequiresi = 1 and v = 0, so1 fileisoneand1.0 filesisother. That is correct English and it surprises everybody the first time. It also means you must pass the formatted precision through, not just the number. - You cannot author the foreign categories. An English source message has
oneandotherand nothing else. The Polish translation needsone,few,manyandother, and those extra forms have to be created during translation. A pipeline that copies the source structure and asks for a string per existing branch produces Polish that is wrong for 22 and for 25. - Do not select the category yourself. Writing an
ifchain that reproduces the Russian rule is a bug waiting for a CLDR update. Call the library. In JavaScript,Intl.PluralRulesis built in and needs no dependency. - Explicit matches do not substitute for categories.
=1matches only the value 1; it does not fill theonebranch for Russian’s 21. A message written with=1andotheris correct in English and wrong in every language whoseonecovers more than one value.
new Intl.PluralRules("en").select(1) // "one"
new Intl.PluralRules("en").select(1.0) // "one" — 1.0 is the NUMBER 1
new Intl.PluralRules("en").select("1.0") // "other" — v = 1
new Intl.PluralRules("ru").select(1) // "one"
new Intl.PluralRules("ru").select(2) // "few"
new Intl.PluralRules("ru").select(5) // "many"
new Intl.PluralRules("ru").select(11) // "many"
new Intl.PluralRules("ru").select(21) // "one"
new Intl.PluralRules("ru").select(22) // "few"
new Intl.PluralRules("ar").select(0) // "zero"
new Intl.PluralRules("ar").select(2) // "two"
new Intl.PluralRules("ar").select(11) // "many"
new Intl.PluralRules("ar").select(100) // "other"The "1.0" line is the demonstration of the v operand: passing a string preserves the visible fraction digits, and passing the number does not, because the number 1.0 is the number 1.
Ordinals and ranges
Cardinal rules are not ordinal rules, and English is the clearest proof: the cardinal categories are one and other, while the ordinals need four, for “1st”, “2nd”, “3rd” and “4th”. In ICU MessageFormat the format type is selectordinal, and in JavaScript it is a separate Intl.PluralRules option.
{place, selectordinal,
one {#st place}
two {#nd place}
few {#rd place}
other {#th place}}
const ord = new Intl.PluralRules("en", { type: "ordinal" });
ord.select(1) // "one" → 1st
ord.select(2) // "two" → 2nd
ord.select(3) // "few" → 3rd
ord.select(4) // "other" → 4th
ord.select(11) // "other" → 11th
ord.select(21) // "one" → 21stNumber ranges are a third data set again. “1–5 files” uses plural range rules, which say which category a range takes given the categories of its endpoints, and those differ by language. Intl.PluralRules exposes selectRange for this in runtimes that have implemented it; ICU4J and ICU4C have had it longer.
Implementing it
- Pick the library and stop hand-rolling.
intl-messageformator@formatjs/intlin JavaScript,ICU4JMessageFormatin Java,PyICUin Python,golang.org/x/text/messagein Go. All read the same CLDR data. - Author every count-bearing string as a full plural message in the source language, even where English needs only two branches. A string extracted as plain text cannot gain plural forms in translation without a code change.
- Pass the count as a named argument and let
#format it. Do not pre-format the number into a string and interpolate it: you lose the locale number format and thevoperand at the same time. - Use
=0for the “none” case rather than adding azerobranch, unless the target language genuinely has one. Thezerocategory in Latvian covers far more than the value zero, so writing “No files” there is wrong for 20 and 30. - Make the translation tool expand the categories per locale. Any competent TMS reads the CLDR rules and presents the right number of boxes; a spreadsheet handed to a translator does not, and that is how Polish ships with two forms.
- Test each locale at the boundary values, not at 1 and 2. For Russian that is 1, 2, 5, 11, 21, 22, 25 and 111. For Polish, 1, 2, 5, 12, 22 and 25. For Arabic, 0, 1, 2, 3, 11, 100. A test suite that checks 1 and 5 passes on a message that is broken for 21.
- Fail the build on a missing
otherbranch and on a message whose branches do not match the target locale’s categories. Both are static checks over your message catalogue and both catch the errors that otherwise reach a native speaker.
The wider point is the one that connects this page to the question of how many forms a language needs: the count-to-wording decision belongs to data published by the Unicode Consortium and updated on a schedule, not to your application and not to a language model asked politely to get the grammar right.