skip to content

In an Angular template, how does I18nPluralPipe choose the message for an invoice's item count, and what does # do in the mapping?

level: middleimportance: nice to knowfreq 22%

answer

  1. a map of message keys
  2. exact match beats category
  3. other as the safety net
  4. hash is replaced by the value

basics

~10 s

I18nPluralPipe 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 lines
ts
import { 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

for a junior

Recall the map format, the '=n' exact keys, the 'other' fallback and that # is replaced by the number.

for a middle

Explain the lookup order, exact key then locale category then other, and what happens when nothing matches.

for a senior

Judge when runtime pluralisation with the pipe is enough and when messages belong in the translated i18n pipeline instead.

for a principal

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