ICU and plural reference
The parser (icu.py)
A message is a list of nodes: Text, Pound (# in a plural), Arg(name, type, style) and Choice(name, kind, cases, offset). Choices nest arbitrarily. Text nodes remember their source offsets, which pseudo-localization uses to rewrite only literal text.
Accepted:
Hello {name}
{n, number} {n, number, integer} {n, number, ::currency/USD} {d, date, short} {t, time, ::hhmm}
{n, plural, offset:1 =0 {nobody} one {you and # other} other {you and # others}}
{g, select, female {She} male {He} other {They}}
{n, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}
{a, select, x {{n, plural, one {# thing} other {# things}}} other {none}}
Rejected, with the message the tool prints:
| Input | Error |
|---|---|
{name |
Unclosed '{' |
name} |
Unmatched '}' |
{} / {{name}} |
Expected argument name after '{' |
{n, bogus} |
Unknown argument type 'bogus' |
{n, plural, one {x}} |
plural argument 'n' is missing the required 'other' case |
{n, plural, many2 {x} other {y}} |
Invalid plural selector 'many2' |
{n, plural, other {x} other {y}} |
Duplicate selector 'other' |
{{name}} is the i18next placeholder syntax, not ICU: in a catalog that contains ICU plurals it is reported, which is the point of keeping i18next and ICU messages in different namespace files (as examples/webapp does).
Apostrophes
ICU4J's default (DOUBLE_OPTIONAL) mode: '' is a literal apostrophe; a single ' starts quoted text only when it directly precedes { or } (or # inside a plural), and the quote ends at the next single '; any other ' is literal.
Don't panic -> Don't panic (literal)
It''s {name}''s -> It's <name>'s
'{'literal'}' -> {literal}
{n, plural, other {'#' is a pound}} -> '#' quoted: no Pound node
Plural rules (plurals.py)
Rules are stored as compact strings in CLDR syntax with whitespace removed around operators; locales sharing a rule share an entry:
"pl": "one:i=1 and v=0;few:v=0 and i%10=2..4 and i%100!=12..14;"
"many:v=0 and i!=1 and i%10=0..1 or v=0 and i%10=5..9 or v=0 and i%100=12..14"
"en de nl sv it fi et ca gl sw ur": "one:i=1 and v=0"
"ja zh ko vi th id ms lo km my": "" # only 'other'
other is implicit and always last. Conditions are or of and of relations operand [% m] (=|!=) ranges where ranges are comma-separated values or lo..hi. Per CLDR, n = 2..4 matches integers only, so 2.5 is not in the range.
Operands
For a number written as a string:
| Operand | Meaning | "1.50" |
|---|---|---|
n |
absolute value | 1.50 |
i |
integer digits | 1 |
v |
visible fraction digits, with trailing zeros | 2 |
w |
visible fraction digits, without trailing zeros | 1 |
f |
visible fraction digits as integer | 50 |
t |
same, without trailing zeros | 5 |
Pass strings, not floats, when the trailing zero matters: "1.0" has v=1, so English picks other ("1.0 items") even though 1 is one.
Worked examples
| Locale | Number | Category | Why |
|---|---|---|---|
| en | 1 | one | i=1 and v=0 |
| en | 1.0 | other | v=1 |
| fr | 0 | one | i=0,1 |
| pl | 22 | few | i%10=2..4, i%100=22 not in 12..14 |
| pl | 12 | many | i%100=12..14 |
| pl | 1.5 | other | v=1 fails every v=0 clause |
| ru | 21 | one | i%10=1, i%100=21 is not 11 |
| ar | 103 | few | n%100=3 is in 3..10 |
| ar | 100 | other | n%100=0 hits no clause |
| ja | anything | other | no rules |
Ordinals use a separate table (en: one n%10=1 and n%100!=11, two, few; fr: one n=1; sv, it, ca, hi, hu...). de, es, ja, ru, pl and a few more are explicitly "other only"; locales absent from the table are not validated for selectordinal.
Where categories matter
- ICU: each
plural/selectordinalmust have the locale's categories (plural-incomplete), and should not have categories the locale never produces (plural-invalid, e.g.fewin a German message). Exact matches=0are ignored for this. - i18next, Rails, Android, stringsdict: same, per entry.
- gettext:
npluralscounts the categories whole numbers can reach. The tool derives this by evaluating the rules for 0..200, which gives pl=3 (one, few, many), ar=6, fr=2, ja=1, and mapsmsgstr[i]to the i-th category. A Polish entry with two forms therefore reportsmissing many.
lingocheck plurals LOCALE prints categories with sample numbers, and lingocheck icu MESSAGE -l LOCALE validates one message from the shell.
Known gap
CLDR 42 added a many category for es, fr, it, pt that triggers only for compact-decimal exponents (operand e, numbers like 1 million). These rules are not embedded, so a many form in those locales is reported as plural-invalid (warning) instead of being required.