6

I saw that angular6 implements i18n for its components and that by using i18n you can internationalize your html but can you do the same with typescript? I have two specific areas

One in a zingChart: - Be able to i18n Example

exampleData = {
 valueBox: {
                text: '<span style="font-size: 32px">%pie-total-value</span> <br/> Example',
                placement: 'center',
                fontWeight: 'normal'
            },
     }

Thank you very much for your time and answers.

3
  • Could you please clarify what you mean by internationalize with TypeScript? Are you asking if you can handle translating the languages in your component (TS) instead of the components template (HTML)?
    – Narm
    Commented Sep 19, 2018 at 14:56
  • 1
    I'm using the angular's i18n tool which works great with html and it creates a messages.xlf and I want to be able to add the same thing with my Typescript text so it is added in the same messages.xlf so when I ran a build all the text (both html and ts) are translated
    – user173092
    Commented Sep 19, 2018 at 15:06
  • A good example is: stackoverflow.com/questions/41182055/… but I want more information on the TranslationsService or another way of doing it
    – user173092
    Commented Sep 19, 2018 at 15:07

4 Answers 4

12

This is not possible through the library's API until now (@angular/language-service v7.2).

Below is my workaround (thank fredrikredflag for his good post on GitHub and thank @BrunoBruzzano for the link):


src/app/i18n.service.ts:

import {Injectable} from "@angular/core";
import {Xliff2} from '@angular/compiler';
// You can also import {Xliff} or {Xtb} instead of {Xliff2}, depending on your project configurations

declare const require;
const content = require('raw-loader!../i18n/messages.fa.xlf');

@Injectable({
    providedIn: 'root'
})
export class I18nService {
    private readonly xliff: any = new Xliff2().load(content, '');

    get(key: string): string {
        return this.xliff.i18nNodesByMsgId[key][0].value;
    }
}

i18n pseudo-component (JUST FOR AUTO-GENERATING TRANSLATIONS in messages.xlf file):

  1. src/app/i18n/i18n.component.ts (Isn't important. Just needed to exists.):

    import {Component} from '@angular/core';
    @Component({templateUrl: './i18n.component.html'})
    export class I18nComponent {}
    
  2. src/app/i18n/i18n.component.html (don't forget using an id!)

    <p i18n="@@newVersionAlert">New version available. Load New Version?</p>
    

Don't forget declaring I18nComponent in your @NgModule.


Usage (after running ng xi18n ... and translating):

In your component:

...
import {I18nService} from './i18n.service';

...
    constructor(private i18nService: I18nService, ...) { ... }

    sampleUsage() {
        confirm(this.t('newVersionAlert'));
    }

    /**
     * translate
     */
    private t(i18nId: string) {
        return this.i18nService.get(i18nId);
    }
...

Utility script to translate i18n.service.ts before build:

(This requirement: require('raw-loader!../i18n/messages.fa.xlf') needs to be translated to match wanted locale.)

PreBuild/prebuild.ts:

import {Xliff2} from "@angular/compiler";  
// You can also import {Xliff} or {Xtb} from "@angular/compiler" depending of your case.

const fs = require('fs');  
const path = require('path');  

const localeId = process.argv[2];  

if (localeId === undefined) throw new Error(`No language specified.\nUsage: node ${path.basename(__filename)} <locale-id${'>'}`);  

const content = fs.readFileSync(`src/i18n/messages.${localeId}.xlf`, 'utf8');  
const xliff = new Xliff2().load(content, '');

const i18nServiceFilePath = './src/app/i18n.service.ts'; 

fs.writeFileSync(i18nServiceFilePath,  
  fs.readFileSync(i18nServiceFilePath, 'utf8')  
    .replace(/(raw-loader!\.\.\/i18n\/messages\.)\w{2}(\.xlf)/, `$1${xliff.locale}$2`)  
);

PreBuild/tsconfig.json:

{
    "compilerOptions": {
        "outDir": "./build",
        "lib": [
            "es2018",
            "dom"
        ],
        "module": "commonjs",
        "moduleResolution": "node",
        "target": "es6",
        "typeRoots": [
            "../node_modules/@types"
        ]
    },
    "files": [
        "prebuild.ts"
    ]
}

package.json:

...
"scripts": {
    "compile-pre-build": "tsc -p PreBuild/tsconfig.json --pretty",
    "pre-build": "node PreBuild/build/prebuild.js",
    ...
...

Usage:

(After one-time npm run compile-pre-build:)

npm run pre-build -- fa

or

npm run pre-build -- en

This will edit i18n.service.ts.

7
  • Amazing answer, it should definitely get more love. Could be added that i18n.service.ts and prebuild.ts needs to be modified if using some other i18n format. Angular has direct support for xliff(2), xtb and xmb Commented May 9, 2019 at 14:06
  • @ShamPooSham; Thanks. I added a comment to prebuild.ts and i18n.service.ts. Commented May 9, 2019 at 15:16
  • I've got an i18nServiceFilePath not found in prebuild.js. How did you fix this? In addition, if I want to have more languages, should I have an array of contents in i18n.service.ts? Thanks
    – Christophe
    Commented Jun 10, 2019 at 20:50
  • @Christophe; 1. OK; I corrected it. Thanks for reminding. 2. No; The locale-id that you include it in the path that is located after raw-loader! (path: ../i18n/messages.fa.xlf => locale-id: fa) is not important. Just insert a locale-id into it. It will replace with the locale-id you pass to prebuild script (en, fa, fr, es, ar, etc) and you should run this before each build (as its name implies: prebuild). Commented Jun 11, 2019 at 7:23
  • Thank you @Mir Ismaili, I made it work. I did not know we should be doing one build with different output path per language in Angular.
    – Christophe
    Commented Jul 28, 2019 at 8:29
4

In Angular 9 you can use global $localize function like this:

$localize`String to translate`

Be aware:

  1. In Angular 9 CLI does not extract these messages with the xi18n, you have to do it manually
  2. Run ng add @angular/localize if you have not gone through $localize migration
0

You can use the Transloco library to do this: https://ngneat.github.io/transloco/.

And then get the translations in the Typescript file like so:

this.translocoService.translate('hello');
-1

You can Extend "ng serve | build" proccess so "AOT compialtion" for i18n translation in .ts is done

  • Main idea is to use jour generated .xlf files(and ids like @@translationId) in .ts
    import { Component } from '@angular/core';

    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.scss']
    })
    export class AppComponent {
      title = 'i18n-ts-demo-ng';
      title2 = '@@my.test.header';
    }
  • and translate them in build proces

    1. add "ng add ngx-build-plus"
    2. create plugin
//create file i18n-plugin.ts in root
import { I18NTransformer } from './i18n';
import { AngularCompilerPlugin } from '@ngtools/webpack';

function findAngularCompilerPlugin(webpackCfg): AngularCompilerPlugin | null {
  return webpackCfg.plugins.find(plugin =>  plugin instanceof AngularCompilerPlugin);
}

// The AngularCompilerPlugin has nog public API to add transformations, user private API _transformers instead.
function addTransformerToAngularCompilerPlugin(acp, transformer): void {
  acp._transformers = [transformer, ...acp._transformers];
}

export default {
  pre() {
    // This hook is not used in our example
  },

  // This hook is used to manipulate the webpack configuration
  config(cfg) {
    // Find the AngularCompilerPlugin in the webpack configuration
    const angularCompilerPlugin = findAngularCompilerPlugin(cfg);

    if (!angularCompilerPlugin) {
      console.error('Could not inject the typescript transformer: Webpack AngularCompilerPlugin not found');
      return;
    }

    addTransformerToAngularCompilerPlugin(angularCompilerPlugin, I18NTransformer);
    return cfg;
  },

  post() {
    // This hook is not used in our example
  }
};
//create file i18n.ts in root
import * as ts from 'typescript';

// TODO move to config
const RequireAlli18NKeys = false; // if true onda all 18n keys must be found othervse error is thrown;

// Read translations
import { Xliff, Node } from '@angular/compiler';
const fs = require('fs');
const path = require('path');

let localeId: string; // hr || en ...

let i18nLocale = 0; // 0 - parameter not found | 1 - parameter is fount so next is locale string (hr, ...)

// parse parameters
process.argv.forEach(pParam => {

  console.log('param:' + pParam);
  // get Locale is using: ng serve ...
  if (pParam.startsWith('--configuration=')) {
    localeId = pParam.replace('--configuration=', '');
    console.log('Locale:' + localeId);
  }

  // Has to be before code down
  if (i18nLocale === 1) {
    i18nLocale = 2;
    localeId = pParam;
    console.log('Locale:' + localeId);
  }

  // Get locale if using: ng build --prod --i18n-locale en ...
  if (pParam.startsWith('--i18n-locale')) {
    i18nLocale = 1;
    localeId = pParam.replace('--config--i18n-locale ', '')
  }
});

// Load translation
// tslint:disable-next-line:max-line-length
if (localeId === undefined) { throw new Error(`No language specified.\nUsage: ng serve --configuration=hr --aot --plugin ~dist/out-tsc/i18n-plugin.js`); }
const content = fs.readFileSync(`src/translate/messages.${localeId}.xlf`, 'utf8');
const xliff = new Xliff().load(content, '');

export const I18NTransformer = <T extends ts.Node>(context: ts.TransformationContext) => {
  return (rootNode: ts.SourceFile) => {
    function visit(node: ts.Node): ts.Node {
      if (
        rootNode.fileName.includes('node_modules')
        || !rootNode.fileName.includes('.ts')
        // || ts.isToken(node)
      ) {
        return ts.visitEachChild(node, visit, context);
      }

      if (ts.isStringLiteral(node)) {
        // teplace @@ with translation
        if (node.text.includes('@@')) {
          // take key for translatioc
          const tSourceKey = node.text;
          const tI18NKey = node.text.replace('@@', '');
          // find key
          const tTranslation: any = xliff.i18nNodesByMsgId[tI18NKey];
          if (tTranslation) {
            // let t1 = tTranslation[0];

            // let tLocaleStr = t1.toString(); //tTranslation[0].value;
            const tLocaleStr = tTranslation[0].value;
            console.log(ConsoleColor.BgCyan, 'i18n key: ', ConsoleColor.Reset, tI18NKey + '=> translation   : ' + tLocaleStr);
            const tNew2 = node.text.replace(tSourceKey, tLocaleStr);
            return ts.createStringLiteral(tNew2);
          }
          const tMessage = 'ERROR! No translation for key: ' + tI18NKey + ', source:' + rootNode.fileName;
          console.log(ConsoleColor.BgRed, tMessage, ConsoleColor.Reset);
          if (RequireAlli18NKeys) {
            throw new Error(tMessage);
          }
        }
      }

      return ts.visitEachChild(node, visit, context);
    }
    return ts.visitNode(rootNode, visit);
  };
};

class ConsoleColor {
  static Reset = '\x1b[0m';
  static Bright = '\x1b[1m';
  static Dim = '\x1b[2m';
  static Underscore = '\x1b[4m';
  static Blink = '\x1b[5m';
  static Reverse = '\x1b[7m';
  static Hidden = '\x1b[8m';

  static FgBlack = '\x1b[30m';
  static FgRed = '\x1b[31m';
  static FgGreen = '\x1b[32m';
  static FgYellow = '\x1b[33m';
  static FgBlue = '\x1b[34m';
  static FgMagenta = '\x1b[35m';
  static FgCyan = '\x1b[36m';
  static FgWhite = '\x1b[37m';

  static BgBlack = '\x1b[40m';
  static BgRed = '\x1b[41m';
  static BgGreen = '\x1b[42m';
  static BgYellow = '\x1b[43m';
  static BgBlue = '\x1b[44m';
  static BgMagenta = '\x1b[45m';
  static BgCyan = '\x1b[46m';
  static BgWhite = '\x1b[47m';
}
  • in terminal start: tsc --skipLibCheck --module umd -w

  • ng serve --configuration=hr --aot --plugin ~dist/out-tsc/i18n-plugin.js

complete example is at https://github.com/Emanuel3003/i18n-ts-demo-ng

Regards

0

Not the answer you're looking for? Browse other questions tagged or ask your own question.