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

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.