Skip to content

README Node.js angular.multi-language

This multi-language sample is an Angular application to demonstrate the use of localization files in ctrlX apps.

Introduction

We use the NGX-Translate library to localize our webapps. Details of this library can be found here: http://www.ngx-translate.com/

The basic concept of localization is to extract localizable texts from HTML and Type Script and to put them into language specific localization files. At runtime localized texts are loaded from localization files depending on the currently selected language and displayes within the UI.

Localization Files

Concept and Notation

All texts to translate are stored in json localization files. Localization files are loaded at runtime from: https://{ip}/assets/i18n/

  • To ensure that several localization files can be located in the assets directory in parallel, they have to comply to the following naming convention: webAppName.(lang-ISO_639-1-code).json
  • Localization files contain localizable texts for exactly one language and for exactly one webapp.
  • For ISO codes refer to https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes

Example:

myApp.en.json
myApp.de.json

Content and structure

Localization files are basically key/values lists. Each key/value pair represents one localizable string.

The basic format of a key/value entry is:

"<key>": "<translated text>"

Key naming convention

Keys have to comply the following convention to avoid conflicts when several localization files are loaded simultaneously at runtime:

(webApp id).(component).(element)[.type]
  • webapp id: to make keys unique across all webapps and id has to be part of a key (e.g. "plc", "wrk", "frw" ,"idm"...)
  • component: denotes the building block (angular component, service etc.) within a webapp where the translatable text is located (e.g. login-dialog,control-details,common-texts...)
  • element: denotes the ui-element within a component on which the text is placed (e.g. connectionfailed, userimage, acceleration...)
  • type: denotes the type of the ui-element. This gives translators additional information that is relevant for translation. It is only required for the certain ui-element types
title  (page / dialog titles)
header (column headers etc.)
tab (tab page titles)
menu (sidebar menus and other menus)
sub menu (sidebar menus and other menus)
button (button text)
tooltip (tooltip text max length?)

Localization file formats

There are two formats of localization files: the flat format and the namespaced format.

Flat format

In flat format each key/value pair corresponds to one line within the json file.

The flat format is the straight forward approach that can be used especially in small webapps that do not have much translatable text. This is quite simple. Though with increasing file length it becomes difficult to keep overview about the file content.

Example

{
  "frw.configStartPage.packetFlowOverview": "Packet flow overview",
  "frw.configStartPage.configurePacketFiltering": "Configure packet filtering",
  "frw.configStartPage.configureNat": "Configure network address translation"
}

or

{
  "wrk.settings.title": "Setting",
  "wrk.settings.help.header": "Help",
  "wrk.settings.control-emulation.header": "Virtual control emulation",
  "wrk.settings.control-emulation.tooltip": "Only evaluated when starting a control",
  "wrk.settings.control-emulation": "Show the emulation process window for a running ctrlX COREvirtual"
}

Namespaced format

To add structure to extensive localization files the namespaced format can be used.

In this format the effective key (e.g. "frw.configStartPage.packetFlowOverview") will be composed of the namespace (e.g. "frw.configStartPage) and the key within the namespace (e.g. "packetFlowOverview"). This means that both formats are equivalent and represent the same localization information.

Example:

{
  "frw.configStartPage": {
      "packetFlowOverview": "Packet flow overview",
      "configurePacketFiltering": "Configure packet filtering",
      "configureNat": "Configure network address translation"
    }
}

or

{
  "wrk.settings": {
  "title": "Setting",
  "help.header": "Help",
  "control-emulation.header": "Virtual control emulation",
  "control-emulation.tooltip": "Only evaluated when starting a control",
  "control-emulation": "Show the emulation process window for a running ctrlX COREvirtual"
}

Important

Only add texts to be translated in the file. Texts that should not be translated do not belong in the translation file. It is not possible to add comments into to a translation file.

Responsibilites of webapps regarding localization

WebApps

  • are responsible to comply naming conventions for localization files
  • are responsible to transfer the localization files of their used weblibs to their own assets folder (angular.json => glob)
  • are responsible to load their own localization files as well as the localization files of their used weblibs (app.module.ts => TranslationModule, MultiTranslateHttpLoader)
  • are responsible to set the current language as well as the fallback language

How to make webapps localizable

Install ngx-translate and ngx-translate-multi-http-loader

Use the following commands to install ngx-translate and ngx-translate-multi-http-loader:

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

Add an assets/i18n folder to your webapp project

Add the assets-folder within the src-folder in webapps, as listed below:

|- <webapp-name>
    |- src              <= sources are here
        |- assets
        |   |- i18n    <= localization files are located here
        |       |- <webapp-name>.en.json
        |       |- <webapp-name>.de.json|
    |- app ...

Add the assets folder of weblibs used in the webapp to the assets array in angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  ...
  "projects": {
    "webapp.<app-name>": {
    ...
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/<app-name>",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "baseHref": "/webapp.<app-name>/",
            ...
            "assets": [
              "src/assets"
            ],
            styles": [
              "src/styles.scss"
            ],
            "scripts": []
          },
          ...
}

Add the TranslateModule to app.module.ts

import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { MultiTranslateHttpLoader } from 'ngx-translate-multi-http-loader';

export function createMultiTranslateHttpLoader(http: HttpClient): TranslateLoader {
  return new MultiTranslateHttpLoader(http, [

//This is how it goes for apps that uses lazy loaded modules (example: firewall app)
{ prefix: './<webapp-name>/assets/i18n/<webapp-name>.', suffix: '.json' },   <= add this for the webapp
{ prefix: './<webapp-name>/assets/i18n/<weblib-name.', suffix: '.json' },    <= add such a line for each weblib used in the webapp

//This is how it goes for apps that does NOT use lazy loaded modules (example: webapp.motion)
{ prefix: './assets/i18n/<webapp-name>.', suffix: '.json' },                 <= add this for the webapp
{ prefix: './assets/i18n/<weblib-name.', suffix: '.json' },                  <= add such a line for each weblib used in the webapp
]);
}

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ...,
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: createMultiTranslateHttpLoader,
        deps: [HttpClient]
      },
      defaultLanguage: 'en'
    }),
    ...

Note

Note that in webapp modules TranslateModule.forRoot() must be called. Note that English (en) is defined as defaultLanguage. That means English is the fallback language if a translation is not available in the currently selected language.

Add the TranslateModule to lazy-loaded feature modules in {feature-name}.module.ts

import { HttpClientModule} from '@angular/common/http';
import { TranslateModule } from '@ngx-translate/core';

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...,
    HttpClientModule,
    TranslateModule.forChild({
      extend: true
    }),
    ...
  ],

Note

Note that in feature modules TranslateModule.forChild() must be called.

Please see details here: https://github.com/ngx-translate/core

Add the TranslateModule for unit tests

Because of injected translate service it is necessary to modify unit tests of app.component (app.component.spec.ts) and other components ({other}.component.spec.ts).

It is also very important to make the change in the "app-routing.module.spec.ts" as well.

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

describe('AppComponent', () => {
  ...
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [
        ...,
        TranslateModule.forRoot(),   // This is sufficcient. Note that only the text keys are displayed when unit tests are executed.
        ...
      ],
      ...
    })
    .compileComponents();
  }));

Translation process

The json language file can be translated using common translation tools like Passolo or BabelEdit.

Enable locale specific formatting with the angular pipe operators

Problem

I need to format a long decimal number with thousand separators (123456789 → 123.456.789, assuming de-DE formatting).\ When I use the Angular DecimalPipe I get "123,456,789" as a result regardless of the language settings (DE or EN) in the ctrlX app.\

I tried to supply a locale to the DecimalPipe like this:

<td mat-cell *matCellDef="let item">{{ item.sum | number:'0.0-0':'de-DE'}}</td>

The idea here is to provide a locale derived from the value of currentLang of the TranslateService.

I get the error:

_ERROR Error: NG02100: InvalidPipeArgument: 'Missing locale data for the locale "de-DE".' for pipe 'DecimalPipe'._

The reason for this error seems to be a missing registration for the locale de-DE\ It seems that at the moment there is no general guideline on how to handle locale aware formatting within our application.

Expected behavior

When the user changes between languages in the ctrlX app, the formatting of numbers and dates is influcenced by that change.\ As a developer I can use the standard Angular pipes like DecimalPipe and the formatting of the output respects the current language selection.\ This must be managed globally. This is not a responsibility of a single feature module.\ As there are multiple locales per language (e.g. en-US, en-GB) , we must either decide on a fixed locale per language or we must provide the user with the ability to select the locale in addition to the language.

Solution

After some research here is a possible solution. It involves the following steps.

  • Register a provider that will a provide a value of the LOCALE_ID that is based on the currently selected language.
  • Register the localeData for the German language.

Here are the changes necessary to make this work. Everything is located in the app.module file.

import { APP_INITIALIZER, LOCALE_ID, NgModule} from '@angular/core';
import { registerLocaleData } from "@angular/common";
import localeDe from "@angular/common/locales/de";

@NgModule({
  declarations: [
  // existing code omitted
  ],
  imports: [
  // existing code omitted
  ],
  providers: [
    {
    // existing code omitted
    }, {
        provide: LOCALE_ID,
        deps: [TranslateService],
        useFactory: (translateService: TranslateService) => translateService.currentLang
        }
  ],
  bootstrap: [AppComponent]
})

export class AppModule {
  constructor() {
    registerLocaleData(localeDe);
  }
}

Gratulations - We're finished - Let's start coding

Support

Developer Community

Please join the Developer Community

SDK Forum

Please visit the SDK Forum

Issues

If you've found an error in these sample, please file an issue

License

SPDX-FileCopyrightText: Bosch Rexroth AG SPDX-License-Identifier: MIT