Angular and Lokalise logos

Angular i18n: internationalization & localization with examples

In this article you will learn with examples how to get started with Angular I18n using the built-in internationalization module. We will cover the following topics:

  • Setting up the Angular application and configuring the built-in localize module.
  • Performing simple translations and providing additional translation data.
  • Extracting translations to XLF files.
  • Working with pluralization and gender information.
  • Working with Angular pipes.
  • Performing translations within components.
  • Adding a language switcher.
  • Building the app and deploying to production.

For the purposes of this Angular i18n internationalization/localization tutorial, we’ll be using version 11 which is the most recent version at the time this post’s writing. The concepts I’m going to show you are applicable for versions starting from 9.1 and above.

The source code can be found at GitHub .

The working demo can be found at lokalise-angular-i18n.web.app/en-US .

You may be also interested in learning how to localize Angular applications with the Transloco solution or how to add internationalization support into Ionic apps .

Preparing for Angular I18n

So, let’s start by creating a new Angular application i18n-angular-demo , shown below:

ng new i18n-angular-demo

Do not enable Angular routing and use CSS for stylesheets. Next, just wait for a couple of minutes and then make sure the app is starting without any issues:

cd i18n-angular-demo
ng serve --open

The latter command should open the application in your browser. After you’ve made sure everything is working, stop the server and install a localize package:

ng add @angular/localize

This is a new package introduced in Angular 9 that will add internationalization support to the app.

Next, we’ll need to modify angular.json . I’ve pinpointed the relevant lines here:

{
  // ...
  "projects": {
    "i18n-angular-demo": {
      // ...
      "i18n": { // <----------------------------
        "sourceLocale": "en-US", // <---------------------------- 1
        "locales": { // <----------------------------
          "ru": "src/i18n/messages.ru.xlf" // <---------------------------- 2
        } // <----------------------------
      }, // <----------------------------
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
             "outputPath": "dist/", // <---------------------------- 3
             // ... 
          },
          "configurations": {
            // ...
            "ru": { // <----------------------------
              "localize": ["ru"] // <---------------------------- 4
            } // <----------------------------
          }
        },
        "serve": {
          // ...
          "configurations": {
            "production": {
              "browserTarget": "i18n-angular-demo:build:production"
            },
            "ru": { // <----------------------------
              "browserTarget": "i18n-angular-demo:build:ru" // <---------------------------- 5
            } // <----------------------------
          }
        }
        // ...
      }
    }
  },
  "defaultProject": "i18n-angular-demo"
}

So, we are doing the following:

  1. Setting the source language of the application to en-US .
  2. Adding support for the ru (Russian) locale. Translations for this language will live in the src/i18n/messages.ru.xlf file. Note that in the previous versions of Angular you would provide the --i18n-locale option when creating translations using command line interface, but now this option is deprecated.
  3. Storing the build directly under the dist/ folder.
  4. Adding the Russian configuration to the build options.
  5. Providing the ru configuration option for the serve command. This will set the language to Russian. Once this is in place, you’ll be able to run the application with the ng serve --configuration=ru command.

Now we can proceed to working with Angular internationalization examples!

Getting Started with Angular Internationalization

Marking the Text for Translation

In order to translate text, use the i18n attribute . Let’s see it in action by replacing src/app/app.component.html content with the following:

<h1 i18n>Hello world!</h1>

i18n is a special attribute that is recognized by the localize package. During the compilation it will be removed, and the tag content will be replaced with the proper translations.

This attribute may accept a translation description such as this:

<h1 i18n="Friendly welcoming message">Hello world!</h1>

Moreover, it is possible to provide the intended meaning of the translation. Just separate the meaning and description with a pipe | character like so:

<h1 i18n="main header|Friendly welcoming message">Hello world!</h1>

This additional data provides context for your translators. Specifically, you may also explain on what pages the translation will be displayed, what tone should be used and so on.

Creating Translation Files

Now, where and how do we store translations for Angular I18n? Usually they live in the src/i18n or src/locales folder. As for the translation format, there are three options which are listed here:

Let’s stick with the default option, but the next question is: how do we actually create translation files? Should we do that manually? No! There is a special command-line tool called xi18n which does the heavy lifting for us and extracts all the translations to a separate file:

ng xi18n --output-path src/i18n

Please note that for Angular 11+ it is advised to use ng extract-i18n command instead . It supports the same command line arguments.

Now in the src/i18n folder you will find a messages.xlf file. This is the base translation file with the following contents:

<?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="8f8c1e44db621d9a326a97396e69787284671553" datatype="html">
        <source>Hello world!</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.html</context>
          <context context-type="linenumber">9</context>
        </context-group>
        <note priority="1" from="description">Friendly welcoming message</note>
        <note priority="1" from="meaning">main header</note>
      </trans-unit>
    </body>
  </file>
</xliff>
  • trans-unit is the tag containing a single translation. id is a translation identifier and has a special meaning. xi18n generates the id for us so do not modify it here! We will discuss this attribute later in more detail.
  • source contains translation source text.
  • context-group specifies where exactly the given translation can be found.
  • context-type="sourcefile" shows the file where translation lives.
  • context-type="linenumber" shows the actual line of code.
  • Also, there are two note tags that provide the translation description and meaning respectfully.

Next, you can copy the messages.xlf file and name your copy messages.ru.xlf . This new file is going to store Russian translations. In order to translate something, add a target tag immediately after the source in the trans-unit as follows:

<?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="8f8c1e44db621d9a326a97396e69787284671553" datatype="html">
        <source>Hello world!</source>
        <target>Всем привет!</target> <!-- <----------------- your translation -->
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.html</context>
          <context context-type="linenumber">9</context>
        </context-group>
        <note priority="1" from="description">Friendly welcoming message</note>
        <note priority="1" from="meaning">main header</note>
      </trans-unit>
    </body>
  </file>
</xliff>

Also, it is a good idea to provide the target-language attribute for the file tag so that translation management systems can detect the locale properly, see below:

<?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" target-language="ru"> <!-- <--------- -->
    <!-- ... -->
  </file>
</xliff>

After performing these changes, start the Russian version of the app and make sure the translation is showing properly:

ng serve --configuration=ru --open

Translation Identifiers

I’ve already mentioned that translation IDs ( id attribute for the trans-unit tag) have special meanings. These IDs are unique and xi18n generates them based on the combination of the source text and its meaning. Therefore, whenever you update the translation source or its meaning, the ID will change. For instance, let’s modify our translation text:

<h1 i18n="main header|Friendly welcoming message">Hello everyone!</h1>

Now regenerate base translation file:

ng xi18n --output-path src/i18n

Now src/i18n/messages.xlf contains the following:

<?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="1e70beb33c2d9c1f2f6744a3a8dc0ad5ac472767" datatype="html">
        <source>Hello everyone!</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.html</context>
          <context context-type="linenumber">9</context>
        </context-group>
        <note priority="1" from="description">Friendly welcoming message</note>
        <note priority="1" from="meaning">main header</note>
      </trans-unit>
    </body>
  </file>
</xliff>

The ID has changed! Try starting the application again:

ng serve --configuration=ru --open

Now the welcoming message will not be translated into Russian because the translation ID has changed. To fix this, update the id attribute and the source tag in the src/i18n/messages.ru.xlf file like so:

<?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" target-language="ru">
    <body>
      <trans-unit id="1e70beb33c2d9c1f2f6744a3a8dc0ad5ac472767" datatype="html">
        <source>Hello everyone!</source>
        <target>Всем привет!</target>
        <!-- ... -->
      </trans-unit>
    </body>
  </file>
</xliff>

Note that if you have multiple occurrences of the same source text and meaning, all of them will have the same translation. This is due to the fact their identifiers are similar.

Custom Translation Identifiers

So, having auto-generated translation IDs is not very convenient because they depend on the source text and the meaning. However, it is possible to provide custom identifiers using the @@ prefix, for example:

<h1 i18n="main header|Friendly welcoming message@@welcome">Hello everyone!</h1>

Re-run the ng xi18n command and take a look at the base translation 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="welcome" datatype="html">
        <source>Hello everyone!</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.html</context>
          <context context-type="linenumber">9</context>
        </context-group>
        <!-- ... -->
      </trans-unit>
    </body>
  </file>
</xliff>

The ID is now set to welcome and it will not be changed even if you modify the source text. Don’t forget to provide a new ID in the messages.ru.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" target-language="ru">
    <body>
      <trans-unit id="welcome" datatype="html">
        <!-- ... -->
      </trans-unit>
    </body>
  </file>
</xliff>

Make sure to assign unique custom identifiers for different translations! If you provide the same IDs, only the first translation will be extracted:

<h1 i18n="main header|Friendly welcoming message@@welcome">Hello everyone!</h1>

<!-- "Other text" will be ignored. Translation for "Hello everyone!" will be used here instead: -->
<p i18n="@@welcome">Other text</p>

Angular Internationalization Use Cases

Translating Attributes

The Angular I18n module allows us to translate both tag content and attributes. Suppose there is a link to your portfolio in the src/app/app.component.html :

<p>
  <a i18n href="/en-US/portfolio" title="My Portfolio">Portfolio</a>
</p>

The link text will be translated properly, but what about the title and href ? To deal with these attributes, provide the i18n-ATTRIBUTE_NAME attributes in the following way:

<p>
  <a i18n i18n-href i18n-title href="/en-US/portfolio" title="My Portfolio">Portfolio</a>
</p>

Perform translation extraction as usual by running:

ng xi18n --output-path src/i18n

Copy new trans-unit tags from the src/i18n/messages.xlf file into src/i18n/messages.ru.xlf and provide target like so:

<?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" target-language="ru">
    <body>
      <!-- ... -->

      <trans-unit id="323af13292ecc714ab3848b9dff48f923893be32" datatype="html">
        <source>Portfolio</source>
        <target>Портфолио</target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.html</context>
          <context context-type="linenumber">12</context>
        </context-group>
      </trans-unit>
      <trans-unit id="18af8f7de89b3c1cebe305b0805a880a35d04eee" datatype="html">
        <source>/en-US/portfolio</source>
        <target>/ru/portfolio</target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.html</context>
          <context context-type="linenumber">12</context>
        </context-group>
      </trans-unit>
      <trans-unit id="9b154a4a7e2e3e6c0e2432761a52b7cf8b4c8395" datatype="html">
        <source>My Portfolio</source>
        <target>Моё портфолио</target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.html</context>
          <context context-type="linenumber">12</context>
        </context-group>
      </trans-unit>
    </body>
  </file>
</xliff>

We have translated the link text, the URL, and the title!

Performing Pluralization

So, we’ll continue our Angular localization tutorial by discussing pluralization . Imagine we would like to show how many ongoing tasks the user has. The built-in I18n module utilizes an ICU message format which may seem a bit complex at first:

<p i18n>{tasksCount, plural, zero {no tasks}
  one {{{tasksCount}} task}
  few {{{tasksCount}} tasks}
  many {{{tasksCount}} tasks}
  other {{{tasksCount}} tasks}}</p>
  • tasksCount is the variable that we’ll create in a moment.
  • plural is the name of an ICU expression
  • zero provides text when there are no tasks
  • one contains text when there is 1 task, or there are 101, 201, 301, etc. tasks.
  • few , many , and other cover all other cases. These are required for the Russian language which has some complex pluralization rules. If you require only English translations then having zero , one , and many categories is enough.

Don’t forget to create a new variable in the src/app/app.component.ts file like so:

// ...
export class AppComponent {
  tasksCount = 3
}

Extract the new translations and update the src/i18n/messages.ru.xlf file as follows:

<?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" target-language="ru">
    <body>
     <!-- ... -->

      <trans-unit id="2aaa6a848257ea436d32c801ac6d7d25a8059edb" datatype="html">
        <source>{VAR_PLURAL, plural, zero {no tasks} one {<x id="INTERPOLATION" equiv-text="{{tasksCount}}"/> task} few {<x id="INTERPOLATION" equiv-text="{{tasksCount}}"/> tasks} many {<x id="INTERPOLATION" equiv-text="{{tasksCount}}"/> tasks} other {<x id="INTERPOLATION" equiv-text="{{tasksCount}}"/> tasks} }</source>
        <target>{VAR_PLURAL, plural, zero {нет заданий} one {<x id="INTERPOLATION" equiv-text="{{tasksCount}}"/> задание} few {<x id="INTERPOLATION" equiv-text="{{tasksCount}}"/> задания} many {<x id="INTERPOLATION" equiv-text="{{tasksCount}}"/> заданий} other {<x id="INTERPOLATION" equiv-text="{{tasksCount}}"/> задания} }</target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.html</context>
          <context context-type="linenumber">15</context>
        </context-group>
      </trans-unit>
    </body>
  </file>
</xliff>

Choosing Translation with Select

Another useful ICU expression is select . It allows you to choose one of the translations based on a value. For example, it is very useful when working with gender information:

<span i18n>Gender: {genderCode, select, 0 {male} 1 {female} other {unknown}}</span>

Based on the value of the genderCode we will display either “male”, “female”, or “unknown”.

Now let’s add a genderCode variable and provide an ability to switch gender inside the src/app/app.components.ts file in the following way:

export class AppComponent {
  tasksCount = 201
  genderCode = 0

  male() { this.genderCode = 0; }
  female() { this.genderCode = 1; }
  other() { this.genderCode = 2; }
}

Next, extract translations and update src/i18n/messages.ru.xlf :

<?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" target-language="ru">
    <body>
      <!-- ... -->

      <trans-unit id="c1df5831cb8b3106de6bf8083aef223d528b2de8" datatype="html">
        <source>Gender: <x id="ICU" equiv-text="{genderCode, select, 0 {...} 1 {...} other {...}}"/></source>
        <target>Пол: <x id="ICU" equiv-text="{genderCode, select, 0 {...} 1 {...} other {...}}"/></target>

        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.html</context>
          <context context-type="linenumber">21</context>
        </context-group>
      </trans-unit>
      <trans-unit id="9740425f2ef8a0757676468e5ed8e7e1b7fa473c" datatype="html">
        <source>{VAR_SELECT, select, 0 {male} 1 {female} other {unknown} }</source>
        <target>{VAR_SELECT, select, 0 {мужчина} 1 {женщина} other {неизвестно} }</target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.html</context>
          <context context-type="linenumber">21</context>
        </context-group>
      </trans-unit>
    </body>
  </file>
</xliff>

Note that there are two translations: one for the “Gender:” part and another for the actual select expression.

Lastly, display three buttons to choose gender:

<button (click)="male()">♂</button>
<button (click)="female()">♀</button>
<button (click)="other()">⚧</button>

Once a button is clicked, the message with the gender info will be updated instantly.

Translation without a Tag

All the examples we’ve seen before required having some sort of tag. Sometimes, you may need to translate plain text without rendering any tag at all. It can be done with a ng-container , for instance:

<ng-container i18n>Copyright 2020</ng-container>

Upon page display, ng-container will be gone, and you’ll have simple plain text. Provide the corresponding Russian translation as usual:

<?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" target-language="ru">
    <body>
      <!-- ... -->

      <trans-unit id="f986261c453f5e38fc1c390b4d903b63765aed86" datatype="html">
        <source>Copyright 2020</source>
        <target>Права защищены 2020</target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/app.component.html</context>
          <context context-type="linenumber">27</context>
        </context-group>
      </trans-unit>
    </body>
  </file>
</xliff>

Using Angular Pipes

The built-in I18n module plays nicely with common pipes used for localization in Angular : DatePipe , CurrencyPipe , DecimalPipe , and PercentPipe . All you need to do is provide the proper locale data in the src/app/app.module.ts :

// ...

import { registerLocaleData } from '@angular/common';
import localeRu from '@angular/common/locales/ru';

registerLocaleData(localeRu, 'ru');

// ...

Now the locale data are loaded, and we can use the pipes mentioned above. For instance, let’s perform date localization:

<p>{{today | date:'fullDate'}}</p>

Create a new today variable in the src/app/app.component.ts file:

// ...

export class AppComponent {
  today: number = Date.now()
  // ...
}

Upon running the Russian version of the app, you will see the localized date!

Translations within Components

Sometimes you may need to perform translations within your components. This can be done by using the $localize() function directly. For example, add the following line to src/app/app.component.ts :

// ...
export class AppComponent {
  company = "Lokalise"
  created_by = $localize`Created by ${this.company}`
}

Use this variable in the src/app/app.component.html template:

<p>{{ created_by }}</p>

The problem is that this translation won’t be automatically extracted by the xi18n tool. Try to boot the server:

ng serve --configuration=ru --open

And you will see a warning message:

WARNING in No translation found for "3990133897753911565" ("Created by {$PH}").

This long number is a translation ID that the localize module tries to search for. PH means “placeholder” that we have to provide within the translation. Therefore, let’s use these data in the src/i18n/messages.xlf like so:

<?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="3990133897753911565" datatype="html">
        <source>Created by <x id="PH"/></source>
      </trans-unit>
    </body>
  </file>
</xliff>

Add Russian translation to the src/i18n/messages.ru.xlf . Don’t forget to provide placeholders, for example:

<?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" target-language="ru">
    <body>
      <!-- ... -->

      <trans-unit id="3990133897753911565" datatype="html">
        <source>Created by <x id="PH"/></source>
        <target>Создано <x id="PH"/></target>
      </trans-unit>
    </body>
  </file>
</xliff>

Now the created_by variable has the correct translation!

$localize accepts a custom ID, meaning, and description as well. The format is the same: meaning|description@@ID . Please note that this data should be wrapped using colons:

created_by = $localize`:used on the main page|explains who created the app@@created_by:Created by ${this.company}`

Managing Translation Files with Lokalise

As you can see, Angular I18n translation files have a pretty complex format. While developers won’t have any problems working with it, translators may be confused by the weird tags and attributes. Moreover, your translator could incidentally remove an important tag and break the app. We do not want that to happen, right?

Luckily, there is a translation management system here to save the day, and this system is called Lokalise. It allows for the management of translation files with a very convenient graphical interface, enables multiple developers to collaborate on a single project, provides integrations with developer tools and services like GitHub, Amazon S3, CLI, and a robust API . Plus more integrations with project management tools like Jira and 50+ others.

To get started with Lokalise, create a free trial account — no credit card is required!

It is possible to perform all manipulations with GUI, but I propose following the “developer’s way” and utilizing the command-line interface instead. Therefore, do the following:

  1. Log in to your account at lokalise.com .
  2. Navigate to Personal Profile > API Tokens.
  3. Generate a new read-write token and keep this tab opened.
  4. Download the latest release of CLIv2 .
  5. Unpack CLI somewhere on your PC and cd into the newly created directory.
  6. Create a new Lokalise project by running lokalise2 project create
    --name AngularI18n --token YOUR_API_TOKEN_HERE --languages
    "[{\"lang_iso\":\"ru\"},{\"lang_iso\":\"en-US\"}]" --base-lang-iso "en-US"
    .

Take note of the project ID that will be returned as a result (along with some other data).

Once the project is created, you can upload translation file as follows:

lokalise2 file upload --lang-iso ru --file "PATH_TO_PROJECT\src\i18n\messages.ru.xlf" --project-id PROJECT_ID --token TOKEN --detect-icu-plurals

Now you can invite translators to your new project!

Adding a Language Switcher

The last thing I would like to show you in this Angular internationalization tutorial is how to create a simple language switcher.

First, add a new localesList variable to the src/app/app.components.ts file in the following way:

// ...
export class AppComponent {
  localesList = [
    { code: 'en-US', label: 'English' },
    { code: 'ru', label: 'Русский' }
  ]

  // ...
}

Make sure to set the proper language codes.

Next, you just need to add an unordered list to the src/app/app.component.html :

<ul>
  <li *ngFor="let locale of localesList">
    <a href="/{{locale.code}}/">
      {{locale.label}}
    </a>
  </li>
</ul>

Deploying the App

So, we have finished building our Angular I18n example app, and now it is time to share our work with the world!

Preparing for Deployment

In this article I will show you how to deploy to Firebase . Therefore, you’ll need to create a new account there to get started.

Then, install Firebase tools:

npm install -g firebase-tools

Make sure to download the latest Russian translations from Lokalise:

lokalise2 file download --unzip-to "PATH_TO_PROJECT/src/i18n" --project-id PROJECT_ID --token TOKEN --format xlf --directory-prefix=/ --filter-langs=ru

Log in to Firebase locally using the following command:

firebase login

Finally, proceed to Firebase Console and create a new application. Make sure to give it a unique name.

Configuring the App

Once everything is ready, run:

firebase init

Follow the setup wizard instructions and:

  • Choose “hosting” as a required feature.
  • Choose not to re-write all requests to index.html .
  • Provide dist as a public directory because our build will live inside that folder.

Once the initialization is complete, a new firebase.json file will be created in the project root.

Performing the Deployment

At this point we are ready to get rolling. Compile your application with an ahead-of-time compiler (which is the preferred way):

ng build --prod --localize

This will create both English and Russian versions of the app under the dist directory in one go. This is very convenient because in the previous Angular versions we had to build each application separately and it took significantly more time. It still possible to provide a specific version of the app to build like so:

ng build --configuration=production,ru

Once the build is finished, deploy to Firebase:

firebase deploy

Now you can browse your application. In my case there are two URLs:

Making it work with Angular Routing

At this point your application should work fine on Firebase. However, if you are going to implement a routing system, additional steps have to be taken.

Creating components and routes

Let’s create two simple routes: /home and /about that should utilize HomeComponent and AboutComponent respectively. Start by creating these two components:

ng generate component home
ng generate component about

Next, open app.module.ts , import the router module and provide the actual routes:

import { RouterModule } from '@angular/router';

// ...

// Replace imports with the following:

  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {path: 'home', component: HomeComponent},
      {path: 'about', component: AboutComponent},
      {path: '', redirectTo: '/home', pathMatch: 'full'}
    ]),
  ],

So, we have added handlers for the /home and /about paths, and also we have provided a redirect to open the “home” page by default.

Now open up app.component.html and move everything except for the language switcher to the home/home.component.html file:

<!-- home.component.html -->

<h1 i18n="main header|Friendly welcoming message@@welcome">Hello everyone!</h1>

<p>
  <a i18n i18n-href i18n-title href="/en-US/portfolio" title="My Portfolio">Portfolio</a>
</p>

<p>{{ created_by }}</p>

<p i18n>{tasksCount, plural, zero {no tasks}
  one {{{tasksCount}} task}
  few {{{tasksCount}} tasks}
  many {{{tasksCount}} tasks}
  other {{{tasksCount}} tasks}}</p>

<span i18n>Gender: {genderCode, select, 0 {male} 1 {female} other {unknown}}</span>

<button (click)="male()">&#9794;</button>
<button (click)="female()">&#9792;</button>
<button (click)="other()">&#9895;</button>

<ng-container i18n>Copyright 2020</ng-container>

<p>{{today | date:'fullDate'}}</p>

Tweak your app.component.html so that it looks like this:

<ul>
  <li *ngFor="let locale of localesList">
    <a href="/{{locale.code}}/">
      {{locale.label}}
    </a>
  </li>
</ul>

<nav>
  <a class="button" routerLink="/home" i18n>Home</a> |
  <a class="button" routerLink="/about" i18n>About</a>
</nav>

<h1 i18n>Main page</h1>

<router-outlet></router-outlet>

Finally, replace the contents inside the about/about.component.html with a header:

<h2 i18n>About Us</h2>

Translating the new pages

We have added some new HTML tags so let’s extract the texts into our translation files:

ng extract-i18n --output-path src/i18n

If you are on Angular < 11 then use ng xi18n command instead (the command line arguments will be the same).

Now copy the generated XML tags from messages.xlf , paste them into the messages.ru.xlf file, and adjust your translations:

<trans-unit id="847171f94c5e932a67df0c3b4d57e4e10d844b52" datatype="html">
  <source>About Us</source>
  <target>О нас</target>
  <context-group purpose="location">
    <context context-type="sourcefile">src/app/about/about.component.html</context>
    <context context-type="linenumber">1</context>
  </context-group>
</trans-unit>

<trans-unit id="92eee6be6de0b11c924e3ab27db30257159c0a7c" datatype="html">
  <source>Home</source>
  <target>Домашняя</target>
  <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">10</context>
  </context-group>
</trans-unit>
<trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
  <source>About</source>
  <target>О нас</target>
  <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">11</context>
  </context-group>
</trans-unit>
<trans-unit id="0d175dbfa58e58e67069b734fd78e7b7a6b83fbb" datatype="html">
  <source>Main page</source>
  <target>Главная страница</target>
  <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">14</context>
  </context-group>
</trans-unit>

This is it!

Configuring Firebase

The most important step here is to configure Firebase properly. As long as we’re using the AOT compiler, Firebase will effectively serve two different applications (for Russian and English locales). Therefore, to make Firebase play nicely with our Angular routes, we should adjust the rewrite rules. To achieve that, open firebase.json and add the following lines:

{
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [ // <-- add this section
      {
        "source": "/en-US/**",
        "destination": "/en-US/index.html"
      },
      {
        "source": "/ru/**",
        "destination": "/ru/index.html"
      }
    ]
  }
}

Another problem is that opening http://lokalise-angular-i18n.web.app directly (without providing locale part in the URL) results in a 404 error. To fix that issue, let’s redirect all such requests to the English version of the app:

{
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],  
    "rewrites": [   
      {
        "source": "/en-US/**",
        "destination": "/en-US/index.html"
      },
      {
        "source": "/ru/**",
        "destination": "/ru/index.html"
      }
    ],
    "redirects": [ // <-- add this section
      {
        "source" : "/",
        "destination" : "/en-US",
        "type" : 301
      }
    ],
  }
}

This is the final version of our firebase.json config file.

Deploying once again

So, we have dealt with all the Firebase issues. Now you can build your application again (as we have added new components and routes):

ng build --prod --localize

And finally deploy everything to Firebase:

firebase deploy

Great job!

Conclusion

So, in this article we have seen how to introduce internationalization in Angular apps with a built-in localize module. We have learned how to configure the Angular app, perform simple translations, work with pluralization and gender information, and how to introduce localization and translate texts within components. Finally, we have built and deployed the demo application to Firebase. Great job!

That’s all for today, folks. I hope you found this article useful. Thanks for staying with me, and I’ll see you next time!

Easily manage Angular i18n translation files

Grab a FREE Lokalise trial and start internationalizing your Angular app. Start now

Further reading

Related posts

Learn something new every two weeks

Get the latest in localization delivered straight to your inbox.

Related articles
Localization made easy. Why wait?
The preferred localization tool of 2000+ companies