Verónica MartínezSep 21, 2017

Internationalization in Angularjs

On several occasions in our projects internationalization has been required. This is something worth considering from the start, since the effort to add it later on is much higher. In this post we’ll discuss our approach to this requirement on both the front and backend sides.

Why should this be done from the start?

The main reason is that you will have to change how you handle both static and dynamic strings in your application, because they will need to be replaced with identifiers. It’s much easier, and less prone to error to put this text in a file first and use the identifiers instead of having to comb through all you views, services, controllers and directives to find strings that need to be replaced.

Another issue is design. Since you’ll most likely have designs carefully crafted with the intended first language in mind. However, difference in word size for different languages, for example, can break a design. It’s not fun to learn that “Closed” is “Geschlossen” in German only after you think you’re done… So the earlier this issue comes up, the easier it will be to solve.

How to set it up

Our tool of choice with AngularJS is angular-translate, an easy to use and very effective module that provides nearly all that is required for this task.

The first thing to do is to set up the translation module:

angular
  .module('translatedApp', ['pascalprecht.translate'])
  .config(['$translateProvider', function($translateProvider) {
    $translateProvider
      .useStaticFilesLoader({
        prefix: '/translations/',
        suffix: '.json',
      })
      .useMissingTranslationHandlerLog()
      .preferredLanguage('en_US');
  }])

In this case we chose to keep the translation sources as static files in the app, so we tell the module to search for the files with .json suffix in the /translations/ directory. We also set the initial language to English, so the first file we need to create is called en_US.json.

en_US.json

{
  "home_page": {
    "title": "Welcome to our website!",
    "description": "This is a simple example"
  },
  "button_text": "Keep exploring!"
}

That is a very simple example of the structure of the language file. One important point is the ability to nest keys, which goes a long way towards helping keep the file organized and maintainable.

In a template, we’d use it either as a filter or a directive:

<div>
  <h1>{{ "home_page.title" | translate }}</h1>
  <p translate="home_page.description"></p>
</div>

<button>{{ "button_text" | translate | uppercase }} <img src="images/arrowForwardIcon.png"></button>

Keep in mind that the directive replaces the entire inner content of the html tag, while the translation filter, like all other filters, can be chained.

Another fundamental feature for translations is variable replacement, which would allow us to have strings like "Welcome, {{username}}!".

Another really cool feature is the resolveClientLocale method. This returns the language key currently used in the user’s browser, which is great to set the app’s initial language. After that, when you have your language selection dropdown, or something similar, you can use the use method to change the language, a change that can be seen in real time. One example on how to do that:

var preferredLanguage = $translate.resolveClientLocale();
store.set('language', preferredLanguage);
$translate.use(store.get('language'));
moment.locale(store.get('language'));

Is this enough to have a fully internationalized app?

Not quite, while this is a huge step forward, apps usually have more than static strings. For example, we use dates and times in many places. A common javascript module to handle them is moment.js, which thankfully also provides support for internationalization. Where this simple code would display all dates created with moment.js in spanish.

moment.locale('es');

Each new module we use that provides information that the user is shown must be checked to see if it implements internationalization, and if it doesn’t, to extract its information and apply the translation filter.

Finally, we usually use a Python/Django backend, and if that backend returns messages for the user, those must be translated as well. If this messages are shown unchanged to the user, it’d be best for them to arrive ready to be displayed to simplify the app’s logic. That is easy to do using Django, since it accepts the Accept-Language HTTP header. Therefore we add an interceptor, so that all requests to the server respond in the preferred language.

angular.module('translatedApp')
  .service('LanguageInterceptor', [function() {
    var service = this;

    service.request = function(config) {
      config.headers['Accept-Language'] = 'en-us';

      return config;
    };
  }]);

Adding in the app’s configuration

angular
  .module('translatedApp', ['pascalprecht.translate'])
  .config(['$translateProvider', function($translateProvider) {
    /* Previous code*/
    $httpProvider.interceptors.push('LanguageInterceptor');
  }])

An important note

One thing to keep in mind is that you will not be able to use the same language key everywhere you need. For example, the language file in our example uses the language key ‘en_US’, while Django expects ‘en-us’, as seen in the interceptor example. In other cases, you may not have the specific language requested, so you should default to the closest you can find, or failing that, use your fallback language.

Other useful tools

A great tool to use along the angular-translate module mentioned is this translation editor. Here you can upload your source translation file, and it will add three columns for each key, one with the key itself, a second with the key’s value in the source file, and a third with an input to change the key’s value in the destination file. This helps ensure that if a third party is translating the app, they will still return a valid JSON file.