In a Flutter grocery app using gen-l10n, how do you write an .arb plural for cart item counts correct in English, German and Polish?
answer
- ICU plural inside the message
- other is mandatory
- =0 =1 =2 plus CLDR categories
- Polish needs one, few and many
- generated code calls Intl.pluralLogic
basics
~20 sWrite an ICU plural such as {count, plural, =0{...} =1{...} other{...}} in each ARB file, giving every language the CLDR categories it needs: one and other for English and German, one, few and many for Polish, always with other.
solid answer
~40 sThe template `app_en.arb` defines `"cartItems": "{count, plural, =0{Your cart is empty} =1{1 item} other{{count} items}}"`; gen-l10n infers `count` as `num` (or you declare `int`) and generates `String cartItems(num count)`. Each translation writes its own plural with the categories its language uses: German needs `one` and `other`, Polish needs `one` (1), `few` (2-4, 22-24, but not 12-14), `many` (0, 5-21, 25...) and `other` for fractions. `other` is mandatory in every plural, and the only exact selectors are `=0`, `=1` and `=2`. The generated code calls `Intl.pluralLogic`, which returns an exact `=0`/`=1`/`=2` match first and otherwise applies the locale's CLDR rule, falling back to `other` when a category is missing, so a Polish file with only `one` and `other` renders the wrong form for 2, 3 and 4.
code
json · 10 lines{
"@@locale": "en",
"cartItems": "{count, plural, =0{Your cart is empty} =1{1 item} other{{count} items}}",
"@cartItems": {
"description": "Number of products in the shopping cart",
"placeholders": {
"count": {"type": "int"}
}
}
}go deeper
Recognise the ICU plural shape inside an ARB value and remember that other is always required.
Explain how gen-l10n turns the plural into an Intl.pluralLogic call, why =0/=1/=2 are the only exact selectors, and which CLDR categories English, German and Polish use.
Catch plurals copied between languages in review, add translator descriptions, and test the edge counts such as 2, 5, 12 and 22 in each shipped locale.
Decide how translation vendors are briefed on plural categories and whether per-locale category coverage is checked automatically before release.
## ICU plural syntax in an ARB file An **ARB** (App Resource Bundle) file is JSON: each key is a message id, each value the message text, and an optional `@key` entry holds metadata. A message can embed an **ICU MessageFormat** plural expression: `{count, plural, =0{...} =1{...} one{...} few{...} many{...} other{...}}` - `count` is the **placeholder** that selects the branch. If the `@cartItems` metadata gives it no type, gen-l10n infers `num`; you may declare `int`, and any other type is rejected with *"Placeholders used in plurals must be of type 'num' or 'int'"*. - Inside a branch, `{count}` prints the number (formatted, if the placeholder declares a `format`). - **`other` is mandatory.** A plural without it fails with *"ICU Syntax Error: Plural expressions must have an \"other\" case."* - The accepted selectors are exactly **`=0`, `=1`, `=2`, `zero`, `one`, `two`, `few`, `many`, `other`**. `=5{...}` is rejected. ## What the generated code does For a plural message gen-l10n generates a method (`String cartItems(num count)`) whose body calls **`Intl.pluralLogic(count, locale: localeName, zero: ..., one: ..., few: ..., many: ..., other: ...)`** from `package:intl`. The mapping has two details worth knowing: 1. `=0`, `=1` and `=2` map onto the same `zero`, `one` and `two` arguments as the category names. Giving both `=1{...}` and `one{...}` in one message means one of them is dropped, and the tool prints *"The plural part specified below is overridden by a later plural part."* 2. `Intl.pluralLogic` first returns an **exact match** when the count is exactly 0, 1 or 2 and that argument is present. Otherwise it asks the locale's **CLDR plural rule** for a category and returns that branch, falling back to `other` (and, for `two`, to `few`) when the branch is missing. ## Why each language needs its own branches Plural categories belong to the **language**, not to the template. The template's branches only define the method signature; each translation writes the branches its grammar needs. | Locale | Categories its CLDR rule returns for whole numbers | Example forms | |---|---|---| | English (`en`) | `one` for 1, `other` for the rest | 1 item, 5 items | | German (`de`) | `one` for 1, `other` for the rest | 1 Artikel, 5 Artikel | | Polish (`pl`) | `one` for 1; `few` when the last digit is 2-4 but the last two digits are not 12-14; `many` for 0, 5-21, 25-31 and so on | 1 produkt, 3 produkty, 5 produktów | Polish also returns `other` for fractional counts (1.5 kg), so a grocery app selling by weight should keep a sensible `other` branch too. ## A worked example for the grocery cart - `app_en.arb`: `{count, plural, =0{Your cart is empty} =1{1 item} other{{count} items}}` - `app_de.arb`: `{count, plural, =0{Dein Warenkorb ist leer} =1{1 Artikel} other{{count} Artikel}}` - `app_pl.arb`: `{count, plural, =0{Koszyk jest pusty} one{1 produkt} few{{count} produkty} many{{count} produktów} other{{count} produktu}}` Tracing Polish: `cartItems(0)` hits the exact `=0`; `cartItems(3)` gets `few` from the Polish rule; `cartItems(12)` gets `many` because 12-14 are excluded from `few`; `cartItems(22)` gets `few` again. ## Testing the edge counts Plural bugs hide at the boundaries, so a widget or unit test per shipped locale is cheap insurance. Instantiate the generated subclass directly and assert the strings for the counts that switch categories: 1. `0`, which should hit the exact `=0` branch; 2. `1`, the `one` form in all three languages; 3. `2`, `3` and `4`, the Polish `few` form; 4. `5`, `12` and `22`, which prove `many`, the 12-14 exception and the return to `few`. `AppLocalizationsPl().cartItems(22)` needs no widget tree, because each generated subclass is an ordinary class constructed with its locale name. ## Mistakes that ship - **Copying the English branches into Polish.** With only `one` and `other`, counts of 2-4 and 22-24 fall to `other`, which a translator usually wrote in the `many` form. gen-l10n does not warn, because it does not check categories per language. - **Building the sentence in Dart.** `'$count ${count == 1 ? l10n.item : l10n.items}'` hard-codes English grammar; the whole phrase must live inside the plural. - **Exact selectors beyond 2.** Only `=0`, `=1` and `=2` exist; use categories for everything else. - **Leaving out `other`.** The parser refuses the message; the generated `Intl.pluralLogic` call needs `other` as a required argument anyway. - **Typing the placeholder as `String`.** A plural placeholder must be `num` or `int`.
- Why does a Polish file with only one and other still pass gen-l10n but show wrong text for 3 items?gen-l10n validates syntax, not grammar: it only requires `other`. At runtime the Polish CLDR rule returns `few` for 3, `Intl.pluralLogic` finds no `few` branch and falls back to `other`, so the user sees whatever form the translator put there, typically the genitive plural meant for 5.
- How does =1 differ from one in a gen-l10n plural?Both land in the same `one` argument of `Intl.pluralLogic`, so only one survives, with a warning. The argument is returned for an exact count of 1 and also whenever the locale's CLDR rule says `one`; for English, German and Polish whole numbers the two coincide.
- Can the English template have fewer plural branches than the Polish translation?Yes. The template defines the method name and placeholders; each locale's ARB file writes its own branches, and the generated subclass for that locale uses them. English needs only `one` and `other`, while Polish adds `few` and `many`.
saying these in an interview costs you the question
- Every translation must use exactly the plural branches of the English template.
- gen-l10n warns when a Polish plural lacks a few branch.
- A plural only needs =0 and =1 branches; other is optional.
- Use =3 or =5 selectors for Polish counts instead of categories.
- Build the plural in Dart with count == 1 ? singular : plural.