skip to content

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?

level: middleimportance: should knowfreq 46%

answer

  1. ICU plural inside the message
  2. other is mandatory
  3. =0 =1 =2 plus CLDR categories
  4. Polish needs one, few and many
  5. generated code calls Intl.pluralLogic

basics

~20 s

Write 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 s

The 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
json
{
  "@@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

for a junior

Recognise the ICU plural shape inside an ARB value and remember that other is always required.

for a middle

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.

for a senior

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.

for a principal

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.