
Localization strategy
Software localization
When it comes to Angular localization, one of the most popular open-source i18n libraries, ngx-translate, lets you define translations for your app and switch between them dynamically. You can either use a service, directive, or pipe to handle the translated content.
To make you familiar with all of them, this tutorial will walk you through localizing in Angular with ngx-translate step by step. For demonstration purposes, we will create a sample feedback form for Phrase, the most reliable localization solution, and launch our demo app in two different languages.
You can access the demo app via Google Firebase to understand how ngx-translate works with an Angular app in a production environment. To get the source code for the demo app, make sure you stop by at GitHub.
🗒 Note » Make sure you have an Angular dev environment set up on your machine. Should this not be the case, please refer to the Angular setup guide.
The Angular framework has a robust built-in i18n library. However, the ngx-translate library has some shiny advantages over the built-in one:
Navigate to the directory where you want to create the new project. Open the command prompt, and run the command shown below to create a new Angular app named ngx-translate-i18n
.
ng new ngx-translate-i18n --routing=false --style=scss
Run the following command to install the ngx-translate/core
library in your app:
npm install @ngx-translate/core
We will need to install a loader that will help us load the translations from files using HttpClient
. Run the command as follows:
npm install @ngx-translate/http-loader
We will add a separate module for ngx-translate. Run the following command to create a new module in your app.
ng g m translate
Add the following code to the translate.module.ts
file:
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { HttpClientModule, HttpClient } from '@angular/common/http'; import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './assets/i18n/', '.json'); } @NgModule({ declarations: [], imports: [ CommonModule, HttpClientModule, TranslateModule.forRoot({ defaultLanguage: 'en', loader: { provide: TranslateLoader, useFactory: HttpLoaderFactory, deps: [HttpClient], }, }), ], exports: [TranslateModule], }) export class NgxTranslateModule { }
We have configured the TranslateLoader
for our application. The TranslateHttpLoader
class is used to define the path and file extension for the translation files. We have imported the TranslateModule
, and the default language for the app is set to English.
ngx-bootstrap is an open-source library that provides an easy way to integrate Bootstrap components into an Angular app. To add ngx-bootstrap to your application, run the following command in the root directory of the project.
ng add ngx-bootstrap
Font Awesome is an open-source library that provides a wide array of icons that can be used to style our app. To include it in your app, add the following code lines to the <head>
section of the index.html
file:
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
Add the following lines of code in the src/app/app.module.ts
file to import the required modules.
import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; import { NgxTranslateModule } from './translate/translate.module'; import { FormsModule } from '@angular/forms'; @NgModule({ ... imports: [ ... BsDropdownModule.forRoot(), NgxTranslateModule, FormsModule, ], ... })
We will use the dropdown module of ngx-bootstrap to display a language selection dropdown in the navbar of our app. For the creation of the feedback form, we will use template-driven forms. That is why we import the FormsModule
as well as the custom NgxTranslateModule
.
Run the following command to create the nav-bar component for the application.
ng g c nav-bar --module app
Open src\app\nav-bar\nav-bar.component.ts
and replace the existing code with the following one:
import { Component } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'app-nav-bar', templateUrl: './nav-bar.component.html', styleUrls: ['./nav-bar.component.scss'], }) export class NavBarComponent { siteLanguage = 'English'; languageList = [ { code: 'en', label: 'English' }, { code: 'de', label: 'Deutsch' }, ]; constructor(private translate: TranslateService) { } changeSiteLanguage(localeCode: string): void { const selectedLanguage = this.languageList .find((language) => language.code === localeCode) ?.label.toString(); if (selectedLanguage) { this.siteLanguage = selectedLanguage; this.translate.use(localeCode); } const currentLanguage = this.translate.currentLang; console.log('currentLanguage', currentLanguage); } }
Here, we have defined a list of languages and their standard locale codes.
The changeSiteLanguage
function will invoke the use
function of the TranslateService
to set the active language of the app to the language selected in the nav-bar menu. We will then set the siteLanguage
by fetching the language name from the list of languages corresponding to the currently active language.
Open src\app\nav-bar\nav-bar.component.html
and replace what you see in there with the following code:
<nav class="navbar navbar-dark navbar-expand-lg"> <a class="navbar-brand">Phrase</a> <span class="spacer"></span> <div class="btn-group" dropdown> <button id="button-animated" dropdownToggle type="button" class="btn btn-link dropdown-toggle" aria-controls="dropdown-animated" > <i class="fa fa-globe" aria-hidden="true"></i> {{ siteLanguage }} <span class="caret"></span> </button> <ul id="dropdown-animated" *dropdownMenu class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="button-animated" > <ng-container *ngFor="let language of languageList"> <li role="menuitem"> <a class="dropdown-item" (click)="changeSiteLanguage(language.code)"> {{ language.label }} </a> </li> </ng-container> </ul> </div> </nav>
We have created a nav-bar that has a drop-down menu with two options for selecting the language of our app during runtime. When we click on the menu item, it will invoke the changeSiteLanguage
function, which will then change the app language dynamically, and the content will be served in the selected language. We will also display the siteLanguage
in the nav-bar so it gets updated as we change the language of our app.
We will use the template-driven form for creating the feedback form for our application. Therefore, we need to create a model. Set up a new folder called models inside the src/app
folder. Create a file called feedback.ts
inside the Models folder and insert the following code:
export class Feedback { name: string; gender: string; rating: string; comment: string; constructor() { this.name = ''; this.gender = ''; this.rating = ''; this.comment = ''; } }
Before creating the feedback form, we will add the translation file for both languages—English and German. Add a folder called i18n
inside the src/assets
folder; add a file called en.json
to this folder and enter the following code:
{ "title": "Service feedback for {{company}}", "name": { "label": "Name", "placeholder": "Enter Name" }, "gender": { "label": "Gender", "select": "Select gender", "options": { "male": "Male", "female": "Female", "other": "Other" } }, "comment": { "label": "Comment", "placeholder": "Enter Comment" }, "rating": { "label": "Rate our customer service: ", "options": { "excellent": "Excellent", "good": "Good", "bad": "Bad" } }, "submit": { "label": "Submit" }, "requiredErrorMessage": "This is a required field.", "successfulSubmitMessage": "Thanks for your valuable feedback!!!\nThe feedback has been submitted successfully.", "copyright": "All rights reserved. {{currentYear}} {{company}}" }
We have now created the key-value pairs for all our translatable content. The feedback form will have four fields: "Name," "Gender," "Comment," and "Rating".
The title key will accept a parameter called "company", which will help us add the value dynamically. The copyright key will accept currentYear
and "company" as the parameter.
Similarly, create the translation keys for German in the de.json
file.
{ "title": "Service-Feedback für {{company}}", "name": { "label": "Namen", "placeholder": "Name eingeben" }, "gender": { "label": "Geschlecht", "select": "Wähle Geschlecht", "options": { "male": "Männlich", "female": "Weiblich", "other": "Andere" } }, "comment": { "label": "Kommentar", "placeholder": "Kommentar eingeben" }, "rating": { "label": "Bewerten Sie unseren Kundenservice:", "options": { "excellent": "Ausgezeichnet", "good": "Gute", "bad": "Schlecht" } }, "submit": { "label": "Senden" }, "requiredErrorMessage": "Dies ist ein Pflichtfeld.", "successfulSubmitMessage": "Vielen Dank für Ihr wertvolles Feedback!!!\nDas Feedback wurde erfolgreich übermittelt.", "copyright": "Alle Rechte vorbehalten. {{currentYear}} {{company}}" }
The label for all the fields in the form is translated with the help of the translate
pipe.
<label>{{ "name.label" | translate }}</label> ... <label>{{ "gender.label" | translate }}</label>
We can also pass the parameter to the translation key as follows:
<h3>{{ "title" | translate: titleParam }}</h3>
We have used the translate
directive to fetch the translation values for the required error message for all the fields. The translation key can be supplied as the content of the HTML element.
<span translate class="text-danger" *ngIf=" (name.touched || feedbackForm.submitted) && name.errors?.required " > requiredErrorMessage </span>
To display the copyright information, we have used the translate
directive and supplied the required parameter using the translateParams
input property.
<p> © <small translate [translateParams]="copyrightInfoParam" class="text-muted" >copyright </small> <br /> </p>
Another way of using the translate
directive is to use the translation key as the attribute value.
<p> © <small [translate]="'copyright'" [translateParams]="copyrightInfoParam" class="text-muted" > </small> </p>
For the dropdown of the gender field in the form, we are creating the translation keys dynamically using a ngFor directive. We have used the translate
pipe to fetch the values for gender.
<select class="form-control" data-val="true" [(ngModel)]="customerFeedback.gender" name="gender" #gender="ngModel" required > <option value="">{{ "gender.select" | translate }}</option> <ng-container *ngFor="let gender of genderList"> <option value="{{ 'gender.options.' + gender | translate }}"> {{ "gender.options." + gender | translate }} </option> </ng-container> </select>
The get()
function of TranslateService
is used to fetch the translated value of a key in a TypeScript file.
saveFeedback() { this.translate .get('successfulSubmitMessage') .subscribe((successMessage: string) => { alert(successMessage); }); console.table(this.customerFeedback); }
We are fetching the value of the successfulSubmitMessage
key to display a success alert message. We are mocking the save functionality of the form by logging the output in the browser.
We will create a new component called MiscellaneousExampleComponent
to cover the following features of ngx-translate:
We first install the ngx-translate-messageformat-compiler
package. It is a compiler for ngx-translate that uses messageformat.js to compile translations using ICU syntax for handling pluralization and gender.
Run the following command:
npm install ngx-translate-messageformat-compiler messageformat
Currently, the ngx-translate-messageformat-compiler package has a dependency on messageformat, which is a deprecated package. The author is already working on adapting to the new dependency.
Refer to the GitHub issue - https://github.com/lephyrus/ngx-translate-messageformat-compiler/issues/74
The next step is to update the translate.module.ts
file as shown below:
import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler'; import { TranslateCompiler } from '@ngx-translate/core'; ... @NgModule({ imports: [ ... TranslateModule.forRoot({ ... compiler: { provide: TranslateCompiler, useClass: TranslateMessageFormatCompiler } }) ], ... })
This will allow ngx-translate to use the messageformat compiler for rendering the translated messages.
Since the ngx-translate-messageformat-compiler
package is using the ICU syntax, we need to update the interpolation syntax in the translation files.
Earlier, we used double curly braces for interpolation:
"title": "Service feedback for {{company}}"
However, the ICU parser is using only a single bracket for interpolation. That is why we need to update the translation as follows:
"title": "Service feedback for {company}"
We will also add the translations for pluralization to the en.json
file:
{ "title": "Service feedback for {company}", "name": { "label": "Name", "placeholder": "Enter Name" }, "gender": { "label": "Gender", "select": "Select gender", "options": { "male": "Male", "female": "Female", "other": "Other" } }, "comment": { "label": "Comment", "placeholder": "Enter Comment" }, "rating": { "label": "Rate our customer service: ", "options": { "excellent": "Excellent", "good": "Good", "bad": "Bad" } }, "submit": { "label": "Submit" }, "requiredErrorMessage": "This is a required field.", "successfulSubmitMessage": "Thanks for your valuable feedback!!!\nThe feedback has been submitted successfully.", "copyright": "All rights reserved. {currentYear} {company}", "pluralization": { "items": "You have selected {count, plural, =0{nothing} one{one} other{multiple}} {count, plural, =0{} one{item} other{items}}.", "gender": "{gender, select, male{He is a} female{She is a} other{They are}} {gender}." } }
Similarly, we will update the de.json
file as shown below:
{ "title": "Service-Feedback für {company}", "name": { "label": "Namen", "placeholder": "Name eingeben" }, "gender": { "label": "Geschlecht", "select": "Wähle Geschlecht", "options": { "male": "Männlich", "female": "Weiblich", "other": "Andere" } }, "comment": { "label": "Kommentar", "placeholder": "Kommentar eingeben" }, "rating": { "label": "Bewerten Sie unseren Kundenservice:", "options": { "excellent": "Ausgezeichnet", "good": "Gute", "bad": "Schlecht" } }, "submit": { "label": "Senden" }, "requiredErrorMessage": "Dies ist ein Pflichtfeld.", "successfulSubmitMessage": "Vielen Dank für Ihr wertvolles Feedback!!!\nDas Feedback wurde erfolgreich übermittelt.", "copyright": "Alle Rechte vorbehalten. {currentYear} {company}", "pluralization": { "items": "Du hast ausgewählt {count, plural, =0{nichts} one{eins} other{mehrere}} {count, plural, =0{} one{Artikel} other{Produkte}}.", "gender": "{gender, select, male{er ist ein} female{Sie ist ein} other{Sie sind }} {gender}." } }
To learn more about the ICU syntax, please refer to the ICU Documentation.
🗒 Note » The key name for pluralization can have any string value. Here, for simplicity, we use pluralization
as the key name.
Add the following code to the miscellaneous-example.component.html
file:
<h3>Pluralization</h3> <br /> <h5>Select Items</h5> <div class="row"> <div class="col-md-4"> <select [(ngModel)]="itemQuantity" class="form-control" data-val="true"> <option value="0">0</option> <option value="1">1</option> <option value="2">2</option> </select> </div> <div class="col-md-4"> <strong translate [translateParams]="{ count: itemQuantity }" >pluralization.items</strong > </div> </div> <br /> <h5>Select Gender</h5> <div class="row"> <div class="col-md-4"> <select [(ngModel)]="selectedGender" class="form-control" data-val="true"> <option value="male">Male</option> <option value="female">Female</option> <option value="others">Others</option> </select> </div> <div class="col-md-4"> <strong>{{ "pluralization.gender" | translate: { gender: selectedGender } }}</strong> </div> </div>
We have added a drop-down list to select the number of items. We will display the translated text based on the quantity chosen. We are using the translate directive to pluralize the selected items.
Similarly, we will display the pluralized gender data based on the selection from the drop-down list. Here, we used the translate
pipe and passed the selectedGender
as the parameter.
Add the following translations to the en.json
file:
"CASESPECIFICKEY": "THE TRANSLATION KEY IS DEFINED AS {case}.", "casespecifickey": "the translation key is defined as {case}.", "Casespecifickey": "The Translation Key Is Defined As {case}.",
We will update the de.json
file as well:
"CASESPECIFICKEY": "DER ÜBERSETZUNGSSCHLÜSSEL IST ALS {case} DEFINIERT.", "casespecifickey": "der Übersetzungsschlüssel ist als {case} definiert.", "Casespecifickey": "Der Übersetzungsschlüssel ist als {case} definiert."
We can cascade the Angular built-in pipes with the translate
pipe. As shown in the example below, we are using the built-in pipes uppercase, lowercase, and title case to generate the translation key.
<p>{{ caseSpecificKey | uppercase | translate: { case: "UPPERCASE" } }}</p> <p>{{ caseSpecificKey | lowercase | translate: { case: "lowercase" } }}</p> <p>{{ caseSpecificKey | titlecase | translate: { case: "Titlecase" } }}</p>
Add the following translations to the en.json
file:
"aboutPhrase": "<h1>What is Phrase?</h1><p>Phrase is an all-in-one platform for scalable software localization and translation management. Be it web or mobile apps, Phrase enables you to translate any kind of software.</p>",
We will also update the de.json
file with the corresponding translations:
"aboutPhrase": "<h1>Was ist Phrase?</h1><p>Phrase ist eine All-in-One-Plattform für skalierbare Softwarelokalisierung und Übersetzungsmanagement. Sei es Web- oder mobile Apps, Phrase ermöglicht es Ihnen, jede Art von Software zu übersetzen.</p>",
The ngx-translate library allows us to use raw HTML tags within our translation. We can use the innerHTML
attribute with the translate pipe on any HTML element.
<div [innerHTML]="'aboutPhrase' | translate"></div>
🗒 Note » Using innerHTML makes the website vulnerable to cross-site scripting (XSS) attacks. Please read the Security considerations for details.
Let us explore some useful properties provided by TranslateService
.
We can use the currentLang
property to fetch the locale code of the currently active language.
const currentLanguage = this.translate.currentLang;
We can use the onLangChange
EventEmitter to "listen" to the language change events:
this.translate.onLangChange .subscribe((event: LangChangeEvent) => { console.log('onLangChange', event); });
The LangChangeEvent
is an interface having the following two properties:
We can use the onTranslationChange
EventEmitter to "listen" to the translation change events:
this.translate.onTranslationChange .subscribe((event: TranslationChangeEvent) => { console.log('onTranslationChange', event); });
The TranslationChangeEvent
provides us with two properties—lang and translations—similar to the LangChangeEvent
interface.
The onDefaultLangChange
is an EventEmitter that is fired when the default language of the application is changed.
this.translate.onDefaultLangChange .subscribe((event: DefaultLangChangeEvent) => { console.log('onDefaultLangChange', event); });
The DefaultLangChangeEvent
provides us with two properties—lang and translations—similar to the LangChangeEvent
and TranslationChangeEvent
interfaces.
Run the following command to create the feedback component:
ng g c feedback --module app
Open src\app\feedback\feedback.component.ts
and replace the existing code with the following one:
import { Component } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Feedback } from '../models/feedback'; @Component({ selector: 'app-feedback', templateUrl: './feedback.component.html', styleUrls: ['./feedback.component.scss'], }) export class FeedbackComponent { companyName = 'Phrase'; genderList = ['male', 'female', 'other']; customerFeedback = new Feedback(); copyrightInfoParam: any; titleParam: any; constructor(private translate: TranslateService) { this.titleParam = { company: this.companyName }; this.copyrightInfoParam = { currentYear: new Date().getFullYear(), company: this.companyName, }; } saveFeedback() { this.translate .get('successfulSubmitMessage') .subscribe((successMessage: string) => { alert(successMessage); }); console.table(this.customerFeedback); } }
The constructor is used to initialize the titleParam
and the copyrightInfoParam
properties. We will use both in the template file while fetching the translation values.
We will create an object of the Feedback
class type, which will bind to the form. The saveFeedback
function will be invoked as soon as the form submission is successfully submitted.
Open src\app\feedback\feedback.component.html
and add the form as displayed below. You can refer to GitHub for the complete source code.
<div class="card"> <div class="card-header"> <h3>{{ "title" | translate: titleParam }}</h3> </div> <div class="card-body"> <form #feedbackForm="ngForm" (ngSubmit)="feedbackForm.form.valid && saveFeedback()" novalidate > <div class="form-group"> <label>{{ "name.label" | translate }}</label> <input type="text" class="form-control" [placeholder]="'name.placeholder' | translate" [(ngModel)]="customerFeedback.name" name="name" #name="ngModel" required /> <span translate class="text-danger" *ngIf=" (name.touched || feedbackForm.submitted) && name.errors?.required " > requiredErrorMessage </span> </div> <div class="form-group"> <label>{{ "gender.label" | translate }}</label> <select class="form-control" data-val="true" [(ngModel)]="customerFeedback.gender" name="gender" #gender="ngModel" required > <option value="">{{ "gender.select" | translate }}</option> <ng-container *ngFor="let gender of genderList"> <option value="{{ 'gender.options.' + gender | translate }}"> {{ "gender.options." + gender | translate }} </option> </ng-container> </select> <span translate class="text-danger" *ngIf=" (gender.touched || feedbackForm.submitted) && gender.errors?.required " > requiredErrorMessage </span> </div> <div class="form-group"> <label>{{ "comment.label" | translate }}</label> <textarea type="text" class="form-control" [placeholder]="'comment.placeholder' | translate" [(ngModel)]="customerFeedback.comment" name="comment" #comment="ngModel" required ></textarea> <span translate class="text-danger" *ngIf=" (comment.touched || feedbackForm.submitted) && comment.errors?.required " > requiredErrorMessage </span> </div> <div class="form-group"> <label>{{ "rating.label" | translate }}</label> <div class=""> <div class="custom-control custom-radio custom-control-inline"> <input id="excellent" type="radio" class="custom-control-input" value="excellent" name="rating" [(ngModel)]="customerFeedback.rating" #rating="ngModel" required /> <label class="custom-control-label" for="excellent" >{{ "rating.options.excellent" | translate }} </label> </div> <div class="custom-control custom-radio custom-control-inline"> <input id="good" type="radio" class="custom-control-input" value="good" name="rating" [(ngModel)]="customerFeedback.rating" #rating="ngModel" required /> <label class="custom-control-label" for="good">{{ "rating.options.good" | translate }}</label> </div> <div class="custom-control custom-radio custom-control-inline"> <input id="bad" type="radio" class="custom-control-input" value="bad" name="rating" [(ngModel)]="customerFeedback.rating" #rating="ngModel" required /> <label class="custom-control-label" for="bad">{{ "rating.options.bad" | translate }}</label> </div> </div> <span translate class="text-danger" *ngIf=" (rating.touched || feedbackForm.submitted) && rating.errors?.required " > requiredErrorMessage </span> </div> <div class="row form-group"> <div class="col d-flex justify-content-end"> <button translate type="submit" class="btn btn-success"> {{ "submit.label" }} </button> </div> </div> </form> <div class="row"> <div class="col"> <p> © <!-- translation directive — key as a child --> <small translate [translateParams]="copyrightInfoParam" class="text-muted" >copyright </small> <br /> </p> </div> </div> </div> </div>
We now have a template-driven form created, which will have the following fields:
All fields are mandatory.
Run the following command to execute the app in your local environment:
ng serve -o
As soon as the app is launched, you will see the screen below. Select the language value from the drop-down in the nav-bar, and you will get the content displayed in the chosen language.
Great job, you have reached the end of this Angular i18n tutorial. Let us do a recap of what we learned about internationalizing with ngx-translate:
If you think your app is now fully ready for localization, give Phrase a try. A software localization platform engineered to streamline app localization end to end, Phrase features a flexible API and CLI, as well as a beautiful web interface for effective collaboration with your translators.
Check out all Phrase features for developers and see for yourself how it can help you take your apps global.
Last updated on September 25, 2022.