← Back to Blog

Fix Flutter Plurals Silently Wrong in Polish & Russian

flutteri18nlocalizationpluralizationicudart

Fix Flutter Plurals Silently Wrong in Polish & Russian

You shipped to Poland. A user opens the cart, it says "2 produktów", and a native speaker winces. It should say "2 produkty." Your build was green. flutter gen-l10n printed nothing. No test failed. And yet the string is grammatically broken for a huge slice of your counts.

This is the bug that never throws. Flutter's gen-l10n will compile an ICU plural that contains only one and other, because those two categories are all English needs. Polish and Russian need four — one, few, many, other — and when the few/many branches are missing, the intl runtime silently falls back to other. No error at build time, no error at runtime. Just wrong grammar in production.

Let's look at a real ARB that looks correct, the strings it actually renders, and the exact fix.

The ARB that compiles fine and is still wrong

Here is a Polish ARB file for a shopping cart. Nothing about it will make gen-l10n complain.

{
  "@@locale": "pl",
  "cartItems": "{count, plural, one{{count} produkt} other{{count} produktów}}",
  "@cartItems": {
    "placeholders": {
      "count": { "type": "int" }
    }
  }
}

Run flutter gen-l10n, get your AppLocalizations, ship it. The generated Dart calls Intl.plural(count, one: ..., other: ..., locale: 'pl'). Because you never passed a few or many argument, Intl.plural computes the CLDR category for the count, finds no matching branch, and returns other.

Why 5 is right but 2 and 22 are wrong

Here's the cruel part: some counts render correctly by accident, which is exactly why you can't catch this by eyeballing the JSON or spot-checking a couple of values.

Polish CLDR cardinal rules (from the Unicode CLDR chart):

  • one — exactly 1
  • few — numbers ending in 2–4, except 12–14 → 2, 3, 4, 22, 23, 24, 102
  • many — most other integers → 0, 5–21, 25, 111 …
  • other — fractions (1.5)

The other string above uses the genitive-plural form "produktów," which happens to be the correct many form. So every count that is genuinely many looks fine, and every count that should be few is broken:

count Buggy one/other output Correct CLDR output Category
1 1 produkt ✅ 1 produkt one
2 2 produktów ❌ 2 produkty few
4 4 produktów ❌ 4 produkty few
5 5 produktów ✅ 5 produktów many
22 22 produktów ❌ 22 produkty few
25 25 produktów ✅ 25 produktów many
102 102 produktów ❌ 102 produkty few

If your QA happened to test with 1 and 5, everything passed. The bug lives in 2, 3, 4, 22, 23, 24, 102 — counts nobody thinks to check. That is why flutter plural wrong count bugs slip through review: the JSON is syntactically valid and a fraction of the outputs are correct.

Russian breaks the same way — on different numbers

Russian has the same four categories but different membership, so a one/other-only file fails on a different set of counts. CLDR Russian cardinal rules:

  • one — ends in 1, except 11 → 1, 21, 101
  • few — ends in 2–4, except 12–14 → 2, 3, 4, 22, 102
  • many — ends in 0 or 5–9, or 11–14 → 5, 11, 25, 100
  • other — fractions

Note 21 and 101 are one ("21 файл"), not plural — something an English-shaped one (=1 only in spirit) never anticipates. A minimal-but-correct Russian file:

{
  "@@locale": "ru",
  "cartItems": "{count, plural, one{{count} файл} few{{count} файла} many{{count} файлов} other{{count} файла}}",
  "@cartItems": {
    "placeholders": {
      "count": { "type": "int" }
    }
  }
}

With only one/other, Russian renders "2 файлов" (should be "файла"), "22 файлов" (should be "файла"), and "102 файлов" (should be "файла") — while 5, 25 and 100 sail through because other was written as the many form.

The fix: declare every category the language needs

The correct Polish ARB spells out all four ICU branches. Each form is a different Polish word ending, not a cosmetic tweak:

{
  "@@locale": "pl",
  "cartItems": "{count, plural, one{{count} produkt} few{{count} produkty} many{{count} produktów} other{{count} produktu}}",
  "@cartItems": {
    "description": "Number of products in the cart",
    "placeholders": {
      "count": { "type": "int" }
    }
  }
}
  • one → "1 produkt"
  • few → "2 produkty", "22 produkty", "102 produkty"
  • many → "5 produktów", "25 produktów"
  • other → "1,5 produktu" (the fractional/genitive-singular form)

You call it exactly as before — the generated method signature doesn't change:

Text(AppLocalizations.of(context)!.cartItems(count));

The intl runtime now has a real branch for every category and stops falling back to other. Your English app_en.arb still uses just one/other — that's correct for English. Each locale carries only the categories its own grammar requires, which is the whole point people miss: the plural shape is per-language, not per-key.

Why you can't lint this by reading JSON

Missing few/many is invisible to the naked eye for three reasons:

  1. It's valid ICU. gen-l10n only requires other; everything else is optional. Drop other and you get a hard error — drop few and you get silence.
  2. gen-l10n doesn't know your target grammar. The tool validates syntax, not linguistic completeness. It has no idea that @@locale: pl obligates you to supply few and many.
  3. Coincidental correctness. Because other often doubles as the many form, a majority of small test counts look fine, masking the broken few band.

The only reliable catch is a validator that knows the CLDR category set for each locale and flags any plural key missing a category that language actually uses. That is exactly what FlutterLocalisation's ICU plural-syntax validation does — it inspects each app_<locale>.arb, maps the locale to its required CLDR categories, and surfaces a dropped few/many for Polish, Russian, Arabic and other multi-form languages before it ships. You edit the strings in the ARB editor instead of hand-writing raw ICU and hoping you covered every branch.

Checklist for Slavic (and beyond)

  • For flutter polish plural and flutter russian pluralization, always author one, few, many, and other.
  • Test counts that hit each band: 1, 2, 5, 21, 22, 25, 102 covers one/few/many boundaries for both languages.
  • Remember 21 and 101 are one in Russian but many/few elsewhere — don't assume "ends in 1" means singular across all Slavic locales.
  • Keep English lean (one/other); don't copy its plural shape into other locales.

Getting icu plural few many flutter right is the difference between an app that reads natively and one that quietly announces it was localized by a machine. If you want the missing-category check done automatically across every locale you ship, try FlutterLocalisation free — it catches the dropped few/many that gen-l10n never will.