Simple Language Translation in Angular 2 (Part 1)

Jecelyn Yeen
πŸ‘οΈ 130,228 views
πŸ’¬ comments

This is part one of a two part series series where we will learn about implementing translation in our Angular 2 application.

Introduction

In part 1 we will learn how to:

  1. Create a pipe that we can use to translate our words in the HTML view. Like this:
<!-- should display 'hola mundo' when translate to Spanish -->
<p>{{ 'hello world' | translate }}</p>
  1. Create a service that we can use to translate our words in JS / Typescript. Like this:
...
// should display 'hola mundo' when translated to Spanish
this.translatedText = this.translate.instant('hello world'); // this.translate is our translate service
...
  1. Ability to define multiple translations (e.g. English, Spanish, Chinese, etc), then set the current language.
...
this.translate.use('es'); // use spanish
...

This is how our UI will look:

Table of Contents

    The translation change when user select different language

    Requirements

    1. When a user clicks on the language buttons, translation should be updated accordingly.

    App Setup

    Here's our file structure:

    |- app/
        |- app.component.html
        |- app.component.ts
        |- app.module.ts
        |- main.ts
        |- translate/
            |- index.ts
            |- lang-en.ts
            |- lang-es.ts
            |- lang-zh.ts
            |- translate.pipe.ts
            |- translate.service.ts
            |- translation.ts
    |- index.html
    |- systemjs.config.js
    |- tsconfig.json

    Notes

    1. We created a translate folder under our app folder for all our translate related files.
    2. lang-[name].ts files are where we keep our translation definitions. In this example, we have English(en), Spanish(es) & Chinese(zh).
    3. translations.ts is where we concat all translations.
    4. translate.pipe.ts and translate.service.ts are the files for our service and pipe of course.
    5. index.ts is for barrel export. More on this later.

    Add Translation Definition

    Let's add some translations in our lang-[name] files.

    // lang-en.ts
    
    export const LANG_EN_NAME = 'en';
    
    export const LANG_EN_TRANS = {
        'hello world': 'hello world',
    };
    // lang-es.ts
    
    export const LANG_ES_NAME = 'es';
    
    export const LANG_ES_TRANS = {
        'hello world': 'hola mundo',
    };
    // lang-zh.ts
    
    export const LANG_ZH_NAME = 'zh';
    
    export const LANG_ZH_TRANS = {
        'hello world': 'δ½ ε₯½οΌŒδΈ–η•Œ',
    };

    Translations

    Now let's link all the translation files in our translation.ts file.

    // app/translate/translation.ts
    
    import { OpaqueToken } from '@angular/core';
    
    // import translations
    import { LANG_EN_NAME, LANG_EN_TRANS } from './lang-en';
    import { LANG_ES_NAME, LANG_ES_TRANS } from './lang-es';
    import { LANG_ZH_NAME, LANG_ZH_TRANS } from './lang-zh';
    
    // translation token
    export const TRANSLATIONS = new OpaqueToken('translations');
    
    // all translations
    const dictionary = {
        [LANG_EN_NAME]: LANG_EN_TRANS,
        [LANG_ES_NAME]: LANG_ES_TRANS,
        [LANG_ZH_NAME]: LANG_ZH_TRANS,
    };
    
    // providers
    export const TRANSLATION_PROVIDERS = [
        { provide: TRANSLATIONS, useValue: dictionary },
    ];

    Notes

    1. We imported all the translation definition files.
    2. We created an opaque token called translations. An opaque token is an object with no application interfaces. It's a special kind of provider lookup key for use in dependency injection. For more details, please refer to Angular official document.
    3. dictionary variable links all our translations.
    4. TRANSLATION_PROVIDERS notes that we use the opaque token that we defined earlier, and supply our dictionary as value. Later we will register TRANSLATION_PROVIDERS during bootstrap (main.ts).

    Translate Service

    Let's create our service.

    // app/translate/translate.service.ts
    
    import {Injectable, Inject} from '@angular/core';
    import { TRANSLATIONS } from './translations'; // import our opaque token
    
    @Injectable()
    export class TranslateService {
        private _currentLang: string;
    
        public get currentLang() {
            return this._currentLang;
        }
    
        // inject our translations
        constructor(@Inject(TRANSLATIONS) private _translations: any) {
        }
    
        public use(lang: string): void {
            // set current language
            this._currentLang = lang;
        }
    
        private translate(key: string): string {
            // private perform translation
            let translation = key;
    
            if (this._translations[this.currentLang] && this._translations[this.currentLang][key]) {
                return this._translations[this.currentLang][key];
            }
    
            return translation;
        }
    
        public instant(key: string) {
            // call translation
            return this.translate(key); 
        }
    }
    

    Notes

    • Note that we import our TRANSLATIONS token and inject it into our constructor.

    Translation Pipe

    Let's now create our translation pipe. Our pipe is simple - no logic in the pipe. We will import and call our translate service to perform the translation.

    // app/translate/translate.pipe.ts
    
    import { Pipe, PipeTransform } from '@angular/core';
    import { TranslateService } from '../translate'; // our translate service
    
    @Pipe({
        name: 'translate',
    })
    
    export class TranslatePipe implements PipeTransform {
    
        constructor(private _translate: TranslateService) { }
    
        transform(value: string, args: any[]): any {
            if (!value) return;
            return this._translate.instant(value);
        }
    }

    Use the Translation

    Both the translation service and pipe are now done. Let's use it in our app component!

    Import to App Module

    Before we can use the translation service and pipe, we need to import it to our app module.

    // app.module.ts
    
    import { NgModule }      from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    
    import { AppComponent }   from './app.component';
    import { TRANSLATION_PROVIDERS, TranslatePipe, TranslateService }   from './translate';
    
    @NgModule({
      imports:      [ BrowserModule ],
      declarations: [ AppComponent, TranslatePipe ], // Inject Translate Pipe here
      bootstrap:    [ AppComponent ],
      providers:    [ TRANSLATION_PROVIDERS, TranslateService ]
    })
    
    export class AppModule { }

    Notes

    1. We register our service and pipe in the application level.
    2. We make our translate pipe available globally (application wide). Please refer to this article.

    The App Component

    Then, we can use them in our component.

    // app/app.component.ts
    
    import { Component, OnInit } from '@angular/core';
    import { TranslateService } from './translate';
    
    @Component({
        moduleId: module.id,
        selector: 'app-root',
        templateUrl: 'app.component.html',
    })
    
    export class AppComponent implements OnInit {
    
        public translatedText: string;
        public supportedLanguages: any[];
    
        constructor(private _translate: TranslateService) { }
    
        ngOnInit() {
            // standing data
            this.supportedLangs = [
            { display: 'English', value: 'en' },
            { display: 'EspaΓ±ol', value: 'es' },
            { display: '华语', value: 'zh' },
            ];
    
            // set current langage
            this.selectLang('es');
        }
    
        isCurrentLang(lang: string) {
            // check if the selected lang is current lang
            return lang === this._translate.currentLang;
        }
    
        selectLang(lang: string) {
            // set current lang;
            this._translate.use(lang);
            this.refreshText();
        }
    
        refreshText() {
            // refresh translation when language change
            this.translatedText = this._translate.instant('hello world');
        }
    }

    Notes

    1. We have an array supportedLanguages to store all supported languages.
    2. By default, we set the language to Spanish. Of course, you can read the user's browser language navigator.language and set default language accordingly.
    3. When a new language is selected, we will refresh our translatedText. (We can do better. We will enhance the way we handle the refresh in part 2).

    The App HTML View

    Here is our HTML view.

    <!--app/app.component.html-->
    
    <div class="container">
        <h4>Translate: Hello World</h4>
        <!--languages-->
        <div class="btn-group">
            <button *ngFor="let lang of supportedLangs"
                (click)="selectLang(lang.value)"
                class="btn btn-default" [class.btn-primary]="isCurrentLang(lang.value)">
                {{ lang.display }}
            </button>
        </div>
        <div>
            <!--translate with pipe-->
            <p>
                Translate With Pipe: <strong>{{ 'hello world' | translate }}</strong>
            </p>
    
            <!--reanslate with service-->
            <p>
                Translate with Service: <strong>{{ translatedText }}</strong>
            </p>
        </div>
    </div>

    Hook them up

    Export barrel

    We will export our translate modules as barrel. A barrel is a way to rollup exports from several modules into a single convenience module. The barrel itself is a module file that re-exports selected exports of other modules. Refer to Angular official documentation for details.

    // app/translate/index.ts
    
    export * from './translate.service';
    export * from './translations';
    export * from './translate.pipe';

    Bootstrap

    Now let's bootstrap our application.

    // app/main.ts
    
    import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
    
    import { AppModule } from './app.module';
    
    const platform = platformBrowserDynamic();
    platform.bootstrapModule(AppModule);

    System config

    If you are using systemjs, and use Angular CLI for project setup, please include the translate barrel in your system-config.ts file. Like this:

    // system.config.ts
    
    // App specific barrels.
      'app',
      'app/translate',  // include this line
      'app/shared',
      /** @cli-barrel */

    Almost done, but...

    Let's run your application. When the application loads, Spanish language is selected, and the translation shows hola mundo as expected.

    Try to select English now. You should see that the Translate with pipe value is not updated.

    Value not updated

    Why

    Angular 2 Pipe is pure by default. Angular executes a pure pipe only when it detects a pure change to the input value. In our case, the input value didn't change, it's still Hello world. Therefore, the value is not updated.

    Solution

    Let's make our pipe impure. Angular executes an impure pipe during every component change detection cycle. In our case, when we update the language selection, change will be triggered, and the impure pipe will update accordingly.

    To make our pipe impure, open app/translate/translate.pipe.ts and add in one line:

    // app/translate/translate.pipe.ts
    ...
    
    @Pipe({
        name: 'translate',
        pure: false // add in this line, update value when we change language
    })
    
    ...

    Please refer to Angular 2 Pipes documentation for details on Pipe.

    Summary

    Yay, we have implemented our own translation. We will cover more advanced features in part 2, coming soon:

    1. Enable a placeholder for translation.
    2. Publish and subscribe to a language change event.
    3. Enable default language and fallback language.

    That's it. Happy coding.

    References:

    1. angular-translate/angular-translate: https://github.com/angular-translate/angular-translate
    2. ocombe/ng2-translate: https://github.com/ocombe/ng2-translate
    3. Valetudox/angular2-translate: https://github.com/Valetudox/angular2-translate

    Jecelyn Yeen

    22 posts

    Coder. Diver. Board Game Lover.

    Speak English, Mandarin, JavaScript, Typescript, C# and more.

    GDE | Angular | Web Technologies.