Plural rules and grammatical number
Most languages don't pluralise like English. Use CLDR plural categories — zero, one, two, few, many, other — via Intl.PluralRules instead of hard-coded 'item' vs 'items' logic.
What it is
Grammatical number is how a language changes a word to reflect how many things it refers to. English has two forms — “1 item”, “0 items”, “2 items”, “1.5 items” — and most developers write count === 1 ? "item" : "items" without thinking about it. That logic is wrong for most of the world’s languages.
The Unicode Common Locale Data Repository (CLDR) defines up to six plural categories that a language may use: zero, one, two, few, many, and other. Each language picks a subset and a rule that maps a number to one of them. Arabic uses all six. Russian, Polish and Ukrainian use one / few / many / other, switching on the last digit of the number (“1 файл”, “2 файла”, “5 файлов”). Welsh uses six. Japanese, Chinese, Korean and Vietnamese have no grammatical number at all — every count uses other.
Intl.PluralRules exposes CLDR’s rules to the runtime, so you never have to encode the logic yourself.
Why it matters
Hard-coded English plural logic produces broken sentences in every locale it cannot describe. A Russian user seeing “5 файла” instead of “5 файлов” reads it the way an English reader reads “5 file” — visibly wrong and visibly machine-translated. Translation memory and review costs go up because translators have to fight the code, and copy quality drops because there is no place to put the right forms.
How to implement
Store strings by category, not by number, and let Intl.PluralRules pick the category.
const ar = new Intl.PluralRules("ar");
ar.select(0); // "zero"
ar.select(1); // "one"
ar.select(2); // "two"
ar.select(3); // "few"
ar.select(11); // "many"
ar.select(100); // "other"
const messages = {
zero: "لا توجد ملفات",
one: "ملف واحد",
two: "ملفان",
few: "{n} ملفات",
many: "{n} ملفاً",
other: "{n} ملف",
};
const text = messages[ar.select(n)].replace("{n}", n);
Use Intl.PluralRules with { type: "ordinal" } for “1st / 2nd / 3rd / 4th” — these follow a different CLDR rule set from cardinals. Pair the result with Intl.NumberFormat so digits are rendered for the locale too (Arabic-Indic digits in ar-EG, Latin digits in ar-SA).
Any serious translation file format already encodes this: ICU MessageFormat ({count, plural, one {1 file} other {# files}}), Fluent, and gettext’s Plural-Forms header all map a number to a category at runtime. Use one of those formats so translators can author the forms their language needs without touching code.
Common mistakes
count === 1 ? "item" : "items"— only works for English-like languages.- Detecting the category by string-matching the number (“if it ends in 1…”) — that is reinventing CLDR badly, and different languages have different rules.
- Concatenating strings like
"You have " + count + " items". Even with the right plural, word order differs by language. Use a template with a placeholder. - Forgetting ordinals — “1st place” does not translate via cardinal rules.
- Adding a
zeroform to English. CLDR says English has nozerocategory; “0 items” usesother.