In an Angular template, how does I18nPluralPipe choose the message for an invoice's item count, and what does # do in the mapping?
answer
- a map of message keys
- exact match beats category
- other as the safety net
- hash is replaced by the value
basics
~10 sI18nPluralPipe looks for an exact '=n' key first, then the locale's plural category such as 'one', then 'other', and throws if none match. In the chosen message each # is replaced by the number.
solid answer
~30 s`{{ count | i18nPlural: itemsMap }}` takes a map such as `{ '=0': 'No items', '=1': 'One item', other: '# items' }`. The pipe first tries the exact key `'=' + value`, then the plural category the current locale assigns to the value (`one`, `few`, `many`, …), then `other`; with no match and no `other` it throws a runtime error. In the chosen message every `#` is replaced by the value as a plain string, so it is not locale-formatted. Exact keys handle special wording; category keys handle each language's grammar.
code
ts · 16 linesimport { Component, input } from '@angular/core';
import { I18nPluralPipe } from '@angular/common';
@Component({
selector: 'app-invoice-count',
imports: [I18nPluralPipe],
template: `<p>{{ lineCount() | i18nPlural: itemsMap }}</p>`,
})
export class InvoiceCount {
lineCount = input.required<number>();
itemsMap: { [k: string]: string } = {
'=0': 'No items',
'=1': 'One item',
other: '# items',
};
}go deeper
Recall the map format, the '=n' exact keys, the 'other' fallback and that # is replaced by the number.
Explain the lookup order, exact key then locale category then other, and what happens when nothing matches.
Judge when runtime pluralisation with the pipe is enough and when messages belong in the translated i18n pipeline instead.
Set a team rule for where pluralised strings live so translators, not template authors, own each language's grammar.
## The problem: "1 items" An invoice summary that says `1 items` or `0 item` looks careless, and the naive fix, a ternary on `count === 1`, only works for English. Other languages have more plural forms. Angular's **`I18nPluralPipe`** (template name `i18nPlural`, from `@angular/common`) picks the right message for a number using a mapping object, in a format modelled on ICU plural messages. ## Using it ```ts itemsMap: { [k: string]: string } = { '=0': 'No items', '=1': 'One item', other: '# items', }; ``` ```html <p>{{ invoice.lines.length | i18nPlural: itemsMap }}</p> ``` Results in an `en-US` app: - `0` renders `No items`; - `1` renders `One item`; - `7` renders `7 items`. ## How it chooses the message The pipe resolves a key in a fixed order: 1. **Exact match first.** It looks for the key `=<value>`, such as `=0` or `=1`. If present, that message wins. 2. **Locale plural category next.** It asks Angular's localization service for the value's **plural category** in the current locale (categories such as `zero`, `one`, `two`, `few`, `many`, `other`, depending on the language) and uses that key if the map has it. 3. **`other` as the fallback.** If neither matched, it uses `other`. 4. **Otherwise it throws.** A map with no matching key and no `other` makes the pipe throw a runtime error ("No plural message found for value"). ## What `#` does In the chosen message, every `#` is replaced by the **value** as a plain string. So `'# items'` becomes `7 items`. It is a straight substitution: the number is not locale-formatted, so `1234` stays `1234` rather than `1,234`. If you need grouping, format the number separately. ## Exact keys versus categories | Key | Matches | Language-aware | |---|---|---| | `'=0'`, `'=1'`, `'=5'` | exactly that number | no | | `'one'`, `'few'`, `'many'`, … | the locale's plural category for the number | yes | | `'other'` | anything not matched above | fallback | For English, `one` covers `1` and `other` covers everything else, so `{ one: '# item', other: '# items' }` is enough. Exact keys are useful for special wording such as "No items". ## Where it fits and where it does not - It is a good fit for **runtime pluralisation** of small UI strings when the app is not going through Angular's compile-time i18n, and for simple cases where the map lives in the component. - For a fully translated application, pluralisation is normally written as **ICU expressions** inside `i18n`-marked template text, so translators can supply each language's categories. The mechanics of that pipeline belong to Angular's i18n tooling rather than to this pipe. - The pipe has a sibling, **`I18nSelectPipe`** (`i18nSelect`), which maps a string key such as a status to a message. ## Testing it Because the lookup order is deterministic, a handful of values covers the behaviour: `0`, `1`, `2` and a larger number such as `21`, plus `null`, which renders an empty string. When the app supports another locale, add values that fall into that language's extra categories, since those are the cases English-speaking developers never exercise by accident. Keep the map as a component field so the same object is reused on every check. ## Common mistakes 1. Forgetting `other`, then seeing an exception for a count nobody tested. 2. Expecting `#` to be locale-formatted. 3. Writing `'1'` instead of `'=1'` and wondering why it never matches: without `=`, the key is treated as a category name, and `1` is not one. 4. Building the map inline in a large template; keeping it as a component field keeps the template readable and the object stable.
- Why does a map written as { '1': 'One item', other: '# items' } never use the first message?Without the `=` prefix the key is treated as a plural category name, and `1` is not a category. Exact matches must be written as `'=1'`; for English the category key `one` also works.
- Does # render 1234 as 1,234?No. The pipe substitutes the value's plain string form. Format the number separately, or build the message from a formatted value, if grouping separators matter.
It works like a sorting clerk with a checklist: first look for a folder labelled with the exact number, then one for the number's grammatical group, then the catch-all folder, and complain only if even that is missing.
saying these in an interview costs you the question
- Writes '1' instead of '=1' for an exact match
- Expects # to be locale-formatted with grouping separators
- Omits 'other' and assumes a blank fallback
- Believes every language has only singular and plural forms
- Thinks I18nPluralPipe reads messages from translation files