Angular’s official internationalization(i18n) support

In the previous blog post series, we have seen how to achieve the internationalization support using ngx-translate library. Please read this post if you didn’t read yet. In this blog post, you will see how to achieve it through Angular’s official i18n tools.  “Internationalization is the process of designing a software application so that it can potentially be adapted to various languages and regions without engineering changes”. Angular uses ngx-i18n tools to support internationalization. The process involves four steps.

  1. Identify the messages: Initially mark the static text messages in your component templates.
  2. Generate standard translation files: Extracts the marked messages into an industry standard translation source file(XLIFF and XMB formats).
  3. Translating the messages: Translating the extracted text messages into the target language by Translator tool.
  4. Complete the translation files: Angular compiler imports the compiled translation files and replace the original text with translation text files. Finally it creates translation files based target language.

The demo example is powered by PrimeNG components and the internationalization support for German and Spanish languages. You can close the Github URL to play with the features.

1.  Identify the messages:

Let’s see how we can identify the messages in different possible ways. 

 i.Mark messages with the i18n attribute:

Angular provides i18n custom directive to identify the messages which needs to be translated. With the help of this attribute, angular compilers and tools can easily find and mark them for translation. The i18n attribute removed by the compiler after translation.

For example, you can just append i18n attribute to the label to make it available for translation,

<label i18n>
   Enter your faviourite Framework
</label>

To make the translation accurately, it is good idea to define meaning and description for the attribute. This helpful to define the particular context of the message while doing translation. Both of these options are optional. Lets define this to span element an example,

<span i18n="Favourite framework| Display the output of favourite framework">
    The selected framework is {{framework}}
</span>

The i18n extraction tools identify the above messages (with the help of i18n markers) then generates  translation files with translation units.  The generated id for each unit with a random unique text as follows. 

<trans-unit id="ba0cc104d3d69bf669f97b8d96a4c5d8d9559aa3" datatype="html">
 ...
</trans-unit>

If you edit generated id then there is more chance of loosing the translations for messages. To avoid these problems, it is good idea to define our custom ids as below.

<span i18n="Favourite framework| Display the output of favourite framework@@favouriteFramework">
   The selected framework is {{framework}}
</span>

Note: The i18n attribute format can be defined as meaning | description | id .

Finally the complete translation unit with custom id values would be as follows.But remember that duplicate id creates confusion and generates translation units with same message.

<trans-unit id="favouriteFramework" datatype="html">
       <source>The selected framework is <x id="INTERPOLATION"/></source>
      <target>Das ausgewählte Framework ist <x id="INTERPOLATION"/></target>
      <context-group purpose="location">
           <context context-type="sourcefile">src/app/app.component.ts</context>
           <context context-type="linenumber">9</context>
      </context-group>
      <note priority="1" from="description"> Display the output of favourite framework</note>
      <note priority="1" from="meaning">Favourite framework</note>
</trans-unit>

You can also create the translation for messages without creating an element in the DOM. You can achieve this in two possible ways.

  1. Use ng-container tag which is used as placeholder without rendering in the DOM.
<ng-container i18n>
   A leading UI Component library
</ng-container>

   2. Use comments with optional meaning and description

<!--i18n: optional meaning|optional description -->
   <br>The current version of PrimeNG is 4.1<br>
<!--/i18n-->

ii. Mark messages with the i18n translation attributes:

In the above section, you have seen how the DOM contents can be translated using i18n attribute. You can also translate element attribute’s text content. For example, title of button element. 

<button pButton styleClass="align-header" i18n-title title="Click me for more details" icon="fa-check">
</button>

Note: The i18n translation attributes should be in the format as i18n-x where x is the attribute of DOM element.

iii. How to handle Singular and Plural criteria:

Each language has specific pluralization rules. We need to handle this pluralization intelligently in the internationalization implementation. Angular handles this  very elegantly in an easy way.  It handles the text content based on number of elements or cardinality. For instance, the framework rating with different possibilities as an example,

<span i18n="@@ratinginput">
  {angular, plural, =0 {No rating} =1 {One star rating} =2 {Two star rating} =3 {Three star rating} other {Very good}}
</span>

In the above example, first parameter indicates the component property, second parameter used to categorize singular or plural type and third parameter indicates pluralization pattern which consists of category and it’s matching value.

The possible Pluralization categories from Angular include:

  • =0 (or any other number)
  • zero
  • one
  • two
  • few
  • many
  • other

iv. How to handle selection expressions:

Similar to pluralization concept, the text selection with different output possibilities requires a special treatment. The select alternative texts based on user selection would be handled as below

<span i18n>
   The selected gender is {gender, select, a {male} f {female}}
</span>

The first parameter indicates the compoent property, second parameter specifies select keyword and third one should be pair of possible value and it’s text representation.

2. Generate standard translation files

Now It is time generate translation files for the defined messages in templates.The ng-xi18n extraction tool  is used to extract the i18n-marked texts into a translation source file in an industry standard formats such as XML Localization Interchange File Format (XLIFF, version 1.2) and XML Message Bundle (XMB).You need to run any one of the below command to generate message.xlf (or xmb) in your project root folder.

./node_modules/.bin/ng-xi18n --i18nFormat=xlf --outFile=messages.xlf
./node_modules/.bin/ng-xi18n --i18nFormat=xlf2 --outFile=messages.xliff2.xlf
./node_modules/.bin/ng-xi18n --i18nFormat=xmb --outFile=messages.xmb

For a better convenience, lets define the npm script as below,

"scripts":
    { "i18n": "./node_modules/.bin/ng-xi18n",

     ... }

Now you can run and generate the translation files easily,

npm run i18n --i18nFormat=xlf --outFile=messages.xlf
npm run i18n --i18nFormat=xlf2 --outFile=messages.xliff2.xlf
npm run i18n --i18nFormat=xmb --outFile=messages.xmb

At the end,  you can find message.xlf file in  your project root folder which defines translation units for each message.

3. Translating the messages

The ngx-i18n tool generated the English version of translation file by default. Now we need to create translation messages for specific target language. First we need to create locale folder in the project root folder and copy the message file by appending language code for the extension (for example, messages.de.xlf and messages.es.xlf).

Now you need to open the message translation files and edit the text nodes (especially target nodes).The translation unit with simple i18n attribute and German translation would be as follows,

<trans-unit id="d58a0b634acf483a0b48cf9715066f2cbb4cd8bf" datatype="html">
    <source>Enter your faviourite Framework</source>
    <target>Geben Sie Ihren faviourite Framework ein</target>
    <context-group purpose="location">
      <context context-type="sourcefile">src/app/app.component.ts</context>
      <context context-type="linenumber">6</context>
    </context-group>
</trans-unit>

The same way it handles plurals and select messages which are defined in the translation files. In real world, there will be official translators for message conversion.

4. Complete the translation files

The JIT compiler involves the below steps to reflect the changes in the browser

  1. Determining the language version for the current user and load translation file using text plugin. Configure this in your index.html file.
// Get the locale id
 document.locale = 'es';
// Map to the text plugin
System.config({ map: { text: 'systemjs-text-plugin.js' } });

Because of the SystemJS project setup, we used systemjs-text-plugin.js to read text files in this demo example.

2. Importing the appropriate language translation file as a string constant in SystemJS plugin. Configure this in systemjs-text-plugin.js file.

exports.translate = function (load) { 
          if (this.builder && this.transpiler) { 
                load.metadata.format = 'esm'; 
                return 'exp' + 'ort var __useDefault = true; 
                exp' + 'ort default ' + JSON.stringify(load.source) + ';'; 
          } 
               load.metadata.format = 'amd'; 
               return 'def' + 'ine(function() {\nreturn ' + JSON.stringify(load.source) + ';\n});'; 
}

3. Creating corresponding translation providers to guide the JIT compiler.

 The below Three providers tell the JIT compiler how to translate the template messages for a particular language while compiling the application

The getTranslationProviders() function in the following src/app/i18n-providers.ts creates those providers based on the user’s locale and the corresponding translation file:

import { TRANSLATIONS, TRANSLATIONS_FORMAT, LOCALE_ID, MissingTranslationStrategy } from '@angular/core';
import { CompilerConfig } from '@angular/compiler';

export function getTranslationProviders(): Promise<Object[]> {

// Get the locale id from the global
const locale = document['locale'] as string;

// return no providers if fail to get translation file for locale
const noProviders: Object[] = [];

// No locale or U.S. English: no translation providers
if (!locale || locale === 'en-US') {
return Promise.resolve(noProviders);
}

// Ex: 'locale/messages.es.xlf`
const translationFile = `./locale/messages.${locale}.xlf`;

return getTranslationsWithSystemJs(translationFile)
.then( (translations: string ) => [
{ provide: TRANSLATIONS, useValue: translations },
{ provide: TRANSLATIONS_FORMAT, useValue: 'xlf' },
{ provide: LOCALE_ID, useValue: locale },
])
.catch(() => noProviders); // ignore if file not found
}

declare var System: any;

function getTranslationsWithSystemJs(file: string) {
return System.import(file + '!text'); // relies on text plugin
}

4. Bootstrapping the application with providers.

The Angular bootstrapModule() method has a second options parameter that can influence the behavior of the compiler.You’ll create an options object with the translation providers from getTranslationProviders() and pass it to bootstrapModule. Open the src/main.ts and modify the bootstrap code as follows:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { getTranslationProviders } from './app/i18n-providers';

import { AppModule } from './app/app.module';

getTranslationProviders().then(providers => {
const options = { providers };
platformBrowserDynamic().bootstrapModule(AppModule, options);
});

The project structure after the entire setup would be as follows

The German version of this demo example appeared as below. Use npm start script to run on your dev server. You can change the locale to ‘es’ in your index.html to see the Spanish version.

The app is now internationalized for English, German and Spanish but we can implement this for many more languages.

Conclusion:

The official angular support for internationalization is improved a lot during the last few months. The support enhanced with pluralization, select and nested expressions. You need follow all the above mentioned steps to make your app with multi language feature to get user experience for multiple regions.

Leave a Reply

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