| name | fluent-syntax-selectors |
| description | Use when writing select expressions, plural forms, or variant-based translations in FTL. Prevents missing default variants and incorrect CLDR plural category usage. Covers cardinal and ordinal plurals, exact numeric matches, string selectors, and nested selectors. Keywords: select expression, CLDR plural categories, variants, default variant, cardinal, ordinal, gender selector, plural forms, 1 item vs 2 items, handle plurals, gender-specific translation.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Fluent Syntax 1.0. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
fluent-syntax-selectors
Quick Reference
Select Expression Anatomy
message-id =
{ $variable ->
[key1] Pattern for key1.
[key2] Pattern for key2.
*[default] Fallback pattern.
}
| Component | Syntax | Role |
|---|
| Selector | $variable -> | Runtime value to match against |
| Variant | [key] pattern | One possible translation |
| Default variant | *[key] pattern | MANDATORY fallback when no key matches |
| Variant key | [identifier] or [number] | String or numeric match target |
Selector Types
| Type | Selector Expression | Variant Keys | Example |
|---|
| Cardinal plural | $count -> | CLDR categories (one, other, ...) | [one] ... *[other] ... |
| Ordinal plural | NUMBER($pos, type: "ordinal") -> | CLDR ordinal categories | [one] {$pos}st *[other] {$pos}th |
| Exact numeric | $count -> | Number literals | [0] None. [1] One. *[other] Many. |
| Formatted number | NUMBER($val, minimumFractionDigits: 1) -> | Formatted literals | [0.0] Zero. *[other] ... |
| String | $gender -> | String identifiers | [masculine] ... *[other] ... |
| Nested | Multiple -> levels | Any combination | See examples |
CLDR Plural Categories by Language
| Language | Cardinal Categories | Example for one |
|---|
| English | one, other | 1 item |
| French | one, other | 0, 1 (0 and 1 are both one) |
| Arabic | zero, one, two, few, many, other | 1 |
| Polish | one, few, many, other | 1 |
| Japanese | other | (no plural distinction) |
| Russian | one, few, many, other | 1, 21, 31... |
All six possible CLDR categories: zero, one, two, few, many, other.
Critical Warnings
EVERY select expression MUST have exactly one *[default] variant. Omitting it is a syntax error. The grammar enforces this -- there is no workaround.
NEVER use ICU MessageFormat syntax {count, plural, one{...} other{...}} in FTL files. Fluent uses { $count -> [one] ... *[other] ... }.
ALWAYS include *[other] as the default variant for plural selectors. The other category exists in every language and is the safest default.
NEVER assume English plural categories apply to other languages. English uses only one and other. Polish uses one, few, many, other. Arabic uses all six categories.
ALWAYS remember that exact numeric matches ([0], [1]) are checked BEFORE CLDR category matches. Use exact matches to special-case specific values.
Decision Trees
Which Selector Type Do I Need?
Is the selector based on a number?
├── YES: Does it need ordinal suffixes (1st, 2nd, 3rd)?
│ ├── YES → Use NUMBER($var, type: "ordinal") selector
│ └── NO: Do you need special formatting (decimal places)?
│ ├── YES → Use NUMBER($var, minimumFractionDigits: N) selector
│ └── NO: Do you need exact value matches (0, 1, 42)?
│ ├── YES → Combine exact [N] keys with CLDR *[other]
│ └── NO → Use plain $var selector with CLDR categories
└── NO: Is it matching a string value?
└── YES → Use $var selector with string variant keys
How Many CLDR Categories Do I Need?
What language are you localizing for?
├── Japanese/Chinese/Korean → Only *[other] (no plural forms)
├── English/German/Dutch → [one] and *[other]
├── French/Portuguese (BR) → [one] and *[other] (note: 0 is "one" in French)
├── Polish/Czech/Slovak → [one], [few], [many], *[other]
├── Arabic → [zero], [one], [two], [few], [many], *[other]
└── Unsure → Check CLDR plural rules, ALWAYS include *[other]
Select Expression Patterns
Cardinal Plurals (Basic)
emails =
{ $count ->
[one] You have one unread email.
*[other] You have { $count } unread emails.
}
The runtime evaluates $count against CLDR cardinal plural rules for the current locale. For English: 1 matches [one], everything else matches *[other].
Cardinal Plurals (Multi-Category Language)
# Polish: one, few, many, other
emails =
{ $count ->
[one] Masz jedną nieprzeczytaną wiadomość.
[few] Masz { $count } nieprzeczytane wiadomości.
[many] Masz { $count } nieprzeczytanych wiadomości.
*[other] Masz { $count } nieprzeczytanych wiadomości.
}
Exact Numeric Matches with CLDR Fallback
items-in-cart =
{ $count ->
[0] Your cart is empty.
[1] You have one item in your cart.
*[other] You have { $count } items in your cart.
}
Exact matches [0] and [1] are checked FIRST. If $count is 0, the [0] variant wins even though 0 would match *[other] by CLDR rules. If $count is 5, no exact match exists so CLDR resolves to *[other].
Ordinal Plurals
your-rank = { NUMBER($pos, type: "ordinal") ->
[1] You finished first!
[one] You finished {$pos}st
[two] You finished {$pos}nd
[few] You finished {$pos}rd
*[other] You finished {$pos}th
}
The type: "ordinal" argument switches to ordinal CLDR categories. Exact match [1] is checked first. Then CLDR ordinal rules apply: in English, one = 1st/21st/31st, two = 2nd/22nd, few = 3rd/23rd, other = 4th-20th/24th-30th, etc.
String Selectors
# $gender is a string passed from the application
welcome =
{ $gender ->
[masculine] Welcome, he is here.
[feminine] Welcome, she is here.
*[other] Welcome, they are here.
}
String selectors match variant keys as literal string comparisons. The application passes $gender as "masculine", "feminine", or any other string. Non-matching values fall through to *[other].
Formatted Number Selectors
your-score =
{ NUMBER($score, minimumFractionDigits: 1) ->
[0.0] You scored zero points. What happened?
*[other] You scored { NUMBER($score, minimumFractionDigits: 1) } points.
}
The NUMBER function with formatting options controls how the value is rendered AND which variant it matches. The formatted representation 0.0 is matched against [0.0].
Nested Selectors
response =
{ $gender ->
[masculine] { $count ->
[one] He wrote one message.
*[other] He wrote { $count } messages.
}
[feminine] { $count ->
[one] She wrote one message.
*[other] She wrote { $count } messages.
}
*[other] { $count ->
[one] They wrote one message.
*[other] They wrote { $count } messages.
}
}
Nested selectors allow multi-dimensional variant selection. Each outer variant contains a complete inner select expression. Every inner expression MUST also have its own *[default] variant.
EBNF Grammar
SelectExpression ::= InlineExpression blank? "->" blank_inline? variant_list
variant_list ::= Variant* DefaultVariant Variant* line_end
Variant ::= line_end blank? VariantKey blank_inline? Pattern
DefaultVariant ::= line_end blank? "*" VariantKey blank_inline? Pattern
VariantKey ::= "[" blank? (NumberLiteral | Identifier) blank? "]"
Key rules enforced by this grammar:
variant_list contains exactly ONE DefaultVariant (the *-prefixed variant).
- The default variant can appear at ANY position in the list, not only last.
- Variant keys are either
NumberLiteral (for exact matches) or Identifier (for CLDR categories or string matching).
- A
NumberLiteral in a variant key can be negative: -1 is valid.
Common Mistakes
| Mistake | Why It Fails | Fix |
|---|
Missing * on default variant | Syntax error: grammar requires exactly one DefaultVariant | Add * before the fallback variant key |
Using ICU syntax {count, plural, ...} | Fluent does not support ICU MessageFormat | Rewrite as { $count -> [one] ... *[other] ... } |
| Using English categories for Polish | Polish needs few and many, not just one/other | Check CLDR plural rules for the target language |
Forgetting $ on variable in selector | Without $, it is a message reference, not a variable | ALWAYS prefix variables with $ |
Using [zero] for English 0 | English CLDR zero category does not exist; 0 maps to other | Use exact match [0] instead of [zero] |
Ordinal without type: "ordinal" | Cardinal rules apply by default; ordinal suffixes will be wrong | Use NUMBER($var, type: "ordinal") |
Reference Links
Official Sources