General

The ngx-translate library is used for internationalization. This library basically provides functionality for replacing keys (as placeholders for the text to be translated) in HTML or TypeScript files with values (the actual translations) defined as key-value pairs in separate JSON files (one for each language to be supported). The JSON files (in UTF8 character encoding) should be placed in the src/assets/i18n directory. It is suggested the 2-letter ISO language codes be used for the file names, the file name extension “json” is mandatory.

Key/value replacement is performed at runtime of the web application, and it is possible to switch between languages at runtime (see below).

For details, please refer to the ngx-translate documentation at the link above.

Recommendations

The keys for the texts to be translated should follow the pattern

<directory>.<filename>.<descriptive key>

where directory is the directory within the app folder containing the respective file, filename the root file name (omitting the file name extension and qualifiers such as “component” or “module”), and descriptive key a name for the key preferably so chosen as to give some indication about its usage or meaning. Key names should be in English, with dashes (-) replacing spaces between words. For clarity, consider using prefixes such as “title” for dialog titles, “btn” for button texts, “lbl” for labels, “tooltip” for tooltip texts, “err” for error messages, etc.

Examples:

  • search.mdm-search.btn-apply-changes
  • details.sensor.err-cannot-load-descriptive-data

For the sake of uniformity, it is recommended to use the translate pipe in HTML files:

<h4 class="modal-title">{{ 'tableview.editview.title-view-editor' | translate }}</h4>

Using the TranslateService.instant() method, a translation for a key can be obtained in the language currently selected at the time of the method call. A typical application scenario for this is a message etc. shown in response to an event triggered by user action (e.g., a button click):

saveBasket(e: Event) {
    if (e) {
      e.stopPropagation();
    }
    if (this.baskets.find(f => f.name === this.basketName) != undefined) {
      this.childSaveModal.hide();
      this.overwriteDialogComponent.showOverwriteModal(
        this.translateService.instant('basket.mdm-basket.item-save-shopping-basket')).subscribe(
          needSave => this.saveBasket2(needSave),
          error => {
            this.saveBasket2(false);
            this.notificationService.notifyError(this.translateService.instant('basket.mdm-basket.err-save-shopping-basket'), error);
        });
    } else {
      this.saveBasket2(true);
    }
  }

A function streamTranslate is defined in mdm-core.module.ts which accepts a translation key and returns an Observable:

export function streamTranslate(translateService: TranslateService, keys: string | Array<string>, params?: any): Observable<any> {
  return translateService.onLangChange.pipe( startWith({}),
      switchMap(() => params ? translateService.get(keys, params) : translateService.get(keys)) );
}

By subscribing to that observable, a change of language in the application can be detected and the current translation for the key passed can be obtained. This is especially useful for class variables containing text to be localized, which is then guaranteed to always be in the language currently selected. Example (see below for an explanation of the marker function TRANSLATE):

import { TranslateService } from '@ngx-translate/core';
import { streamTranslate, TRANSLATE } from '../core/mdm-core.module';

export class MDMNavigatorComponent implements OnInit {

  loadingNode = <TreeNode>{
    label: 'Loading subordinate items...',
    leaf: true,
    icon: 'fa fa-spinner fa-pulse fa-fw'
  };
  
  [...]

  contextMenuItems: MenuItem[] = [
    { label: 'Add to shopping basket', icon: 'fa fa-shopping-cart', command: (event) => this.addSelectionToBasket() }
  ];

  constructor(private translateService: TranslateService) {
  }

  ngOnInit() {
    streamTranslate(this.translateService, TRANSLATE('navigator.mdm-navigator.loading-subordinate-items')).subscribe(
            (msg: string) => this.loadingNode.label = msg);
    streamTranslate(this.translateService, TRANSLATE('navigator.mdm-navigator.add-to-shopping-basket')).subscribe(
            (msg: string) => this.contextMenuItems[0].label = msg);
  }
  
  [...]
}

Please also note that a suitable default value should be provided for the text if there is the possibility of it being needed before the translation library has been fully initialized. These default texts should be in English.

Language selection in the web application

The user can select the language for the UI from a list presented in the web application's main menu bar. The entries for this list are defined in the languages array of the AppComponent class. For each language, a SelectItem must be added to this array (label: the name of the language in that language itself, value: the name of the JSON file with the translations, excluding the “json” extension):

export class AppComponent implements OnInit {
  [...]
 
  languages = <SelectItem[]> [
    { label: 'English', value: 'en' },
    { label: 'Deutsch', value: 'de' },
    { label: 'Français', value: 'fr' },
  ];
  
  [...]
}

Extracting translation keys from source files

Using the ngx-translate-extract library, translation keys can be automatically extracted from the source files and collected in a JSON file for use with ngx-translate. Documentation for ngx-translate-extract is available at the link above.

A script (extract-translations) which extracts the keys to two files (en.json and de.json for English and German, respectively) has been defined in package.json and can be invoked on the command line by npm run extract-translations.

"extract-translations": "ngx-translate-extract --input ./src --output ./src/assets/i18n/en.json ./src/assets/i18n/de.json --clean --sort --format namespaced-json --marker TRANSLATE"

In this script, a marker function (TRANSLATE, defined in mdm-core.module.ts) is specified for marking translation keys outside the methods provided by ngx-translate. Whenever a translation key is used in an ngx-translate method (such as instant()) or in conjunction with the translate pipe or directive, ngx-translate-extract automatically extracts that key to the JSON file. Keys located elsewhere in a typescript file must, if they are to be included in the JSON file, be marked using a marker function, which should not do anything but return the string passed into it:

export function TRANSLATE(str: string) {
  return str;
}

At each extraction run, new keys found in the sources are added to the JSON file(s), keys no longer used in the source files removed from the JSON file(s), and all other keys in the JSON file(s) left untouched, together with any already existing translations for these.

The script can, of course, be modified as needed. Please note that some of the ways to specify the target JSON files mentioned in the ngx-translate-extract documentation appear to be non-functional (e.g., the {en,de}.json notation for creating two files, en.json and de.json).