[Angular] Ngx-Translate – Lazy Loading, Pluralization, Caching, Unit Test, Events and More!

In Angular, we already get basic internationalization built-in support, ngx-translate provides much more features with robust and flexible solutions to fulfil translation requirements in the application.

In this detailed guide, we will walk through the basic to advanced implementation of ngx-translate module including lazy-loading, language switch callback events, pluralization, caching, creation of unit tests etc. with easy-to-follow examples.

By using the ngx-translate we can define translations in JSON as well as other formats like XLIFF and Gettext and dynamically switch languages at runtime. Following are some of the key features and benefits of ngx-translate module:

  • Simple and Easy-to-use API for translating text
  • Support for lazy loading translations
  • Pluralization and interpolation
  • Caching of translation files
  • Language change events and persistence
  • Extraction of translatable strings
  • Easy integration testing

 

This tutorial will help you get started with NGX Translation integration and then we will touch on some more advanced use cases with examples:

 

Getting Started with ngx-translate

In our Angular project, we will start by installing the ngx-translate with its http-loader module for loading the translations over HTTP calls.

Execute the following command to install these packages via npm:

npm install @ngx-translate/core @ngx-translate/http-loader --save

Next, we need to import the TranslateModule into the root AppModule:

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';

import {AppComponent} from './app.component';

// Import ngx-translate modules
import {TranslateModule, TranslateLoader} from '@ngx-translate/core';
import {TranslateHttpLoader} from '@ngx-translate/http-loader';
import {HttpClientModule, HttpClient} from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,

    // ngx translate modules
    HttpClientModule,
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: HttpLoaderFactory,
        deps: [HttpClient]
      }
    })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

// Required for AOT compilation
export function HttpLoaderFactory(http: HttpClient) {
  return new TranslateHttpLoader(http);
}

This imports the required modules and configures the TranslateHttpLoader to load translation files.

Now, we will create a simple translation file called en.json:

{
  "HELLO": "Hello!"  
}

In app.component.ts, we can set the default language and retrieve the translation:

import {TranslateService} from '@ngx-translate/core';

@Component({
  // ...
})
export class AppComponent {

  constructor(private translate: TranslateService) {
    // Set default language
    translate.setDefaultLang('en'); 
    
    translate.get('HELLO').subscribe(res => {
      console.log(res); // Hello!
    })
  }

}

We have covered the basic integration steps including how to create basic translation en JSON file, set the default language and load that value in the App Component using the subscription.

 

Changing Languages at Runtime

One of the core features of ngx-translate is to change languages at runtime. We can manually change the language by calling the use() method:

this.translate.use('fr');

We can also subscribe to the onLangChange event to detect when the language has been changed:

this.translate.onLangChange.subscribe((lang) => {
  // Language changed to lang
});

Generally, we show a dropdown selection using which a user can change the language translation on runtime as shown below:

<select #langSelect (change)="translate.use(langSelect.value)">
  <option value="en">English</option>
  <option value="fr">French</option>
</select>

When the user selects a different language from the dropdown, we trigger the translate.use() method to change the current translation.

We can also get the current selected language by calling the currentLang property:

const current = this.translate.currentLang;

This way we can change the translation on the fly at runtime, detect language changes using a subscription, and get to know the current language.

 

Lazy Loading Translations

In NGX Translate, by default, all the translation files are loaded on application startup. But when the application size and its number of modules increase in size, it becomes very inefficient to load all the non-required resources.

To optimize this default behaviour, we can load the translation file lazily as demanded by modules.

To achieve this, we need to implement a custom loader that will extend the TranslateLoader:

import {TranslateLoader} from '@ngx-translate/core';
import {HttpClient} from '@angular/common/http';
import {Observable, of} from 'rxjs';

export class CustomLoader implements TranslateLoader {

  constructor(private http: HttpClient) {}

  getTranslation(lang: string): Observable<any> {
    return this.http.get<any>('/assets/i18n/'+lang+'.json')
      .pipe(
        catchError(error => {
          // Handle error
          return of(null); 
        })
      );
  }
}

Here are calling the getTranslation method to fetch the translation lazily on request via HTTP get call.

Now in AppModule, we provide our loader as showb below:

@NgModule({
  // ...
  
  imports: [
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useClass: CustomLoader
      }
    })
  ]
})

By using the lazy loader, the translations will be fetched for the current language which will ensure quick startup time and efficient applications.

 

Pluralization

Various languages generally have complex rules for generating plurals but Ngx-translate can handle such requirements by defining the plural cases as below:

{
  "APPLES": "There is one apple|There are many apples"
}

The pipe-delimited cases handle singular and plural forms. This is how we can fetch the correct plural cases in our code:

import {TranslateService} from '@ngx-translate/core';

@Component({
  // ...
})
export class AppComponent {

  constructor(private translate: TranslateService) {
    const count = 4;
    translate.get('APPLES', {count}).subscribe(translation => {
      // translation = "There are many apples"
    })
  }
}

The pluralization is handled automatically by ngx-translate based on the count parameter as we passed above.

We can have as many as plural forms as needed for the language:

{
  "APPLES": "One apple|Few apples|Many apples"  
}

 

Interpolation

By using the Interpolation, we can dynamically insert the values into existing translations:

{
  "HELLO_USER": "Hello {{name}}!"
}

This way we can retrieve an interpolated translation and pass the dynamic value into the translation it self by passing its value as below:

translate.get('HELLO_USER', {name: 'John'}).subscribe(res => {
  // Hello John!
})

This way we can avoid the need to have separate translations for each user name like in our case.

We can use both pluralization and interpolation together as below:

{
  "NEW_MESSAGES": "You have {{count}} new message|You have {{count}} new messages"
}

This is how we can handle both pluralization and interpolation at once to provide a single text:

  constructor(private translate: TranslateService) {
    const messageCount = 5;
    this.translate.get('NEW_MESSAGES', {count: messageCount})
      .subscribe((translation) => {
        console.log(translation);
        // Logs: "You have 5 new messages" 
      });
  }

 

Caching and Preventing Duplicate Requests

Yes, we can also cache translation files to reduce the network calls for fetching the same translation files over HTTP calls which can greatly improve the performance of the application. The Ngx-translate package provides built-in caching options:

@NgModule({
  imports: [
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useClass: CustomLoader
      },
      cache: true, // enable cache
      cacheLimit: 10, // max files to cache
      useExpired: true, // use expired cache
      maxAge: 60000 // cache age
    })
  ]
})

By adding these properties we can easily control the cache storge behaviour which is saved inside the memory itself.

We can also achieve a cache mechanism on our customer translate loaders as well:

// custom-loader.ts

import {TranslateLoader} from '@ngx-translate/core';
import {HttpClient} from '@angular/common/http';
import {Observable, of} from 'rxjs';
import {tap, shareReplay} from 'rxjs/operators';

export class CustomLoader implements TranslateLoader {

  private translations: {[lang: string]: Observable<any>} = {};

  constructor(private http: HttpClient) {}

  getTranslation(lang: string): Observable<any> {

    // Check if translation exists in cache
    if (this.translations[lang]) {
      return this.translations[lang];
    }

    // Fetch from backend
    return this.http.get<any>('/assets/i18n'+lang+'.json')
      .pipe(
        tap(translation => {
          // Save to cache
          this.translations[lang] = of(translation).pipe(
            shareReplay(1)
          );
        }),
        catchError(error => {
          // Handle error
          return of(null);
        })
      );
  }
}

Here we are checking if the translation file exists in the cache, if it’s there then we serve the file from the cache itself otherwise we fetch it from the actual backend server. Cacheing can greatly reduce server loads when there are a huge number of instances running on the enterprise level of applications.

 

Organizing Translation Files

When the application grows, we may need to organize translations across multiple files. In such case, we can group translation by feature module as shown below:

/src
  /app
    /core
    /settings
    /user
  /assets
    /i18n
      core.json
      settings.json
      user.json

The TranslateHttpLoader can automatically load translation files named after the module:

export function HttpLoaderFactory(http: HttpClient) {
  return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}

Also, the regional variants of languages such as fr-CA can be separated into distinct files as well.

 

Automating Extraction

We can use tools to extract keys from the source code as manually updating translation files becomes error-prone. We can execute the following @ngx-translate/core extract package by running CMD command:

ngx-translate-extract --input ./src --output ./translations --format=json --clean

It will scan the source code for translation and output the .json files. We can easily automate this process by integrating extraction into the CI/CD pipeline to keep translations in sync.

Persisting Locale

We can also persist the user’s language selection across sessions by saving the preference in the browser’s local storage. This is how we can set and get the selected language in the local storage under config as shown below:

// Get initial language
export function getInitialLanguage() {
  const language = localStorage.getItem('language');
  return language || 'en';
}

// Save language
translate.onLangChange.subscribe((event) => {
  localStorage.setItem('language', event.lang);
});

On app load, we can fetch the saved or local storage saved language preference inside the main.ts file as shown below:

// main.ts
import {getInitialLanguage} from './config';

translate.setDefaultLang(getInitialLanguage());

 

Testing Translations

Now we will discuss how to implement testing and unit tests to prevent issues early and prevent bugs. Here are some best practices for testing with ngx-translate:

 

Simulating Language Changes

Here is how we can test language changes and retrieve translations to easily simulate language changes in tests. This will validate if translations are loading correctly and they are switching correctly.

// Set initial
translate.use('en'); 

// Test translation
expect(translate.instant('HELLO')).toEqual('Hello');

// Switch language
translate.use('fr'); 

// Test translation  
expect(translate.instant('HELLO')).toEqual('Bonjour');

 

Snapshot Testing

We can also perform snapshot testing, which proves a great technique for testing translation files. The idea is to take a “snapshot” of the file and co0mpare with future changes:

import * as fr from './fr.json';

it('should match snapshot', () => {
  expect(fr).toMatchSnapshot(); 
});

for example, if the French file changes, the test will fail and show the new snapshot. Otherwise, we can review and update the snapshot if it’s an intentional change. The whole purpose of snapshot testing is to ensure translations don’t get modified by accident.

 

Validating Plurals and Interpolation

We can also put a test for pluralization and interpolation to make sure it works correctly. We can pass different count parameters and validate plurals as below:

expect(translate.instant('APPLES', {count: 1})).toEqual('One apple');
expect(translate.instant('APPLES', {count: 5})).toEqual('Many apples');

For testing interpolation, we can pass dynamic values:

expect(translate.instant('HELLO_USER', {name: 'John'})).toEqual('Hello John');

This will help to catch issues with pluralization and interpolation cases in our application.

 

Conclusion

We can discuss various parts of using the NGX translate module in the Angular application. We looked on the following topic with examples from a beginner level to advanced:

  • API for managing translations
  • Support for lazy loading modules
  • Pluralsinterpolation, and caching
  • Language persistence and events
  • Best practices for extraction and testing

Leave a Comment

Your email address will not be published. Required fields are marked *