Angular L10n with I18next

Phrase
Software Localization Tutorials
8 min readMay 1, 2019
If i18next is your favourite i18n and l10n library for frontend development, this is the right place to see how you can localize Angular applications with i18next. On top of that, we’ll see how we can use PhraseApp library integrations to automate the process and control the translation data with greater visibility.

We’ve been dealing with Angular l10n and i18n for quite a while now. We’ve compiled a list of best libraries for Angular i18n and even described how to localize Angular apps with ngx-translate or by using a built-in i18n module. If you’re familiar with i18next though and don’t want to switch to any other framework — we can’t blame you — there is an option to integrate it to your Angular project with the help of a community-maintained plugin.

This article will guide through the Angular l10n process with i18next and show you how to:

  • Create a new Angular project using the latest version
  • Integrate the i18next module into the app
  • Set a default locale and switch to another locale
  • Handle translation files
  • Use the Phrase command line interface (CLI) for syncing our translations with the cloud and improving our workflow

By the way, all the code shown in this tutorial is also hosted on GitHub. Let’s get started!

Get Ready for Angular l10n: Create a New Angular 7 Project

First of all, navigate to the Angular Quickstart Page and install Node.js, Angular-cli and create a new application. Let’s name it “my-i18n-app”. Answer “yes” to add routing and choose your preferred CSS methods.

➜ npm install -g @angular/cli
➜ ng new my-i18n-app
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS [ http://sass-lang.com/documentation/file.SASS_REFERENCE.html#syntax ]
...

Add i18next

Next, we need to hook up the i18next library with the use of the angular-i18next provider.

Let’s install those libraries…

➜ npm install i18next angular-i18next --save

Modify our app.module.ts to integrate and initialize the i18next config:

import { BrowserModule, Title } from '@angular/platform-browser';
import { NgModule, APP_INITIALIZER, LOCALE_ID } from '@angular/core';
import { I18NextModule, ITranslationService, I18NEXT_SERVICE, I18NextTitle, defaultInterpolationFormat } from 'angular-i18next';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';


export function appInit(i18next: ITranslationService) {
return () => i18next.init({
whitelist: ['en', 'gr'],
fallbackLng: 'en',
debug: true,
returnEmptyString: false,
ns: [
'translation',
'validation',
'error',
],
interpolation: {
format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
},
});
}

export function localeIdFactory(i18next: ITranslationService) {
return i18next.language;
}

export const I18N_PROVIDERS = [
{
provide: APP_INITIALIZER,
useFactory: appInit,
deps: [I18NEXT_SERVICE],
multi: true
},
{
provide: Title,
useClass: I18NextTitle
},
{
provide: LOCALE_ID,
deps: [I18NEXT_SERVICE],
useFactory: localeIdFactory
}];

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
I18NextModule.forRoot()
],
providers: [I18N_PROVIDERS],
bootstrap: [AppComponent]
})
export class AppModule { }

Here, we just import the i18next library and run it on appInit lifecycle hook. That way, Angular wouldn’t load until i18next initialize event fired. By default, we handle English and Greek translations.

Now update the app.component.html to interpolate the i18next tag for translating the content.

<div style="text-align:center">
<h1>
{{ 'message' | i18next }}!
</h1>
<img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>
<h2>Here are some links to help you start: </h2>
<ul>
<li>
<h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="https://angular.io/cli">CLI Documentation</a></h2>
</li>
<li>
<h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2>
</li>
</ul>

<router-outlet></router-outlet>

If we run the application now, we’ll notice that the message is not shown at all. Instead, it prints the key string.

The reason for that is that we haven’t created and referenced any translation strings for the i18next library to search. Let’s do that now.

Setting and Switching Locales

In order to load translations to our app, we need to install a backend plugin. In addition, we’d like to have a locale switcher based on a few parameters like a cookie or an URL parameter so we need another plugin that would handle that for us.

Install the following plugins:

npm install i18next-xhr-backend i18next-browser-languagedetector --save

We need the i18next-xhr-backend to load the translations from a file using Ajax and the i18next-browser-languagedetector to detect the current user locale base on some options we specify.

Now we need to enable them in our appInit. Update the app.module section.

export function appInit(i18next: ITranslationService) {
return () =>
i18next
.use(i18nextXHRBackend)
.use(i18nextLanguageDetector)
.init({
whitelist: ['en', 'el'],
fallbackLng: 'en',
debug: true,
returnEmptyString: false,
ns: [
'translation'
],
interpolation: {
format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
},
backend: {
loadPath: 'assets/locales/{{lng}}.{{ns}}.json',
},
// lang detection plugin options
detection: {
// order and from where user language should be detected
order: ['querystring', 'cookie'],

// keys or params to lookup language from
lookupCookie: 'lang',
lookupQuerystring: 'lng',

// cache user language on
caches: ['localStorage', 'cookie'],

// optional expire and domain for set cookie
cookieMinutes: 10080, // 7 days
}
});
}

Notice that we used the following loadPath:

loadPath: ‘assets/locales/{{lng}}.{{ns}}.json’,

This is the path that we’ll have for our locale data. Let’s create that folder now and add the missing translations.

➜ mkdir -p src/assets/locales/
➜ cd src/assets/locales
➜ touch en.translations.json
➜ touch el.translations.json

// el.translations.json
{
"message": "Καλως ήλθατε στο PhraseApp i18next"
}

// en.translations.json
{
"message": "Welcome to PhraseApp i18next"
}

If we run the application again and pass the right parameters, we will see the correct message:

It would be nice if we had a dropdown that we could use to switch locale. Let’s do that now:

First, let’s create our header component that will host the language dropdown…

➜ mkdir header
➜ touch header/header.component.html
➜ touch header/header.component.ts

Add the contents of the dropdown:

<div>
<select formControlName="languages" (change)="changeLanguage($event.target.value)">
<option *ngFor="let lang of languages" [value]="lang" [selected]="language === lang">
<a *ngIf="language !== lang" href="javascript:void(0)" class="link lang-item {{lang}}">{{ '_languages.' + lang | i18nextCap }}</a>
<span *ngIf="language === lang" class="current lang-item {{lang}}">{{ '_languages.' + lang | i18nextCap }}</span>
</option>
</select>
</div>

Then add the code for the changeLanguage handler:

import { ITranslationService, I18NEXT_SERVICE } from 'angular-i18next';
import { Component, ViewEncapsulation, Inject, OnInit } from '@angular/core';

@Component({
selector: 'header-language',
encapsulation: ViewEncapsulation.None,
templateUrl: './header.component.html',
})
export class HeaderComponent implements OnInit {

language = 'en';
languages: string[] = ['en', 'el'];

constructor(
@Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService
) {}

ngOnInit() {
this.i18NextService.events.initialized.subscribe((e) => {
if (e) {
this.updateState(this.i18NextService.language);
}
});
}

changeLanguage(lang: string){
if (lang !== this.i18NextService.language) {
this.i18NextService.changeLanguage(lang).then(x => {
this.updateState(lang);
document.location.reload();
});
}
}

private updateState(lang: string) {
this.language = lang;
}
}

We use the i18NextService to subscribe for initialised events and update the current state. On changeLanguage event we change the current locale and reload our current location. Before we run the application again, we need to make sure we register this component at the app.module.ts

import { HeaderComponent } from './header/header.component';
...

@NgModule({
declarations: [
AppComponent,
HeaderComponent . <--- Added
],

Finally, add the component to the app.component.html and run the app

Using Phrase Library Integrations to Handle Translation Files

Currently, as we’re not using the built-in i18n module that the framework provides, we need to be careful with managing the localization files. A lot of things can go wrong and ideally we need a library that will manage all of our translation projects and be in sync with any updates from external translators.

For that, we can use the Phrase CLI to integrate an advanced translation management system in our app and solve those issues. The process is as easy as one, two, three:

First, navigate to the Phrase CLI page and install it based on your current OS. Then go to the sign-up page and register for a free account if you don’t have one.

Before we interact with the API, we need to configure our client. Run the following command in your local shell:

➜ ./phraseapp init
PhraseApp.com API Client Setup

Please enter your API access token (you can generate one in your profile at phraseapp.com):
<token>

The <token> parameter is needed, and you can create one from your Account > Access Tokens. Once generated, enter in the input and you’ll be asked to select a project from the list or you can create a new one…

Loading projects...
1: Test (Id: 8fa47c48c3ba80aebe255e99651de3e4)
2: WP POT File Test (Id: 5ed97cfde5a2ac8bf4fbc6edd82eb4a9)
3: Handmade's Tale (Id: 40fe26fda781e98df0134cf91e02aea4)
4: Create new project

> 1

Next, you will be asked to specify the default language file formats that we are going to use for that project and their location. Select number 38 for i18next:

...
38: i18next - i18next, file extension: json
39: episerver - Episerver XML, file extension: xml
...
Select the format to use for language files you download from PhraseApp (1-39): 38

The next question requests the location of our locales. Enter the static folder path we used before:

Enter the path to the language file you want to upload to PhraseApp.
For documentation, see https://help.phraseapp.com/phraseapp-for-developers/phraseapp-client/configuration#push
Source file path: [default ./locales/<locale_name>/translations.json] ./src/assets/locales/<locale_name>.translation.json

Enter the path to which to download language files from PhraseApp.
For documentation, see https://help.phraseapp.com/phraseapp-for-developers/phraseapp-client/configuration#pull
Target file path: [default ./locales/<locale_name>/translations.json] ./src/assets/locales/<locale_name>.translation.json

We created the following configuration file for you: .phraseapp.yml

phraseapp:
access_token: <TOKEN>
project_id: <PROJECT_ID>
push:
sources:
- file: ./src/assets/locales/<locale_name>.translation.json
params:
file_format: i18next
pull:
targets:
- file: ./src/assets/locales/<locale_name>.translation.json
params:
file_format: i18next

For advanced configuration options, take a look at the documentation: https://help.phraseapp.com/phraseapp-for-developers/phraseapp-client/configuration
You can now use the push & pull commands in your workflow:

$ phraseapp push
$ phraseapp pull

Do you want to upload your locales now for the first time? (y/n) [default y] y
Uploading src/assets/locales/el.translation.json... done!
Check upload ID: 79199169613ae8ab80ec886801309d52, filename: el.translation.json for information about processing results.
Uploading src/assets/locales/en.translation.json... done!
Check upload ID: afdc9f81683ed209538f5461180779ff, filename: en.translation.json for information about processing results.
Project initialization completed!

Once we’ve finished with the configuration, we can pull the latest locales in our local environment and inspect our uploaded locales in the Phrase Dashboard:

We can also pull or sync the remote translations into our local project in one command:

➜ ./phraseapp pull 
Downloaded en to src/assets/locales/en.translation.json
Downloaded de to src/assets/locales/de.translation.json
Downloaded el to src/assets/locales/el.translation.json

Now, if you inspect the contents of the file, you’ll see the downloaded translations, for example, the en.translation.json will contain the following:

{
"general": {
"back": "Back",
"cancel": "Cancel",
"confirm": "Are you sure?",
"destroy": "Delete",
"edit": "Edit",
"new": "New",
"test": "Test"
},
"hello": "Hello world",
"layouts": {
"application": {
"about": "About",
"account": "Account",
"app_store": "App Store",
"imprint": "Imprint",
"logout": "Logout",
"my_mails": "My Mails",
"press": "Press",
"preview": "Preview",
"profile": "Profile",
"sign_in": "Login",
"sign_up": "Register"
}
},
"message": "Welcome to PhraseApp i18next",
"_languages": {
"el": "Greek",
"en": "English"
}
}

Now the process of translating content for our Angular applications can become more streamlined and agile.

Final Thoughts

Angular is a mature platform for developing first-class single-page applications. In terms of internationalization and localization, it has a built-in module and offers the option to integrate a custom solution such as i18next. This tutorial explained the steps required to use this popular plugin and showed how we can improve our workflow by using Phrase for managing our translations.

I hope you enjoyed this article. Stay tuned for more content related to those amazing technologies.

Originally published on The Phrase Blog.

--

--