Software localization

Angular Tutorial on Localizing with I18next

Make Angular localization your own and learn how to implement multilingual support for your Angular applications with the help of i18next.
Software localization blog category featured image | Phrase

We've been dealing with Angular localization for quite a while now, not only in terms of localizing with the built-in i18n module but also with third-party Angular libraries for internationalization.

If i18next is your favorite library, and you don't want to switch to any other framework than Angular, there is an option to integrate it into your Angular project with the help of a community-maintained plugin.

This article will guide you through the Angular localization process with i18next step by step 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 translations with the cloud and improving the workflow

By the way, all the code used in this tutorial is 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.

App printing key string | Phrase

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:

App with correct and localized message | Phrase

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

Demo app with language switcher | Phrase

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:

Phrase Dashboard | Phrase

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.