Angular i18n best practices
17.04.2025Battle-tested advice to level up internationalization of your ng application.
Heads-up: this article makes most sense if you do i18n the Angular way, i.e. the build-time translations as described in the docs. If you’re localizing in the runtime (for example, by loading JSON files), you’re welcome to read on and… think over the approach. I believe the built-in method is mature and guaranteed to be supported, which may not be the case for external libraries many years down the road.
The official guide covers every aspect of setup and support for multiple languages, and I’m not going to repeat after it. If anything, treat this piece as a supplement for the “Prepare component” page, because that’s what we’ll focus on: the concrete, sometimes edge, cases of authoring templates with i18n in mind.
1. Translatability
Every piece of text wrapped in i18n[-attribute]
or $localize
`` costs - in the size of generated .xlf files and in the money paid to the translators. Make sure you leave out unnecessary strings.
1.1. Proper names, technical phrases
Good: A worldwide tagline to be kept in original no matter the current language.
1
2
3
4
5
<header>
<h1>KFC</h1>
<h2>Finger lickin' good</h2>
</header>
<p i18n>Welcome to...</p>
Good: HTTP error page. Hard to justify translating universally recognized code number.
1
2
<h1>404</h1>
<p i18n>Page not found.</p>
1.2. Private views
Good: Admin features may be usually untranslated, even if they’re alongside public-facing code.
1
2
3
4
5
6
7
<h2 i18n>Order summary</h2>
<ul>
<li i18n>Double Classic Bucket</li>
</ul>
@if (isCashier) {
<p class="warning">Friendly remind the customer about product spiciness.</p>
}
2. Attribute placement
OK: Text is properly tagged and will be translated.
1
2
3
4
5
6
7
8
9
<app-modal-content type="info" i18n [scrollable]="true">Congrats! A free fudge shake will be added to your order.</app-modal-content>
<app-modal-footer
confirmLabel="Great!"
(confirm)="addShake()"
cancelLabel="I'm afraid I don't drink carbs"
(cancel)="removeShake()"
i18n-confirmLabel
i18n-cancelLabel
/>
Better: i18n
as the very last attribute in element, so one can see at a glance the text is tagged. i18n-*
attributes close to their counterparts.
1
2
3
4
5
6
7
8
9
<app-modal-content type="info" [scrollable]="true" i18n>Congrats! A free fudge shake will be added to your order.</app-modal-content>
<app-modal-footer
i18n-confirmLabel
confirmLabel="Great!"
(confirm)="addShake()"
i18n-cancelLabel
cancelLabel="I'm afraid I don't drink carbs"
(cancel)="removeShake()"
/>
3. Nesting
Bad: The following template will throw an error during i18n extraction because of nested markers.
1
2
3
4
<p class="tos" i18n>
Confirm that you have read and accepted
<a href="tos.pdf" i18n>Terms of Service</a>
</p>
Good: Distinct i18n elements, space added with &ngsp;
1
2
3
4
<p class="tos">
<ng-container i18n>Confirm that you have read and accepted</ng-container>&ngsp;
<a href="tos.pdf" i18n>Terms of Service</a>
</p>
4. White spaces and formatting
4.1. Leading and trailing characters
Aim to exclude spaces at the beginning and end of translated elements.
Bad: The following template…
1
2
3
<p class="delivery">
<span i18n>Deliver to: </span>{{ address }}<span i18n> (FREE)</span>
</p>
…will generate translate units like this…
1
2
<source>Deliver to: </source>
<source> (FREE)</source>
…so if the translator omit blank characters, you can end up with continuous, unlegible text (sorry for my Esperanto).
1
Liveru al:101 Wall St(SENPAGA)
Better: i18n elements contain bare content.
1
2
3
<p class="delivery">
<span i18n>Deliver to:</span> {{ address }} <span i18n>(FREE)</span>
</p>
4.2. New lines and indentation
Avoid line breaks inside tagged elements.
Problematic: The following template will generate 3 separate trans-units, even though they are exact same text.
1
2
3
4
5
6
7
8
9
<p i18n>
The order has been placed.
</p>
<div>
<p i18n>
The order has been placed.
</p>
</div>
<span i18n>The order has been placed.</span>
Better: The unambiguous, inlined elements that will surely fall into single translation id. No need to sacrifice structure thanks to <ng-container>
. Prettier ignore comment or similar can fight automatic formatting or wrapping, especially on long pieces of text.
1
2
3
4
5
6
7
8
<p>
<ng-container i18n>The order has been placed.</ng-container>
</p>
<div>
<!-- prettier-ignore -->
<p i18n>The order has been placed.</p>
</div>
<span i18n>The order has been placed.</span>
5. “Dirty” content
Try to exclude interpolations and HTML tags from tagged content.
Problematic: This will be marked for translation…
1
<p i18n>Hello {{ name }}!<br>What do you fancy today?</p>
…but the translator will be presented source full of placeholders to reckon.
1
<source>Hello <x id="INTERPOLATION" equiv-text="{{ name }}"/>!<x id="LINE_BREAK" ctype="lb" equiv-text="<br>"/>What do you fancy today?</source>
Better: After thoughtful changes, the resulting translation units will be much more manageable.
1
2
3
4
<p>
<ng-container i18n>Hello</ng-container> {{ name }}!<br>
<ng-container i18n>What do you fancy today?</ng-container>
</p>
6. Translation ids
Prefer auto-generated translation ids over custom ones, unless you’re sure you can track every single occurrence and change. More on this in the docs.
Problematic: Under the same custom id, there are two labels of different intended meaning. Can be caused by copy-paste or unwary update (only text, without looking at i18n). During extraction, the <source>
tag will be populated with first occurence of given id. Consequently, translation of this first occurence will be applied to all places of the custom id, losing the nuance. This is hard to spot because only foreign versions are affected. Thankfully, Angular already warns if there are disparate messages sharing the same id.
1
2
3
4
5
<h2 i18n>Select an order you'd like to repeat.</h2>
<app-order-history-list [orderHistory]="orderHistory()" />
<app-order-history-load-more [interval]="-1" i18n="@@prev">Previous week</app-order-history-load-more>
<app-order-history-load-more [interval]="1" i18n="@@next">Next week</app-order-history-load-more>
<button type="button" (click)="continue()" i18n="@@next">Continue</button>
Better: Remove custom ids and the labels will be recognized as different pieces of text.
1
2
3
4
5
<h2 i18n>Select an order you'd like to repeat.</h2>
<app-order-history-list [orderHistory]="orderHistory()" />
<app-order-history-load-more [interval]="-1" i18n>Previous week</app-order-history-load-more>
<app-order-history-load-more [interval]="1" i18n>Next week</app-order-history-load-more>
<button type="button" (click)="continue()" i18n>Continue</button>
Note: Sometimes you’ll find yourself the other way around - matching labels that may translate to different ideas. In such cases, custom ids will help you split the output in .xlf file. For example we can have:
- “Copy”, a marketing term;
- “Copy”, as you’d do with Ctrl+C.
1
2
3
4
5
6
7
8
<h3 i18n>Banner generator</h3>
<label for="copy" i18n="@@bannerCopy">Copy</label>
<input id="copy" type="text">
<label for="link" i18n>Link</label>
<input id="link" type="url">
<h3 i18n>Banner code</h3>
<pre>{{ bannerCode() }}</pre>
<button type="button" (click)="copyBannerCode()" i18n>Copy</button>
7. Conditional and ICU expressions
Don’t try to overload pluralization for anything other than numeral values.
Bad: Supposedly smart way to translate enum values, it can easily desynchronize as soon as enum is changed.
1
2
3
4
enum Poultry {
CHICKEN, // 0
TURKEY, // 1
}
1
<span i18n>{selectedPoultry, plural, =0 {Chicken} =1 {Turkey}} nuggets</span>
Better: Good old switch case. Alternatively, select
clause with string enums.
1
2
3
4
5
6
7
8
9
10
11
<span>
@switch (selectedPoultry) {
@case (poultry.CHICKEN) {
<ng-container i18n>Chicken</ng-container>
}
@case (poultry.TURKEY) {
<ng-container i18n>Turkey</ng-container>
}
}
&ngsp;<ng-container i18n>nuggets</ng-container>
</span>
8. Interpunction
Sometimes it can be beneficial to bring separators, bullets, etc. out of translated element.
Good: Normally we want as much of the sentence as possible tagged i18n.
1
<p i18n>Hey - are you a Frequent Eater Programme (FEP) member already?</p>
Good: In label-value scenarios, interpunction may be left behind.
1
2
3
<p class="payment">
<ng-container i18n>Payment method</ng-container>: {{ paymentMethod }}
</p>
9. Compound sentences
Ideally, a single i18n element comprises maximum amount of sentence or even paragraph - we then won’t have to deal with fragmented translations. However it can stand in conflict with the “dirtiness” of source when the content involves interpolations or formatting.
Challenging: An initial design contains a partially highlighted text.
1
2
3
<p>
<ng-container i18n>Join</ng-container> <strong class="highlight" i18n>Frequent Eater</p> <ng-container i18n>Programme today!</ng-container>
</p>
The thing is, in foreign language (let’s do Polish) this whole expression would have slightly different word order. After translation of these detached pieces and compilation we’d end up with HTML like so…
1
2
3
<p>
Dołącz do <strong class="highlight">Częstego Zjadacza</strong> Programu teraz!
</p>
…which doesn’t sound quite right - something like “Join Programme Eater Frequent today!”.
We can always just throw all the richness into one tagged element and offload the work to translators. Before that, consider:
- Changing the requirements - maybe the whole passage could be bold?
- Delegating complicated content to sort of CMS so it can be fetched from backend while maintaining support for localization;
- Simplifying the format, to give an idea:
1
2
// The label is translatable, a complete sentence, and the translator only needs to keep the bbcode-style tags intact
public label = $localize`Join [hilite]Frequent Eater[/hilite] Programme today!`;
1
2
<!-- A pipe to get the final HTML -->
<p [innerHTML]="label | kfcCode"></p>
10. Typos
Check your codebase periodically for any misrepresentation of i18n
, for example: i18
, 18n
, i18m
, i19n
, etc.
Bad: The following paragraph won’t be picked up for translation.
1
<p i18>Important note: ...</p>
Summary
- Sympathize with the people (or even machine) who work with your content. Are the translations fragmented, littered or lacking context? Look at the .xlf files and run the localized application.
- Communicate with stakeholders. Can this passage be changed for easier translation? Which parts can be left untranslated?
- Make it easy for your fellow developers. Tag clearly, discuss the rules, share the challenging cases.
PS: I provide services related to Angular i18n. See how I can help with your project.