Angular is quite a big framework with plenty of subsidiary packages. It is one of the most commonly chosen options when it comes to web application development. In the rest of this article, we will show you how to internationalize (i18n) your Angular application with the officially recommended package, the @angular/localize
.
The @angular/localize
package is available as of Angular 9. Since then, it has evolved, and nowadays, it tends to become the de facto standard in the context of Angular internationalization.
In the following chapters, you will find out how to mark content for localization, extract marked content, translate, and finally, merge everything back.
All code samples used in this article are available on the GitHub repo.
Let’s begin by creating a new Angular project using the Angular CLI tool.
For the sake of simplicity, select not to use Angular routing and choose CSS as a stylesheet format during project skeleton generation.
ng new angular-i18n-example
Would you like to add Angular routing?
No Which stylesheet format would you like to use? CSS
Note: We are using the most recent version of the Angular framework (12.0.1) at the time of writing this.
To use Angular’s localization features, you will need to add the @angular/localize
package to the project. This command will update the project’s package.json
and polyfills.ts
files and load $localize
onto the global scope.
cd angular-i18n-example
ng add @angular/localize
To be able to localize content, we first need to add some. So, let’s update the app component files.
Adding the i18n
attributes in the below-posted component template, we marked the content that needs to be localized. The i18n
is a custom attribute recognized by Angular. The value of this attribute is optional. If provided, it should match the following order: meaning|description@@id
. All these value parts are optional. The meaning represents the intent or context of the text message. It helps translators to translate different messages with the same meaning in the same way. The description holds additional information that the translator should be aware of to provide a more accurate translation. The |
character separates the meaning
and the description
. The @@id
represents the user-defined ID of the message.
Additionally, you can use the i18n
attribute with the <ng-container>
. That way, you do not need to add an extra DOM element to the page just for the sake of localization.
A more advanced case would be the localization of element attributes. For that purpose, you can use an i18n-<attribute>
attributes. In that case, the <attribute>
should represent the name of the attribute you want to localize (e.g. i18n-title
for the title
attribute).
Note: The posted example contains messages with the ICU Plural and ICU Select message syntax. If you are unfamiliar with ICU syntax, please check this post for more details.
The app.component.html
file:
<div>
<p i18n="Simple message example@@message.simple">A simple message.</p>
<p i18n="Argument message example@@message.argument">Hi, {{ name }}! 👋</p>
<p i18n="Plural message example@@message.plural">{count, plural, one {{{count}} item} other {{{count}} items}}</p>
<p i18n="Gender message example@@message.gender">{gender, select, male {Mr {{ name }}} female {Mrs {{ name }}} other {Client {{ name }}}}</p>
<p i18n="Formatted number message example@@message.number-format">Formatted number: {{ amount | number: ".2" }}</p>
<p i18n="Formatted date message example@@message.date-format">Formatted date: {{ currentDate | date: "fullDate" }}</p>
<div>
<ng-container>{{ footerMessage }}</ng-container>
</div>
</div>
To display the above-defined template successfully, you will need to update the component file as well.
Besides required data, the app.component.ts
file also contains an example of localization within the component (check the footerMessage
). The direct call of the $localize
function helps us to achieve that. To pass the same optional params as in the template file (meaning, description, and @@id), add them wrapped with colons before the text message.
The app.component.ts
file:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
name: string = 'John';
count: number = 5;
gender: string = 'male';
amount: number = 7.5;
currentDate: number = Date.now();
company: string = 'Localizely';
footerMessage: string = $localize`:Component argument message example@@message.component-argument:Made with ❤️ by ${this.company}`;
}
To extract previously marked content, run the following CLI command from the project root.
ng extract-i18n --output-path src/locale
Note that it is good practice to keep all localization files in one place. For that purpose, we have used the --output-path
command option. If everything went well, you should have the messages.xlf
file under the src/locale
directory. This file represents the source language file that we will later use as a basis for other XLIFF (XML Localization Interchange File Format) files.
By default, Angular uses English in the United States (en-US
) as a source locale. To change this, you will need to update the project configuration (i18n
option within the angular.json
file). In our case, the default configuration is adequate.
Also, extract-i18n
supports extraction in different file formats. In this tutorial, we have used the default one, the xlf
. You can find more info regarding the extract-i18n
here.
Now that we have successfully extracted the source language file, we need to translate it into new languages. To achieve this, we are going to make a copy of the messages.xlf
file and rename it to messages.fr.xlf
. This new file is going to hold the translations for the French. Adding any new language in the future would follow the same procedure. Also, keeping the locale code in the file name is a good practice. Therefore, navigation through the project and future changes are a bit easier.
The messages.xlf
file:
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en-US" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="message.simple" datatype="html">
<source>A simple message.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">2</context>
</context-group>
<note priority="1" from="description">Simple message example</note>
</trans-unit>
<trans-unit id="message.argument" datatype="html">
<source>Hi, <x id="INTERPOLATION" equiv-text="{{ name }}"/>! 👋</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<note priority="1" from="description">Argument message example</note>
</trans-unit>
<trans-unit id="message.plural" datatype="html">
<source>{VAR_PLURAL, plural, one {<x id="INTERPOLATION"/> item} other {<x id="INTERPOLATION"/> items}}</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">6</context>
</context-group>
<note priority="1" from="description">Plural message example</note>
</trans-unit>
<trans-unit id="message.gender" datatype="html">
<source>{VAR_SELECT, select, male {Mr <x id="INTERPOLATION"/>} female {Mrs <x id="INTERPOLATION"/>} other {Client <x id="INTERPOLATION"/>}}</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
<note priority="1" from="description">Gender message example</note>
</trans-unit>
<trans-unit id="message.number-format" datatype="html">
<source>Formatted number: <x id="INTERPOLATION" equiv-text="{{ amount | number: ".2" }}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">10</context>
</context-group>
<note priority="1" from="description">Formatted number message example</note>
</trans-unit>
<trans-unit id="message.date-format" datatype="html">
<source>Formatted date: <x id="INTERPOLATION" equiv-text="{{ currentDate | date: "fullDate" }}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">12</context>
</context-group>
<note priority="1" from="description">Formatted date message example</note>
</trans-unit>
<trans-unit id="message.component-argument" datatype="html">
<source>Made with ❤️ by <x id="PH" equiv-text="this.company"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">16</context>
</context-group>
<note priority="1" from="description">Component argument message example</note>
</trans-unit>
</body>
</file>
</xliff>
Below, you can find the updated content of the messages.fr.xlf
file.
We’ve made the following changes:
file
element with target-language
attributetrans-unit
element with the target
element (translation)The messages.fr.xlf
file:
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en-US" target-language="fr" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="message.simple" datatype="html">
<source>A simple message.</source>
<target>Un message simple.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">2</context>
</context-group>
<note priority="1" from="description">Simple message example</note>
</trans-unit>
<trans-unit id="message.argument" datatype="html">
<source>Hi, <x id="INTERPOLATION" equiv-text="{{ name }}"/>! 👋</source>
<target>Salut, <x id="INTERPOLATION" equiv-text="{{ name }}"/>! 👋</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<note priority="1" from="description">Argument message example</note>
</trans-unit>
<trans-unit id="message.plural" datatype="html">
<source>{VAR_PLURAL, plural, one {<x id="INTERPOLATION"/> item} other {<x id="INTERPOLATION"/> items}}</source>
<target>{VAR_PLURAL, plural, one {<x id="INTERPOLATION"/> article} other {<x id="INTERPOLATION"/> articles}}</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">6</context>
</context-group>
<note priority="1" from="description">Plural message example</note>
</trans-unit>
<trans-unit id="message.gender" datatype="html">
<source>{VAR_SELECT, select, male {Mr <x id="INTERPOLATION"/>} female {Mrs <x id="INTERPOLATION"/>} other {Client <x id="INTERPOLATION"/>}}</source>
<target>{VAR_SELECT, select, male {Monsieur <x id="INTERPOLATION"/>} female {Mme <x id="INTERPOLATION"/>} other {Cliente <x id="INTERPOLATION"/>}}</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
<note priority="1" from="description">Gender message example</note>
</trans-unit>
<trans-unit id="message.number-format" datatype="html">
<source>Formatted number: <x id="INTERPOLATION" equiv-text="{{ amount | number: ".2" }}"/></source>
<target>Numéro formaté: <x id="INTERPOLATION" equiv-text="{{ amount | number: ".2" }}"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">10</context>
</context-group>
<note priority="1" from="description">Formatted number message example</note>
</trans-unit>
<trans-unit id="message.date-format" datatype="html">
<source>Formatted date: <x id="INTERPOLATION" equiv-text="{{ currentDate | date: "fullDate" }}"/></source>
<target>Date formatée : <x id="INTERPOLATION" equiv-text="{{ currentDate | date: "fullDate" }}"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">12</context>
</context-group>
<note priority="1" from="description">Formatted date message example</note>
</trans-unit>
<trans-unit id="message.component-argument" datatype="html">
<source>Made with ❤️ by <x id="PH" equiv-text="this.company"/></source>
<target>Fabriqué avec ❤️ par <x id="PH" equiv-text="this.company"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">16</context>
</context-group>
<note priority="1" from="description">Component argument message example</note>
</trans-unit>
</body>
</file>
</xliff>
Once we’ve translated all content from newly created XLIFF files, we should merge everything back into the app. In this example, we use an ahead-of-time (AOT) compilation that converts Angular HTML and TypeScript code into efficient JavaScript code. As of Angular 9, AOT is the default option for both development and production builds. To build a separate distributable copy of the app for each locale, we will need to update the project configuration.
Setting the i18n
project option, we are defining the locales of the project. In other words, we are specifying the locale of messages in the source code (sourceLocale
) and other locales that we created by translating the initial XLIFF file (locales
). The rest of the configuration belongs to the build and serve phases, which will help us to run the localized app.
The angular.json
file:
{
...
"projects": {
"angular-i18n-example": {
...
"i18n": {
"sourceLocale": "en-US",
"locales": {
"fr": "src/locale/messages.fr.xlf"
}
},
"architect": {
"build": {
...
"options": {
"localize": true,
"aot": true
},
"configurations": {
...
"en-US": {
"localize": ["en-US"]
},
"fr": {
"localize": ["fr"]
}
},
...
},
"serve": {
...
"configurations": {
...
"en-US": {
"browserTarget": "angular-i18n-example:build:en-US"
},
"fr": {
"browserTarget": "angular-i18n-example:build:fr"
}
},
...
},
...
}
...
}
...
}
...
}
Due to the deployment complexities and the need to minimize rebuild time, the development server only allows running one locale at a time. To run the project in development mode, replace <LOCALE>
with one of the available values: en-US
or fr
.
ng serve --configuration=<LOCALE> --open
Depending on the needs, you can handle language selection differently. One way would be to configure a server to redirect all HTTP requests according to the value of the Accept-Language
HTTP header. Of course, such handling would require fallback logic (e.g. missing header, request for not-supported language, etc.). Another one would be to delegate the language selection to users and to configure the server to always return the app for the default language during the initial request. The third way would be a mixture of the previous two.
Accompanying above mentioned ways of handling language selection, below you can find a simple example that would let users change the language after the initial load.
The app.component.html
file:
<div class="lang-picker">
<div *ngFor="let locale of locales">
<a href="/{{ locale.localeCode }}/">
{{ locale.label }}
</a>
</div>
</div>
...
The app.component.ts
file:
import { Component } from '@angular/core';
interface Locale {
localeCode: string;
label: string;
}
...
export class AppComponent {
locales: Locale[] = [
{ localeCode: 'en-US', label: 'English' },
{ localeCode: 'fr', label: 'Français' },
];
...
}
Note: Presented language selection won’t work in development mode due to development server constraints.
At the time of writing this, there was no official recommendation regarding the RTL handling. However, there are several ways to solve this. Essentially, this is just an update of the dir
attribute according to the selected language. To support that, you will need to add few lines to the app component. In the first place, you will need to inject an app locale into the AppComponent
constructor. Second, you will need to wrap all your content with the div
element and set the dir
attribute to correspond to that locale.
The app.component.ts
file:
import { Component, Inject, LOCALE_ID } from '@angular/core';
...
export class AppComponent {
constructor(@Inject(LOCALE_ID) public locale: string) {}
...
}
The app.component.html
file:
<div [attr.dir]="locale === 'ar' ? 'rtl' : 'ltr'">
...
<div>
In case the presented RTL handling does not suit you, the same problem can be solved using the i18n-dir
attribute.
In this post, we took a look at the @angular/localize
package and showed how to internationalize Angular apps with it. In most cases, the most demanding part of this task is the translation of XLIFF files. The XLIFF files contain a lot of data, and therefore can be difficult for translators to understand. Also, since they are basically XML files, there is a high probability that manual editing could easily lead to new problems (e.g. invalid file syntax). The use of professional tools is the solution to these problems. The Localizely platform supports plenty of localization formats, including XLIFF. Moreover, it allows you easily upload and download translation files, collaborates with other team members, setup Over-the-air translation updates with S3 integration, and much more.
Like this article? Share it!
Zoran is a Software Engineer at Localizely. His primary interest is web development, but he also has a solid background in other technologies. For the last few years, he has been involved in the development of software localization tools.
Subscribe to the Localizely blog newsletter for quality product content in your inbox.
Related