# Plural forms in translations: ICU messages and CLDR

> English needs two plural forms, Arabic needs six. How ICU MessageFormat and the CLDR categories decide which word form a number gets, and how to check it.

Published 2026-10-28 · https://aiskills402.com/blog/icu-plural-messages-cldr-rules

A message such as "3 files" looks trivial until it is translated. Each language decides which form of the noun, and sometimes of the verb, a given number requires, and a string built by adding an "s" to a word cannot express that. The standard answer is a plural message: one string per language that lists the forms and lets a library choose among them using rules published by the Unicode CLDR project. This guide explains how the categories work, how the ICU message syntax uses them, where hand-written messages go wrong, and how to test a message against the real rules of a runtime.

## Categories are per language, not per sentence

CLDR names six plural categories, running from zero through one and two, then few and many, with a catch-all called other. Every language uses a subset of them, and the catch-all never goes missing, because it doubles as the general form in languages that have only a single form ([Unicode CLDR, plural rules](https://cldr.unicode.org/index/cldr-spec/plural-rules)).

The spread is wide. MDN describes English as having two cardinal forms, Chinese as having one, and Arabic as having six ([MDN, Intl.PluralRules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules)). So a translator working into Arabic has to supply six phrases for a message whose English original has two, while a Chinese translator supplies one.

The categories also do not copy grammar-book terms. CLDR defines them by a practical test: two numbers belong to different categories when swapping one for the other forces a change somewhere else in the sentence. That change can be in a noun, a verb or a pronoun. The CLDR text uses an example where the noun never changes, "sheep", yet English still needs two categories because "is here" becomes "are here" and "it" becomes "them" (the sheep example is in [Unicode LDML, Numbers](https://unicode.org/reports/tr35/tr35-numbers.html)).

A second CLDR example shows why guessing from a neighbouring language fails. The CLDR page quotes a sample defect report about draft Ukrainian rules, where the forms of "day" differ for 1, for 2, for 5, and again for fractions such as 1.31 (the report asks for a separate category for fractions) ([Unicode CLDR, plural rules](https://cldr.unicode.org/index/cldr-spec/plural-rules)). A message written from an English template, with a singular arm and a plural arm, covers none of that.

## How an ICU message expresses it

ICU MessageFormat puts the alternatives in one pattern string. According to the ICU guide, a plural argument picks a sub-message by combining a number with the rules of the target language, while a select argument picks by one of a fixed set of keywords ([ICU User Guide](https://unicode-org.github.io/icu/userguide/format_parse/messages/)). A minimal English message looks like this:

```
{count, plural,
  =0 {Empty}
  one {# item}
  other {# items}
}
```

Three details in that snippet carry most of the weight.

- **The `other` arm is mandatory.** The ICU guide states that a plural argument always needs an `other` variant ([ICU User Guide](https://unicode-org.github.io/icu/userguide/format_parse/messages/)). A parser that finds none rejects the message, which is the lucky failure, because it is visible.
- **`=0` is not the same as `zero`.** An arm written with an equals sign matches precisely that number whatever the language, whereas `zero` is a category belonging to a few languages only. CLDR explains that English has no zero category because the same message works for 3 friends and for 0 friends, yet many languages sound more natural with a dedicated "none" sentence, so message APIs should let developers write explicit 0 and 1 cases ([Unicode LDML, Numbers](https://unicode.org/reports/tr35/tr35-numbers.html)). An English `zero` arm is therefore dead text, and the exact arm is the right tool.
- **The hash sign stands for the number.** Inside an arm it is replaced by the number, as the ICU documentation's plural examples show ([ICU User Guide](https://unicode-org.github.io/icu/userguide/format_parse/messages/)). Whether the library also formats it with the locale's own grouping separators depends on the library, so test it.

The same guide gives a remark that matters for translators. An English source that lists exact arms for `=0` and `=1` (up to the offset plus one) can skip the `one` arm entirely, since nothing would ever select it. The translation, however, has to add `one`, `few` and the others wherever the target language needs them ([ICU User Guide](https://unicode-org.github.io/icu/userguide/format_parse/messages/)). An English source with `=0`, `=1` and `other` is a valid source and a poor template.

## Offset, select and nesting

The ICU guide demonstrates an offset in a message about a host inviting guests: the pattern declares an offset of 1 and the hash then shows the count minus that offset, producing "and 3 other people" without a separate arm for each size ([ICU User Guide](https://unicode-org.github.io/icu/userguide/format_parse/messages/)). Exact arms such as `=1` still look at the full count, which is general behaviour of the format rather than something every library documents prominently, so test it in the library you use.

On nesting, the guide recommends making a complex argument the outermost structure of a message, writing whole sentences inside the arms, and, when select and plural are combined, putting the select outside and the plural inside ([ICU User Guide](https://unicode-org.github.io/icu/userguide/format_parse/messages/)). The reason it gives is practical: a plural wrapped around a sentence fragment leaves a translator unable to see how the fragment fits the rest, and unable to widen or narrow the part that varies.

## Apostrophes break more messages than plurals do

ICU treats a pair of ASCII apostrophes as one literal apostrophe and uses apostrophes to quote syntax characters such as braces. Since ICU 4.8 an apostrophe opens quoted text only if the very next character is one that must be quoted, and the guide recommends the typographic apostrophe for human-readable text and keeping the ASCII one for syntax ([ICU User Guide](https://unicode-org.github.io/icu/userguide/format_parse/messages/)).

The consequence shows up in languages that elide a vowel before a name (the version rule is in the [ICU guide](https://unicode-org.github.io/icu/userguide/format_parse/messages/)). French "l'" followed directly by a placeholder puts an apostrophe right before a brace, so the library starts a quoted span and prints the placeholder as raw text. Doubling the apostrophe, or using the typographic character, avoids it. This follows from the quoting rule above; the guide does not mention French, so treat the example as an application of the rule rather than a quotation. Older libraries that predate the 4.8 behaviour can treat apostrophes differently, which is a reason to test on the exact runtime.

## Ordinals are a separate rule set

"1st, 2nd, 3rd" is not a cardinal plural. MDN notes that English has four ordinal forms while Chinese and Arabic have just one, and that the same `select()` method of `Intl.PluralRules` can return a tag for a cardinal or an ordinal number ([MDN, Intl.PluralRules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules)). In ICU syntax the counterpart of `plural` for this purpose is `selectordinal`. Copying the English set of arms into every language is the usual mistake; a language with a single ordinal form needs only `other`.

## A check that can fail

Reading a message does not prove it. The runtime that will serve the page already holds the CLDR data, so ask it which category each of a handful of numbers gets, then compare that to the arms in the message.

```js
const locales = ["en", "ar", "ja"];
for (const type of ["cardinal", "ordinal"]) {
  for (const loc of locales) {
    const rules = new Intl.PluralRules(loc, { type });
    const seen = new Set(rules.resolvedOptions().pluralCategories);
    console.log(type, loc, [...seen].join(","));
  }
}
```

The output lists exactly the categories that language can select. A translated message whose category arms differ from that list is suspect in one of two ways: an arm that can never be chosen, or a missing arm that sends some numbers to `other`. Exact arms such as `=0` and `=1` are not categories and do not count here, and an English source that covers them may leave out `one` on purpose. Then sample numbers (see the [MDN page](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) for the method) with `rules.select(n)` for edge values such as 0, 1, 2, 5, 11, 21 and a fraction, and print the sentence each locale would show. Make the check able to fail by feeding it a message you know is wrong, for example a Japanese message with a `one` arm, and confirm it is flagged. Using `resolvedOptions().pluralCategories` is general practice with this API rather than something the cited MDN extract spells out, so confirm it against your runtime.

## When this does not apply

Scope: ICU MessageFormat strings and the CLDR categories. It does not apply to libraries that use their own plural syntax, such as gettext catalogues with their own formulas, where rules are written per catalogue and may differ from CLDR. The newer, redesigned MessageFormat syntax has a different notation (this guide did not read its specification), so the snippets here are for the long-standing syntax. CLDR data also changes between releases, so a language's category set or the numbers in each category can shift, and the check above should be re-run after upgrading a runtime. This text did not run experiments on any library, it did not read the full CLDR chart for each language, and it does not list the categories of any language beyond those the cited pages mention. Finally, correct categories do not make a translation good: the words inside each arm still need a native reader.

If you are translating whole locale files and want the surrounding workflow, the article on [translating locale files with a model](/blog/translate-locale-files-llm) covers the file-level risks that sit around messages like these.

## Writing these messages with a skill

Our [icu-plural-messages](https://aiskills402.com/skills/icu-plural-messages) skill writes and repairs ICU strings for several locales in one request. For each language it picks the matching categories, never leaves out the catch-all arm, adds exact arms where nothing or one needs its own wording, copes with offset, select, selectordinal and nesting, handles apostrophe and brace quoting, and hands back bare JSON keyed by locale. It targets libraries such as react-intl, FormatJS, Angular, Flutter intl and Java MessageFormat.
