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:
- Adding the
i18n
section. Be sure to add it under the project config. - Setting the source language of the application to
en-US
. - Adding support for other locales.
- Specifically, I would like to translate my app into Russian (the
ru
locale). Feel free to add any other languages as necessary. - 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. - Set
localize
totrue
in order to generate application variants for each locale. We’ll use the ahead-of-time compilation. - 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 theid
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:
- We’re adding a new configuration specifically for the
ru
locale. - 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 tobuildTarget
.
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:
- Log in to your account at lokalise.com.
- Navigate to Profile settings > API tokens.
- Generate a new read-write token and keep this tab open.
- Download the latest release of CLIv2.
- Unpack the CLI somewhere on your PC and
cd
into the newly created directory. - 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- I18n and l10n: List of developer tutorials
- Lokalise API: What it is and how to use it
- Ionic translation and localization
- Angular localization with Transloco
- Vue 3 i18n: Building a multi-language app with locale switcher
- Localization testing: What is it & how to do it
- Localization in Node.js and Express.js with i18n examples