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 using a special tool.
  • Working with pluralization and gender information.
  • Working with Angular pipes.
  • Performing translations within components.
  • Adding a language switcher.
  • Working with Angular routes.
  • Building the app using the AOT compiler and deploying to production.

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

The source code can be found at GitHub.

The working demo can be found at lokalise-i18n-angular15.web.app.

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

Check out how our platform can help you translate your Angular apps faster.

 

    Preparing for Angular i18n

    We’ll require the Angular CLI, so be sure to install it by running:

    npm install -g @angular/cli

    Next, create a new Angular 15 application using the following command:

    ng new i18n-angular-lokalise

    Follow the installation wizard’s instructions:

    • Do not enable Angular routing (yet—we’ll discuss this topic later in the article).
    • Use CSS for style sheets (this is up to you really; I’m not going to apply any styling).

    Next, simply wait a couple of minutes and then make sure the app is starting without any issues:

    cd i18n-angular-lokalise
    ng serve --open

    The ng serve --open 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, which will add internationalization support to the app.

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

    {
      "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
      "version": 1,
      "newProjectRoot": "projects",
      "projects": {
        "i18n-angular-lokalise": {
          "projectType": "application",
          // ...
          "i18n": { // <--- 1
            "sourceLocale": "en-US", // <--- 2
            "locales": { // <--- 3
              "ru": { // <--- 4
                "translation": "src/locale/messages.ru.xlf" // <--- 5
              }
            }
          },
          "architect": {
            "build": {
              "builder": "@angular-devkit/build-angular:browser",
              "options": {
                "localize": true, // <--- 6
                "i18nMissingTranslation": "error", // <--- 7
                // ...
              },
              "configurations": {
                // ...
              },
              "defaultConfiguration": "production"
            },
            // ...
          }
        }
      }
    }
    

    So, we are doing the following:

    1. Adding the i18n section. Be sure to add it under the project config.
    2. Setting the source language of the application to en-US.
    3. Adding support for other locales.
    4. Specifically, I would like to translate my app into Russian (the ru locale). Feel free to add any other languages as necessary.
    5. 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 this option is no longer used.
    6. Set localize to true in order to generate application variants for each locale. We’ll use the ahead-of-time compilation.
    7. Report any missing translations upon building the app.

    You can adjust other options as required and provide language-specific configuration as explained in the official documentation.

    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 the 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 contain translation metadata 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/locale folder. As for the translation format, there are multiple options:

    • XLIFF 1.2 (default)
    • XLIFF 2
    • XML message bundle (XMB)
    • JSON
    • ARB

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

    Let’s configure this tool by opening the angular.json file and finding the extract-i18n section inside it:

    // ... other options are omitted ...
    
    "extract-i18n": {
      "builder": "@angular-devkit/build-angular:extract-i18n",
      "options": {
        "browserTarget": "i18n-angular-lokalise:build",
        "format": "xlf",
        "outputPath": "src/locale"
      }
    }
    
    // ...

    Please note that if you’re using Angular 17+, the browserTarget option should be renamed to buildTarget.

    Alternatively, you can pass these options from the command line as shown in the docs.

    Extracting translations

    Now simply run the following command:

    ng extract-i18n

    In the src/locales 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="2241399650733502220" datatype="html">
            <source>Hello world!</source>
            <context-group purpose="location">
              <context context-type="sourcefile">src/app/app.component.html</context>
              <context context-type="linenumber">1</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 — extract-i18n 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 the translation lives.
    • context-type="linenumber" shows the actual line of code.
    • Also, there are two note tags that provide the translation description and meaning, respectively.

    You can learn more about XLIFF files in general and how to translate them in our tutorial.

    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="2241399650733502220" datatype="html">
            <source>Hello world!</source>
            <target>Привет, мир!</target> <!-- YOUR TRANSLATION HERE -->
            <context-group purpose="location">
              <context context-type="sourcefile">src/app/app.component.html</context>
              <context context-type="linenumber">1</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, let’s check that everything is translated properly. We’ll tweak the configuration within the angular.json file:

    "build": {
        // ...
        "configurations": {
          // ...
          "ru": {
            "localize": ["ru"] // <--- 1
          }
        },
        // ...
      },
      "serve": {
        "builder": "@angular-devkit/build-angular:dev-server",
        "configurations": {
          // ...
          "ru": {
            "browserTarget": "i18n-angular-lokalise:build:development,ru" // <--- 2
          }
        },
        // ...
      },
      // ...
    }

    Here are two relevant lines:

    1. We’re adding a new configuration specifically for the ru locale.
    2. This configuration should be available when serving the app in development. Please note that if you’re using Angular 17+, the browserTarget option should be renamed to buildTarget.

    Now we can easily use the ng serve command and instruct Angular to show us the Russian version of the app:

    ng serve --configuration=ru --open

    Ensure that the translation is displayed properly, and proceed to the next section.

    Translation identifiers

    I’ve already mentioned that translation IDs (the id attribute for the trans-unit tag) have special meanings. These IDs are unique and extract-i18n 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 as follows:

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

    Then regenerate base translation file:

    ng extract-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="1970068439210080997" datatype="html">
            <source>Hello everyone!</source>
            <context-group purpose="location">
              <context context-type="sourcefile">src/app/app.component.html</context>
              <context context-type="linenumber">1</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

    You’ll see the below error message in the console because the translation ID has changed:

    No translation found for "1970068439210080997" ("Hello everyone!" - "main header").

    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="1970068439210080997" datatype="html">
            <!-- ... -->
          </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 that 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>

    Rerun the extract-i18n 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">
            <!-- ... -->
          </trans-unit>
        </body>
      </file>
    </xliff>
    

    The ID is now set to welcome, and it will not change 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>
    

    Be 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>

    Feel free to check the Translation keys: naming conventions and organizing article to learn more on keys organization.

    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 normal by running:

    ng extract-i18n

    Copy the new trans-unit tags from the src/i18n/messages.xlf file into the src/i18n/messages.ru.xlf file 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="8336405567663867706" 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">4</context>
            </context-group>
          </trans-unit>
          <trans-unit id="6709320430327029754" 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">4</context>
            </context-group>
          </trans-unit>
          <trans-unit id="9201103587777813545" 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">4</context>
            </context-group>
          </trans-unit>
        </body>
      </file>
    </xliff>
    

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

    Performing pluralization

    We’ll continue our Angular localization tutorial by discussing pluralization. Imagine that we’d 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:

    <span i18n>
      {tasksCount, plural, zero {no tasks} one {one task} other {{{tasksCount}} tasks}}
    </span>
    • 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.
    • other covers all other cases.

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

    // ...
    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="2380939390102386148" datatype="html">
            <source> <x id="ICU" equiv-text="{tasksCount, plural, =0 {no tasks} =1 {one task} other {{{tasksCount}} tasks}}" xid="1905583134648350404"/>
    </source>
            <context-group purpose="location">
              <context context-type="sourcefile">src/app/app.component.html</context>
              <context context-type="linenumber">7,9</context>
            </context-group>
          </trans-unit>
    <trans-unit id="1905583134648350404" datatype="html"> <source>{VAR_PLURAL, plural, =0 {no tasks} =1 {one task} other {<x id="INTERPOLATION"/> 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">8</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 helpful when working with gender information:

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

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

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

    export class AppComponent {
      tasksCount = 3;
      genderCode = 0;
    
      male() { this.genderCode = 0; }
      female() { this.genderCode = 1; }
      other() { this.genderCode = 2; }
    }

    Next, extract the translations and update the src/i18n/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="1533376390543422664" datatype="html">
            <source>Gender: <x id="ICU" equiv-text="{genderCode, select, 0 {male} 1 {female} other {other answer}}" xid="942937716535918063"/></source>
            <target>Пол: <x id="ICU" equiv-text="{genderCode, select, 0 {male} 1 {female} other {other answer}}" xid="942937716535918063"/></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="942937716535918063" datatype="html">
            <source>{VAR_SELECT, select, 0 {male} 1 {female} other {other answer}}</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">11</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 within the app.component.html file:

    <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 previously required some sort of tag. Sometimes, you may need to translate plain text without rendering any tags at all. This can be done with a ng-container, for instance:

    <ng-container i18n>Copyright 2023</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="7830261646811052149" datatype="html">
            <source>Copyright 2023</source>
            <target>Копирайт 2023</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>
    

    Using Angular pipes

    The built-in i18n module plays nicely with common pipes used for localization in Angular: DatePipe, CurrencyPipe, DecimalPipe, and PercentPipe.

    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>

    Now run the extractor again — it should detect the newly added content properly.

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

    <?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" equiv-text="this.company"/></source>
            <target>Создано компанией <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">20</context>
            </context-group>
          </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 accidentally remove an important tag and break the app. We don’t 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 management of translation and localization 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 has a robust API. Plus, there are more integrations with project management tools like Jira and 50+ others.

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

    It is possible to perform all manipulations with the 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 Profile settings > API tokens.
    3. Generate a new read-write token and keep this tab open.
    4. Download the latest release of CLIv2.
    5. Unpack the 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 a translation file as follows:

    lokalise2 file upload --lang-iso ru --file "PATH_TO_PROJECT\src\locale\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 next 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.

    Then, 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 sample app, and now it is time to share our work with the world!

    Preparing for Deployment

    In this section, 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

    Be sure to download the latest translations from Lokalise:

    lokalise2 file download --unzip-to "PATH_TO_PROJECT/locale/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

    Performing the deployment

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

    ng build

    This will create both English and Russian versions of the app under the dist directory in one go. This is very convenient because in previous Angular versions we had to build each application separately and this 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, let’s add a special package for Firebase:

    ng add @angular/fire

    Once the package is installed, you’ll need to answer a couple of questions:

    • Features to use: choose “hosting”
    • What Firebase account to use
    • Which project and website to use (you’ll have an option to create a new Firebase project)

    Having done that, run the following command:

    ng deploy

    After a few moments your site will be up and running!

    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'; // <--- add this
    
    // ...
    
    // 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 we also 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:

    <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>
    
    <span i18n>
      {tasksCount, plural, =0 {no tasks} =1 {one task} other {{{tasksCount}} tasks}}
    </span>
    
    <br>
    
    <span i18n>Gender: {genderCode, select, 0 {male} 1 {female} other {other answer}}</span>
    
    <button (click)="male()">♂</button>
    <button (click)="female()">♀</button>
    <button (click)="other()">⚧</button>
    
    <br>
    
    <p>{{today | date:'fullDate'}}</p>
    
    <br>
    
    <ng-container i18n>Copyright 2023</ng-container>
    
    <p>{{ created_by }}</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>

    Replace the contents inside about/about.component.html with a header:

    <h2 i18n>About Us</h2>

    Also, move the code from the app.components.ts file to the home/home.components.ts file (the localeList should stay within app.components.ts):

    export class HomeComponent {
      tasksCount = 3;
    
      genderCode = 0;
    
      male() { this.genderCode = 0; }
      female() { this.genderCode = 1; }
      other() { this.genderCode = 2; }
    
      today: number = Date.now();
    
      company = "Lokalise";
      created_by = $localize`Created by ${this.company}`;
    }

    Translating the new pages

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

    ng extract-i18n

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

    <?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="5736481176444461852" 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="2821179408673282599" 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="1726363342938046830" 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="326281569793279925" 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>
          <!-- ... -->
        </body>
      </file>
    </xliff>
    

    That’s it!

    Configuring Firebase

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

    {
        // ...
        "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 the 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 as shown here:

    {
      "hosting": {
        "target": "i18n-angular-lokalise",
        "public": "dist/i18n-angular-lokalise",
        "ignore": [
          "**/.*"
        ],
        "headers": [
          {
            "source": "*.[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].+(css|js)",
            "headers": [
              {
                "key": "Cache-Control",
                "value": "public,max-age=31536000,immutable"
              }
            ]
          },
          {
            "source": "/@(ngsw-worker.js|ngsw.json)",
            "headers": [
              {
                "key": "Cache-Control",
                "value": "no-cache"
              }
            ]
          }
        ],
        "rewrites": [
          {
            "source": "/en-US/**",
            "destination": "/en-US/index.html"
          },
          {
            "source": "/ru/**",
            "destination": "/ru/index.html"
          }
        ],
        "redirects": [ // <--- add this
          {
            "source": "/",
            "destination": "/en-US",
            "type": 301
          }
        ]
      }
    }

     

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

    Deploying once again

    We have now dealt with all the Firebase issues, so you can build and deploy your application again:

    ng deploy

    Great job!

    Next steps and further reading

    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 the demo application and deployed it to Firebase. Well done!

    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

    Talk to one of our localization specialists

    Book a call with one of our localization specialists and get a tailored consultation that can guide you on your localization path.

    Get a demo
    Related articles
    Localization made easy. Why wait?