Why your plural rules are broken in Arabic and Russian

Diagram showing CLDR plural forms: 1-form to 6-form languages on a spectrum

Most engineering teams discover their plural rule implementation is broken the same way: a user reports that the app says "You have 21 items" when it should say "21 items" — or worse, they see a completely untranslated English fallback in a locale that was supposedly shipped. The string in question is something like "You have {count} {count, plural, one {item} other {items}}", and it works correctly in English and German because both languages have two plural forms. It fails in Arabic because Arabic has six, and in Russian because Russian's rules are irregular in ways that don't map to a two-form mental model.

The fix is not to add more strings. It's to understand what CLDR plural rules actually specify and why most i18n library implementations of them contain subtle but consequential gaps.

The CLDR plural rule specification

The Unicode Common Locale Data Repository (CLDR) defines plural categories for every language it covers. The categories are: zero, one, two, few, many, and other. Not every language uses all six — English only uses one and other. But the system must be implemented to handle the full set, and the logic for which numbers map to which category is language-specific and non-obvious.

Each plural rule in CLDR is expressed as a condition operating on the integer value n, the number of visible fraction digits f, the integer value of the visible fraction digits v, and a few other operands. The rule for English one is simply n = 1. The rule for Russian one is n % 10 = 1 and n % 100 != 11 — meaning 1, 21, 31, 41, 51, 61, 71, 81, 91, 101... all take the one form, but not 11. The few form applies to numbers ending in 2–4 but not 12–14. Everything else is many. Russian doesn't use zero, two, or other for integers.

Arabic uses all six forms. The zero form applies when n = 0. The one form applies when n = 1. The two form applies when n = 2. The few form applies when n % 100 is between 3 and 10. The many form applies when n % 100 is between 11 and 99. other covers the rest. An Arabic-speaking user counting items sees six grammatically distinct forms in natural speech; a product that serves only one and other produces text that reads as grammatically incorrect to a native speaker for most integer values above 2.

Why most i18n libraries only half-implement this

The dominant JavaScript i18n libraries — i18next, react-intl, formatjs — implement the CLDR plural rules, but implementation quality varies in the details. The common failure mode is not the rule logic itself but the message format that wraps it.

ICU message format is the standard for expressing plural-sensitive strings. A fully correct Arabic plural string looks like:

{
  "item_count": "{count, plural, =0 {لا يوجد عناصر} one {عنصر واحد} two {عنصران} few {{count} عناصر} many {{count} عنصرًا} other {{count} عنصر}}"
}

That's a six-form ICU message. The failure mode is that most teams write:

{
  "item_count_one": "عنصر واحد",
  "item_count_other": "{count} عناصر"
}

They've externalized the plural selection to the application code with a conditional like count === 1 ? t('item_count_one') : t('item_count_other'). This works for English. For Arabic, it produces two forms where six are needed. For Russian, it produces the wrong split because the hard-coded === 1 check doesn't implement the modulo logic.

The correct approach is to let the i18n library — or the ICU message parser — handle plural selection, because the library has locale-aware rules. The application code only provides the count; it doesn't decide which form to use. When the library is correctly configured with CLDR data for the target locale, t('item_count', { count: 21 }) in Russian correctly selects the one form because 21 % 10 = 1 and 21 % 100 ≠ 11.

The Polish and Czech problem: few is not what you think

Teams that do implement multi-form plurals often assume that few covers small numbers (2, 3, 4) and many covers large ones. This is roughly true for Slavic languages, but the boundary condition is where bugs hide.

Polish plural rules: one for n = 1. few for n % 10 in [2, 3, 4] and n % 100 not in [12, 13, 14]. many for everything else including n = 0. "22 pliki" uses few. "12 plików" uses many. "112 plików" uses many. "122 pliki" uses few again. The hundreds digit doesn't matter; the tens-and-units combination does, with exceptions for the teens.

A team that shipped a Polish localization using only one and other would produce grammatically wrong output for every count value except 1 where the correct declension isn't other. Users who count things frequently — a file manager, a task app, a notification count — encounter the error constantly.

We're not saying that engineering teams are careless about this. The problem is that plural rules are easy to test for n = 0, 1, 2 and easy to miss for n = 11, 12, 21, 22. A test suite that checks "zero/one/two items" will pass for every language and miss the tens-and-hundreds edge cases that CLDR actually specifies.

CLDR-compliant plural testing: what the test matrix should look like

For any locale where you're shipping plural-sensitive strings, the minimum test matrix should cover the following values of n: 0, 1, 2, 3, 4, 5, 10, 11, 12, 13, 20, 21, 22, 100, 101, 102, 111, 112, 1000, 1001.

For each value, the test should assert the correct CLDR category for that locale and verify that the rendered string uses the corresponding translation. This is mechanical enough to automate: the CLDR data is available as a structured JSON dataset (the cldr-core package), and a test can derive the expected category for any (locale, n) pair programmatically rather than hand-specifying expected outputs.

A localization team at a mid-size e-commerce company discovered their Arabic plural forms were wrong during a localized product review in late 2024. The product had shipped Arabic support six months earlier. The string "عندك {count} رسالة" ("You have {count} message/s") was displaying the other form for all counts above 2. The fix was straightforward once the bug was diagnosed — add the missing four ICU forms and populate the translations — but the root cause was that no one had run a CLDR-matrix test during QA. The test suite verified "1 message" and "5 messages" and considered plural handling validated.

Fractional numbers and v operand edge cases

One more failure mode that surfaces in financial and scientific applications: fractional numbers. CLDR plural rules apply to non-integer values too, and the rules differ from integer rules in some locales.

In English, "1.0 items" takes the other form because v > 0 (there's at least one visible fraction digit). "1 item" takes one because v = 0. Most i18n libraries handle this correctly when given a formatted number string that carries decimal information. Where it breaks is when application code rounds a float to an integer before passing it to the translation function: t('items', { count: Math.round(1.7) }) passes count: 2, not 1.7, and the plural form is correct by accident. Pass the raw float for accurate plural selection when the visible format includes decimals.

This matters practically in financial UIs where "1.5 transactions" or "0.5 BTC" appear in user-facing text. French uses one for 0 and 1 but also for fractional values between 0 and 2 — so "1.5 transaction" in French takes the singular form. An app that passes 1.5 rounded to 2 would use the plural form and produce a subtle grammatical error.

Translation delivery and the plural form gap

Even when engineers implement ICU plural handling correctly, the translation delivery pipeline often introduces gaps. A translator working in a CAT tool that only presents two string slots ("singular" and "plural") for an Arabic string has no mechanism to provide the other four forms. The tool either drops them silently or inserts the other value as a fallback. The developer receives what appears to be a complete translation file; the missing forms only surface at runtime when a user triggers a count in the 3–10 range.

The right verification step is a post-export CLDR completeness check: for each locale that uses N plural forms per CLDR spec, verify that every plural-sensitive string in the file provides exactly N translated forms. This check doesn't require running the app; it's a structural assertion on the resource file that can run in CI before the file is merged. It catches the CAT-tool truncation problem and the "translator provided two forms for a six-form language" problem at the point where it's cheap to fix rather than after the locale has shipped to production.