skip to content

Marking Text for Translation

The i18n attribute, its i18n-<attr> variants and $localize tagged strings mark text, with ICU plural and select for variants. Interviewers ask how meaning, description and custom ids keep it stable.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Angular, how do you mark template text, an element attribute, and a string in component code for translation?

level: juniorimportance: must knowfreq 55%

answer

  1. three markers, one per location
  2. a compiler-recognized attribute, not a directive
  3. attribute text needs its own prefixed marker
  4. tagged template literal in TypeScript
  5. wrapper that leaves no element behind

basics

~20 s

Template text gets the i18n attribute on its element, an attribute value gets i18n-<attribute> (for example i18n-title), and strings in TypeScript use the $localize tagged template literal. <ng-container i18n> marks text without adding an element.

solid answer

~40 s

Angular has three markers. In a template, put `i18n` on the element whose text should be translated: `<h2 i18n>Your cart</h2>`. The `i18n` attribute covers the element's content only, so an attribute value such as a `title`, `placeholder` or `aria-label` needs its own `i18n-title`-style marker. In component code, wrap the string in the `$localize` tagged template literal: ``$localize`Item removed` ``. All three are read statically by Angular's build and extraction tooling; `i18n` is not a directive you import and it does not appear in the rendered DOM. When text has no element of its own, `<ng-container i18n>` marks it without adding a DOM element. The project needs the `@angular/localize` package, added with `ng add @angular/localize`.

code

ts · 24 lines
ts
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-cart-header',
  template: `
    <h2 i18n="Cart page heading">Your cart</h2>
    <div class="summary-row">
      <ng-container i18n>Subtotal</ng-container>
      <span class="amount">{{ subtotal() }}</span>
    </div>
    <button type="button" i18n i18n-title title="Remove every item from the cart" (click)="clear()">
      Empty cart
    </button>
    <p role="status">{{ status() }}</p>
  `,
})
export class CartHeader {
  readonly subtotal = signal('0.00');
  readonly status = signal('');
  clear(): void {
    this.subtotal.set('0.00');
    this.status.set($localize`:Status after emptying the cart:Your cart is now empty`);
  }
}

go deeper

for a junior

Name the three markers and where each goes: i18n on elements, i18n-title style markers on attributes, and the $localize tag in TypeScript.

for a middle

Explain that the compiler and extractor read these markers statically, which is why $localize must tag a literal, and when ng-container is the right wrapper.

for a senior

Show how you audit a codebase for unmarked strings, including text built in services, attributes like aria-label, and messages split into fragments.

for a principal

Discuss setting team conventions for marking, such as when to require descriptions, and how marking discipline affects translation cost and quality later.

## What "marking" means in Angular i18n Angular's built-in internationalization (i18n) works on **marked messages**. You mark every piece of user-visible source text; the extraction tool collects the marked messages into a translation file; translators fill in one copy per language; and the build (or a runtime loader) substitutes the translations. Marking is the only step that happens in your components, and Angular gives you three markers, one for each place text can live. The feature ships in the `@angular/localize` package. `ng add @angular/localize` installs it and adds `"@angular/localize"` to the TypeScript `types`, so `$localize` type-checks. If the package is missing and you build a localized version, the Angular CLI stops with an error explaining how to add it. ## Marker 1: the `i18n` attribute for element content Put `i18n` on the element whose text content should be translated: ```html <h2 i18n>Your cart</h2> <p i18n>Shipping is calculated at checkout.</p> ``` Key facts: - `i18n` is a **custom attribute recognized by the Angular compiler and tools**. It is not a directive, so nothing is imported into the component's `imports`, and it is not rendered to the DOM. - The whole content of the element becomes **one message**, including interpolations and nested elements, which become **placeholders** the translator can move around. - Its value can carry metadata (`meaning|description@@customId`) to help translators; the bare attribute is enough to mark the text. ## Marker 2: `i18n-<attribute>` for attribute values An element's attributes are separate messages. `i18n` on a `<button>` translates its label, not its `title`. Each translatable attribute gets its own marker named after it: ```html <button i18n i18n-title title="Remove this item from the cart">Remove</button> <input i18n-placeholder placeholder="Promo code" /> <img i18n-alt alt="Product photo" src="product.png" /> ``` The pattern is `i18n-{attribute_name}`, and it takes the same `meaning|description@@id` metadata as `i18n`. ## Marker 3: `$localize` for strings in TypeScript Text created in code, such as a toast, a page title set from a service, or a label passed to a child component, is marked with the **`$localize` tagged template literal**: ```ts this.toast.show($localize`Item removed from your cart`); const heading = $localize`:Cart page heading:Your cart`; ``` Points interviewers probe: 1. `$localize` is a **tag**, written directly before a backtick literal. The extractor reads the literal text at build time, so `$localize(someVariable)` or a string built elsewhere cannot be extracted. 2. Metadata goes between colons at the start: ``$localize`:meaning|description@@id:text` ``. 3. Expressions become placeholders; name them with `${count}:itemCount:` so translators see a readable name instead of `PH`. ## Marking text that has no element: `<ng-container>` Sometimes the text sits next to other nodes and you do not want a wrapper `<span>` to change layout or CSS selectors. `<ng-container i18n>` marks it without adding an element; Angular renders the container as an HTML comment, not a DOM element: ```html <div class="summary-row"> <ng-container i18n>Subtotal</ng-container> <span class="amount">{{ subtotal() }}</span> </div> ``` ## Choosing the marker | Where the text lives | Marker | Example | | --- | --- | --- | | Element content | `i18n` | `<h2 i18n>Your cart</h2>` | | Attribute value | `i18n-<attr>` | `<input i18n-placeholder placeholder="Promo code">` | | Text with no element | `<ng-container i18n>` | `<ng-container i18n>Subtotal</ng-container>` | | TypeScript code | `$localize` tag | ``$localize`Item removed` `` | ## Common mistakes - Assuming `i18n` on an element also covers its `title`, `alt` or `placeholder`. - Marking fragments of a sentence separately, so translators cannot reorder words. - Calling `$localize` like a function on a variable, which the extractor cannot see. - Wrapping text in extra `<span>` elements only to mark it, when `<ng-container>` exists. - Hardcoding user-visible strings in services and forgetting that they need `$localize` too. Once everything is marked, extraction and per-locale builds take over; those steps are separate concerns from marking.

  • Does the i18n attribute need to be imported into a standalone component's imports array?
    No. `i18n` is not a directive; the Angular compiler recognizes it while compiling the template and emits translated-message instructions instead. It never appears in the rendered DOM, so there is nothing to import. What the project does need is the `@angular/localize` package, which provides `$localize` and the extraction and merge tooling.
  • Why can the extractor not pick up $localize(message) where message is a variable?
    Extraction is static: it reads the literal text of each `$localize`-tagged template in the source. A variable's value only exists at run time, so there is no source text to extract, and a function-call form is not the tag syntax at all. Mark the literal where the text is written.
  • How do you give an interpolation inside marked template text a readable placeholder name?
    Add an `//i18n(ph="name")` comment inside the interpolation: `<p i18n>Hello, {{ username //i18n(ph="name") }}!</p>`. The template equivalent in code is ``$localize`Hello, ${username}:name:!` ``. Without a name, template placeholders default to `INTERPOLATION`, `INTERPOLATION_1` and so on.

Marking text is like a proofreader putting sticky tabs on every page that needs translating: the tab changes nothing on the page, but whoever collects the pages later knows exactly which ones to send out, and a tab on the paragraph does not cover the caption beside it.

saying these in an interview costs you the question

  • i18n is a directive you must add to the component's imports
  • i18n on an element also translates its title and placeholder
  • $localize can be called on a variable holding the text
  • You must wrap loose text in a span to translate it
  • Only template text needs marking; strings in services translate automatically
open as a page

In an Angular template, how do you use ICU plural and select expressions for a cart summary that varies by item count and shopper gender?

level: middleimportance: must knowfreq 48%

basics

~20 s

Inside i18n-marked text, write {count, plural, =0 {...} =1 {...} other {...}} for counts and {gender, select, female {...} male {...} other {...}} for string choices. Plural categories follow the locale's rules, other is the fallback, and clauses nest.

open as a page

In Angular's i18n metadata meaning|description@@customId, what does each part do, and which parts affect the message ID?

level: middleimportance: should knowfreq 38%

basics

~20 s

The description is context for translators and never changes the message ID. The meaning disambiguates identical text and is hashed into the generated ID with the text. A custom @@id replaces the generated ID entirely.

open as a page

A reviewer rejects Angular code that builds a cart message from two $localize fragments around a count; why is that wrong, and how should it be marked?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Fragments become separate translation units, so translators cannot see the whole sentence, reorder words or agree the noun with the count. Mark one whole message with a named placeholder, and put count-dependent wording in an ICU plural.

open as a page