Fix Wrong Arabic & Polish Plurals in Flutter ARB Files
Your Polish user sees "5 książki" in the app. It should be "5 książek". Your Arabic user sees "2 كتاب" where Arabic has a dedicated dual form, "كتابان". You check your Dart code, your gen-l10n setup, your l10n.yaml — everything compiles, everything runs. Because the bug isn't in your code.
It's in your ARB files. Your app_pl.arb or app_ar.arb only defines one and other, and Flutter's intl runtime silently falls back to other for every count that needed few or many. No compile error, no runtime warning — just grammatically broken text that native speakers notice instantly.
Why gen-l10n never warns you
In ICU plural syntax — the format ARB files use — only the other category is required. The official Flutter i18n docs are explicit: "Only the more general messageOther field is required." That rule is per message, per file, and it's language-agnostic.
So when a translator (or a machine-translation pass, or a copy-paste from your English template) produces this:
"bookCount": "{count, plural, one{{count} książka} other{{count} książki}}"
flutter gen-l10n accepts it happily. It has no opinion on whether Polish needs more categories. At runtime, Intl.plural evaluates the count against CLDR plural rules for the active locale, asks for the many category for count = 5 — and, finding no many branch in your message, falls back to other. Result: "5 książki". Grammatically wrong, and invisible to every English-speaking tester on your team.
This is the answer to the confused search queries this bug generates — flutter arabic plural not working, gen-l10n plural other only. Nothing is "not working". Everything is working exactly as specified. Your ARB is just incomplete.
The plural categories each language actually needs
CLDR defines six possible categories: zero, one, two, few, many, other. English uses two. The languages that generate the angriest reviews use more:
| Language | Categories | What they cover (integers) |
|---|---|---|
| English | one, other |
1 / everything else |
| Polish | one, few, many, other |
1 / 2–4, 22–24, 32–34… / 0, 5–21, 25–31… / fractions |
| Russian | one, few, many, other |
1, 21, 31… / 2–4, 22–24… / 0, 5–20, 25–30… / fractions |
| Ukrainian | one, few, many, other |
same shape as Russian |
| Arabic | all six | 0 / 1 / 2 / 3–10 / 11–99 / 100, 101… |
Two details that trip people up:
- Polish/Russian rules cycle with the tens. In Russian, 21 is
oneand 22 isfew. You cannot fake this with=1or count thresholds in Dart — you need the real categories in the ARB. - In Slavic languages,
otheris mostly for fractions (e.g. "1,5 książki"). If you only shipone/other, theotherstring does double duty for 2, 5, 100 and 1.5 — it can't be right for all of them. - Arabic
many(11–99) takes the singular noun in the accusative (e.g. "11 كتابًا"), which surprises developers who assume bigger number = more plural.
The authoritative reference is the CLDR language plural rules chart — bookmark it and check every locale you ship.
Copy-paste corrected ARB files
Here's a complete, correct plural message across the template and the two problem locales. First app_en.arb:
{
"@@locale": "en",
"bookCount": "{count, plural, one{{count} book} other{{count} books}}",
"@bookCount": {
"description": "How many books the user has",
"placeholders": {
"count": { "type": "num" }
}
}
}
A correct app_pl.arb — four categories, not two:
{
"@@locale": "pl",
"bookCount": "{count, plural, one{{count} książka} few{{count} książki} many{{count} książek} other{{count} książki}}"
}
Now 1 → "1 książka", 3 → "3 książki", 5 → "5 książek", 22 → "22 książki".
And a correct app_ar.arb — all six:
{
"@@locale": "ar",
"bookCount": "{count, plural, zero{لا كتب} one{كتاب واحد} two{كتابان} few{{count} كتب} many{{count} كتابًا} other{{count} كتاب}}"
}
Note what this buys you: 2 renders as the dual "كتابان" with no digit at all, 7 as "7 كتب", 15 as "15 كتابًا". With a one/other-only file, all of those collapse into whatever your translator put in other.
Russian and Ukrainian follow the Polish shape (one/few/many/other) with their own tens-cycling rules — the same fix applies to app_ru.arb and app_uk.arb.
Catch dropped categories before shipping, not after a 1-star review
The structural problem: translations for few and many get dropped silently — during machine translation, during a lazy copy of the English template, or when a TMS round-trips through a format that flattens plurals. You need an automated check. Here's a small Dart script that scans every ARB in your lib/l10n directory and fails if a plural message is missing a category its locale requires:
import 'dart:convert';
import 'dart:io';
const required = {
'en': {'one', 'other'},
'pl': {'one', 'few', 'many', 'other'},
'ru': {'one', 'few', 'many', 'other'},
'uk': {'one', 'few', 'many', 'other'},
'ar': {'zero', 'one', 'two', 'few', 'many', 'other'},
};
void main() {
var failed = false;
for (final file in Directory('lib/l10n')
.listSync()
.whereType<File>()
.where((f) => f.path.endsWith('.arb'))) {
final locale = RegExp(r'app_(\w+)\.arb').firstMatch(file.path)![1]!;
final needed = required[locale];
if (needed == null) continue;
final json = jsonDecode(file.readAsStringSync()) as Map<String, dynamic>;
json.forEach((key, value) {
if (key.startsWith('@') || value is! String) return;
if (!value.contains('plural,')) return;
final present = RegExp(r'(zero|one|two|few|many|other)\s*\{')
.allMatches(value)
.map((m) => m[1]!)
.toSet();
final missing = needed.difference(present);
if (missing.isNotEmpty) {
failed = true;
stderr.writeln('$locale/$key missing: ${missing.join(', ')}');
}
});
}
exit(failed ? 1 : 0);
}
Run it as dart run tool/check_plurals.dart locally or as a CI step. The regex-based category scan is deliberately simple; extend the required map with each locale you add, straight from the CLDR chart.
One caveat: =1{...} exact matches are not the same as one{...}. In Russian, =1 matches only 1, while one also matches 21, 31, 41… If your file uses =1 instead of one, the script above will (correctly) still flag one as missing.
Or let your translation workflow enforce it
A script catches the problem in CI; it's better still to catch it while editing. FlutterLocalisation is an ARB editor and translation-management platform built for exactly this workflow: you edit app_ar.arb and app_pl.arb in a UI instead of raw JSON, and its ICU plural-syntax validation flags any locale that's missing a plural category the language actually requires — a dropped few or many in Arabic, Polish, or Russian gets surfaced before it ever reaches a build, across every locale you manage. There's a free tier to start with, and more Flutter i18n deep-dives on the blog.
Try FlutterLocalisation free — paste in your existing ARB files and see which plural categories your locales are silently missing.