June 01, 2021coding

Angular internationalization (i18n) tutorial

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.

Create new Angular project

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.

Add the localize package

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

Adjust the content for localization

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}`;
}

Extract marked content for localization

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.

Translate XLIFF files

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: &quot;.2&quot; }}"/></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: &quot;fullDate&quot; }}"/></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:

  • Extended file element with target-language attribute
  • Extended each trans-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: &quot;.2&quot; }}"/></source>
        <target>Numéro formaté: <x id="INTERPOLATION" equiv-text="{{ amount | number: &quot;.2&quot; }}"/></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: &quot;fullDate&quot; }}"/></source>
        <target>Date formatée : <x id="INTERPOLATION" equiv-text="{{ currentDate | date: &quot;fullDate&quot; }}"/></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>

Merge translations into the app

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

Add language switcher

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.

Support RTL languages

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.

Conclusion

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, and much more.

Try Localizely for free.

Enjoying the read?

Subscribe to the Localizely blog newsletter for quality product content in your inbox.

Related

Flutter localization: step-by-step
March 29, 2022
In “Coding
Unicode
December 02, 2021
In “Coding
Copyrights 2022 © Localizely