Want to support more than 1 language in your app? Maybe you think this is just an easy task, but there is a lot that you should/can do to create multilingual support for your web app.

First of all, I show a i18n/l10n tutorial for Angular 1, and afterwards I give a brief overview about the steps you have to make to get the same result in Angular 2.

So let's start with Angular 1! We use quite some Angular modules and other services to make our localized app ready for production:

  • angular-i18n
  • angular-dynamic-locale
  • angular-translate
  • angular-translate-loader-static-files
  • Lingohub
  • Grunt tasks for copying, revision and minify files

1. Start with angular-i18n

AngularJS supports i18n/l10n for datetime, number and currency filters. The locale files are included in the official angular-i18n package.

We only copy the languages (from the installed angular-i18n bower package) that are used in our app to our own 'locales' folder, by using a Grunt copy task (you can also use gulp or any other task runner ;)). This task runs before starting the development server.

locales: {
  expand: true,
  cwd: 'bower_components/angular-i18n',
  src: ['angular-locale_en.js', 'angular-locale_de.js'],
  dest: '<%= yeoman.app %>/locales/angular-i18n'
}

But there is one problem with these files - you can only include one. If you include every file, the last will overwrite all others. This would be the approach when only using the English format for date, numbers etc.:

<script src="/locales/angular-locale_en.js"></script>

But we would like to give the user multiple languages to choose from. Solution: angular-dynamic-locale is used for changing angular $locale (which means formatting dates, numbers, currencies, etc.) asynchronously. You just need to configure the pattern (where your angular-locale_*.js files are located) in you app's config:

app.config(function(tmhDynamicLocaleProvider) {
  tmhDynamicLocaleProvider.localeLocationPattern('/locales/angular-i18n/angular-locale_{{locale}}.js');
})

Then set the preferred locale (either from $cookies or a value from DB) via the app's run function and/or via controller when the user changes his language:

tmhDynamicLocale.set(locale); // locale is 'en' or 'de'

It is safe to remove the script-tag as defined above, because when tmhDynamicLocale.set() is called in the run function the right JS file (e.g. angular-locale_de.js) will be loaded!

2. Ready to internationalize with angular-translate

In the AngularJS world there’s the awesome angular-translate module. It provides services, directives and filters for translating your texts. If you haven't used it before, please take a look at their useful guide.

I will only give a deeper look on how we use JSON files with staticFilesLoader, because we created only 1 JSON file, en.json, in development with our source texts. The source and translated JSON files are structured like this: /locales/{locale}.json. All you have to do is telling angular-translate to load the files from that path:

app.config(function($translateProvider) {
  $translateProvider.useStaticFilesLoader({
    prefix: '/locales/',
    suffix: '.json'
  });
  $translateProvider.preferredLanguage('en').fallbackLanguage('en');
  
  // 'sanitize' would render UTF8 chars wrong!
  // 'escape' would not show HTML-tags which are inside our translations!
  // @see https://github.com/angular-translate/angular-translate/issues/1101
  $translateProvider.useSanitizeValueStrategy('sceParameters');
})

It is also important to define a sanitization strategy. You can find more information about that in the security section at the angular-translate guide.

3. Translate with Lingohub

But how do you get the translated JSON files (e.g. de.json for German, fr.json for French, it.json for Italian or es.json for Spanish)? If you do not want to send your translator a raw en.json file, the best way is to upload the file/s to Lingohub and use the editor to translate the texts or just order the translations from a professional translator network.

After translating you can simply download the translated files or even better: Make a connection to your GitHub repository and push back the translated files in the according folder.  TOP recommended workflow ;)

4. Deployment

Once texts are translated successfully, we add a timestamp to our file names to always provide our users with the latest translations. Nobody is happy when they're seeing removed translation keys or old texts, because the old cached JSON files are being served.

For that we use another Grunt copy task for revisioning our JSON files. While copying them to the 'dist' folder we add an UNIX timestamp to all our JSON files in the 'locales' folder:

expand: true,
cwd: '<%= yeoman.app %>/locales/',
src: ['*.json'],
dest: '<%= yeoman.dist %>/locales/',
rename: function(dest, src) {
  return dest + src.replace('.json', '.<%= revTimestamp %>.json');
}

The same revTimestamp is defined as an Angular constant, so that it can be used in the configuration for staticFilesLoader, like this:

app.config(function($translateProvider, REVISION) {
  var revSuffix = REVISION ? '.' + REVISION : '';

  $translateProvider.useStaticFilesLoader({
    prefix: '/locales/',
    suffix: revSuffix + '.json'
  });
})

That's it! Now your app fully supports multiple languages and always serves the latests texts to your users.

Angular 2 and ng-xi18n

As described in a previous blog post, Angular2 will change everything. So that's why I want to describe the workflow for Angular2 a little bit.

  1. Add an i18n HTML attribute to your elements you want to translate.
  2. Use the ng-xi18n tool to extract strings to a standard XLIFF file.
  3. Translate the file (e.g. with Lingohub).
  4. The Angular compiler (ngc) imports the completed translation files, replaces the original messages with translated text and generates a new version of the application in the target language.
You build and deploy a separate version of the application for each supported language.

The following code example shows the template in the component and how the text gets extracted with the new Message Extraction Tool (ng-xi18n). The main difference to the angular-translate (Angular1) approach is, that you do not have to extract the texts manually and define translation keys.

Command to create the messages.xlf:

./node_modules/.bin/ng-xi18n

Translating and updating texts

If you update your HTML and remove or add texts, the messages.xlf will change (it will be overwritten when executing the ng-xi18n command). If you translate your XLIFF file a messages.de.xlf will be generated. The easiest way not to lose already translated texts is to work with Lingohub. If you upload the changed messages.xlf again, Lingohub detects added and deleted texts, so you just need to translate the new added ones.

Deployment

There are two approaches to get a working internationalization with the translated XLIFF files: JiT (Just-in-Time) or AoT (Ahead-of-Time) compiler.

Using JiT is the standard development approach shown throughout the Angular documentation. It's great .. but it has shortcomings. AoT is used during a build process and radically improves performance for your web app. That's why I will only describe the AoT approach here. Read more at Ahead-of-time VS Just-in-time.

After creating (and translating) the messages.de.xlf, put it in your new created 'locale' folder. (I would save this with a GIT commit). Next, run the ngc compile command for each supported language (e.g. de for German). The result is a separate version of the application for each language.

./node_modules/.bin/ngc --i18nFile=./locale/messages.de.xlf --locale=de --i18nFormat=xlf

Why I like this workflow

First of all, the developer doesn't need to extract texts by her-/himself - the original app comes with the defined source texts. Second, XLIFF is a standardized format for translating content and it should be the format to use, because the developer (or manager of translations) can provide the translator with context information. I also think that this approach is faster in production, because the templates are prebuilt and that's why there will never be a Flash of Untranslated Content caused by asynchronous loaders.

Why I don't like it

IDs in your XLIFF are generated automatically, depending on the source text. Everytime you change the source text a new node in the XLIFF is generated and the old one will be removed. If you already translated the XLIFF file you will not see which text has changed in the German file. This is very similar when working with PO files. Lingohub cannot notify the translator that a text needs to be revised, because the source text has slightly changed.

Change language at runtime?

This is not possible with the current approaches. I also followed an Angular i18n discussion on GitHub, and found this comment very interesting: "If the user does 1,000 things in your app, only 1 of these being changing the language, we'd rather have maximum performance on the other 999 things without sacrificing any bit of performance for that 1 thing."

I am not sure if it is the way Angular 2 tells you to go, but it seems for now that reloading the page is necessary to provide your app in another language.

After all, if you prefer the angular-translate (Angular 1) approach and want to use a similar workflow for Angular 2, just take a look at ng2-translate. It is very flexible and works at runtime: no page reloading to change the language.

References

docs.angularjs.org/guide/i18n

lingohub.com/blog/2015/03/angular-2-i18n-update-ng-conf-2015

stackoverflow.com/questions/34797512/angular2-i18n-at-this-point

angularjs.blogspot.co.at/2016/09/angular-2-rc6_1.html

github.com/StephenFluin/i18n-sample

angular.io/docs/ts/latest/cookbook/i18n.html

Try lingohub 14 days for free. No credit card. No catch. Cancel anytime