Software localization
Angular Tutorial on Localizing with ngx-translate
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.
Why use ngx-translate instead of Angular i18n?
The Angular framework has a robust built-in i18n library. However, the ngx-translate library has some shiny advantages over the built-in one:
- The ngx-translate library allows us to change the language of the application at runtime without reloading the whole app. However, Angular allows us to use only one language at a time. If you want to use a different language, then you need to reload the application with a new set of translations.
- The ngx-translate library allows us to use JSON files for translation by default. We can also create our own loader to support any format we want.
- The ngx-translate library has a wide range of APIs, which allows us to manipulate the translation data during runtime.
Configuring ngx-translate for an Angular app
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" />
Updating the AppModule
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
.
Creating a nav-bar component
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.
Creating a model
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 = ''; } }
Creating translation files
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}}" }
Using the translate pipe
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>
Using the translate directive
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>
Dynamic translation keys
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>
Fetching a translation key in TypeScript files
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.
Miscellaneous example component
We will create a new component called MiscellaneousExampleComponent
to cover the following features of ngx-translate:
- Pluralization
- Using the translate pipe with other pipes
- Using raw HTML tags within the translation
Configuring the app to use pluralization with 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.
Updating the translation file
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.
Adding pluralization
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.
Using the translate pipe with built-in Angular pipes
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>
Using raw HTML tags within a translation
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.
Exploring TranslateService
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:
- lang—a string denoting the locale code of the currently active language
- translations—an object containing the key-value translation pairs.
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.
Creating a Feedback Component
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:
- Name—an input field used to store the name of the user
- Gender—a select field displaying three different options to choose from
- Comment—a text area field used to store comments provided by the user
- Rating—a radio group field that asks the user to rate Phrase
All fields are mandatory.
Angular I18n Execution Demo
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.
Concluding our Angular i18n tutorial with ngx-translate
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:
- Using the translate pipe
- Using the translate directive
- Cascading the translate pipe with built-in Angular pipes
- Using interpolation with translation keys
- Pluralization
- Using raw HTML tags within the translation
- Change the language of the app dynamically during run time
- Dynamically generating the translation keys
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.