Trifork Blog

Axon Framework, DDD, Microservices

Internationalization with AngularJS

April 10th, 2014 by
|


AngularJS-large

Many web applications need to support multiple languages. The process of building in this support in a piece of software can be split in two parts: Getting it technically ready to support multiple languages/regions, and getting it ready for a particular language/region. The first part is called internationalization, often abbreviated to i18n (18 being the number of characters left out in the abbreviation). The second part is called localization, abbreviated as L10n. In this blog post, we’ll see how we can support i18n in an AngularJS-based web application. There is an example project available containing all source code. It’s a Maven project based on Tomcat 7 (Servlet API 3.0) and JDK 6, and can be downloaded here. The example builds on a an example case I created as part of a previous blog on AngularJS.

Scope

It’s useful to start by quickly reviewing what i18n includes, and what aspects of it we’re going to implement. At a most basic level, we should eliminate hard coded texts. Instead, pieces of text should be identified by a label or unique key. The translation of this label to an actual piece of text will be dependent on the language. For each supported language, there is a translation table containing the actual texts for each label. In the Java world, this table is known as a ResourceBundle. This is the most elementary type of i18n, and we will see it in action in our example application.

It turns out that in actual applications, simply serving a static string for each label doesn’t cut it. Consider the message “The compiler found 8 errors.”. Here, the number 8 is obviously not a fixed part of the text. This should be a dynamic parameter of the message. The process of creating the actual message from a kind of template and a set of parameters is often called interpolation. But what if there is only 1 error? Then the text should read “1 error.” and not “1 errors.”. Getting this aspect right is called pluralization. In the Java world, both interpolation and pluralization can be done by the MessageFormat class. We’ll see how these can be handled through AngularJS in our example application.

When doing interpolation, a specific aspect that is language-dependent is the formatting of the parameters. For instance, in case the parameter is of a number type, the decimal separator is a point (“.”) in English but it’s a comma (“,”) in Dutch. A much more complex case is formatting dates in long form. This involves translating a technical representation of a date/time (for instance, an ISO8601 date like ‘2014-03-16’) to its natural language representation (March 16th, 2014). This is an aspect that isn’t included in our example project, although it could be handled along the same lines.

In addition to the things described above, there are some i18n fringe phenomena that I won’t really cover in the discussion. One of those things is displaying the right currency sign. I personally believe this issue is wrongly placed under the i18n umbrella; if you have an international application dealing with money, currency handling is a business logic issue and not merely a presentation thing. Other things are time zones, i18n of non-text resources (e.g., images) and non-Western languages that use a direction of writing other than left-to-right.

Does AngularJS support i18n?

If you look at the AngularJS developer guide, you’ll see that AngularJS supports some of the things we just described. There’s formatting of date/time, numbers and currency. Also, there is support for pluralization. But there are two important limitations. First, there is no out of the box support for the process of translating a label/key to an actual text for a given language (which in my mind is an i18n requirement that is more basic than formatting dates and numbers). Second, the language to be used for date/time and number formatting must be chosen when the page is initialized and can’t be changed dynamically. Overall, you could say that AngularJS has rather limited i18n support.

Although the out-of-the-box support of i18n by AngularJS is a bit disappointing, doing i18n with AngularJS still has a lot of potential. AngularJS has a filter syntax (things like {{ 12 | currency }} producing $12.00), which really seems to fit nicely with i18n. Why not have {{ 'HORSE' | xlat }} producing ‘horse’ in English and ‘paard’ in Dutch? Also, handling i18n in AngularJS (and thus in the client/browser) makes for a very nice architecture. i18n would be dealt with as a presentation-layer aspect only, and the server side business logic wouldn’t have to know anything about it.

Looking at 3rd party i18n extensions to AngularJS, one quickly stumbles upon angular-translate. This is an implementation of the basic idea of using a filter to perform i18n in AngularJS. I’ve used it for a real-world application, and it generally works fine. The problem I have with it though, is caused by the fact that requirements and technical details surrounding i18n vary greatly between applications. For instance: How do you determine the initial language to be used for a user? Where do you store the current language? Where do the translation tables come from? If translation tables are changed/expanded dynamically, when does this take place and how? angular-translate deals with this variety by being very configurable and modular. The result is that I often find myself having to implement a custom implementation of some angular-translate component to get it to do what I need it to do; and that is somewhat tricky.

Just coding what you actually need, directly in AngularJS, may be a lot simpler in many cases. That’s the approach taken in our example project, which we will discuss below.

The basics

The first thing we want to achieve is to have language-agnostic labels being translated into a language-specific text by a filter. In our HTML form, we want to write something like:

<div>
  
  <label>{{ 'FIRST_NAME' | xlat }}</label>
  <input data-ng-model="data.firstName" type="text">
</div>

The basic task we need to achieve is to implement the xlat filter. A very rough implementation could be like this:

formApp.filter('xlat', ['$rootScope', function($rootScope) {
  // The code here executes only once, during initialization.
  // We'll return the actual filter function that's executed
  // many times.
  var tables = {
    'en': { 'FIRST_NAME': 'First name:' },
    'nl': { 'FIRST_NAME': 'Voornaam:' }
  };
  $rootScope.currentLanguage = 'en';
  return function(label) {
    // tables is a nested map; by first selecting the
    // current language (kept in the $rootScope as a
    // global variable), and selecting the label,
    // we get the correct value.
    return tables[$rootScope.currentLanguage][label];
  };
}]);

Here, the translation table is hard coded into the filter function. We’re creating a $rootScope variable (which is like a “global” variable) to keep track of the selected language. And this filter is part of the the same AngularJS module as our form itself. So there is room for improvement as far as software architecture goes. But the basic idea works fine. We cannot only use this filter in the {{ ... }} context, but also in other places where AngularJS expressions occur, such as in the ng-options attribute of a select input:

<div>
  <label>{{ 'FAV_COLOR' | xlat }}</label> 
  
  <select 
      data-ng-model="data.color" 
      data-ng-options="('COLOR_' + opt | xlat) 
                       for opt in colorOptions">
  </select>
</div>

Improving the architecture

i18n is a shared concern among many parts of our application. It’s best placed in a separate module, in its own JavaScript file. The actual logic of the i18n can be put in an AngularJS service factory. This service can be injected as a dependency into the filter (for doing translations) and in controllers (for having the user change the current language). The initial translation tables can be retrieved from a separate file (maybe generated dynamically by the server). The new xlat.js module looks like this:

// We'll create a separate module that we can depend on
// in our main application module.
var xlat = angular.module('xlat', []);

xlat.factory('xlatService', function() {
  // This function will be executed once. We use it as
  // a scope to keep our current language in (thus avoiding
  // the ugly use of root scope).
  var currentLanguage = 'en';
  // We copy the initial translation table that we included
  // in a separate file to our scope. (As may might change
  // this dynamically, it's good practice to make a deep copy
  // rather than just refer to it.)
  var tables = $.extend(true, {}, initialXlatTables);
  // We return the service object that will be injected into
  // both our filter and our application module.
  return {
    setCurrentLanguage: function(newCurrentLanguage) {
      currentLanguage = newCurrentLanguage;
    },    
    getCurrentLanguage: function() {
      return currentLanguage;
    },    
    xlat: function(label, parameters) {
      // This is where we will add more functionality
      // once we start to do something more than
      // simply look up a label.
      return tables[currentLanguage][label];
    }    
  };
});

// The filter itself has now a very short definition; it simply
// acts as a proxy to the xlatService's xlat function.
xlat.filter('xlat', ['xlatService', function(xlatService) {
  return function(label) {
    return xlatService.xlat(label);
  };
}]);

And to support switching languages in the form, we modify our form.js module like this:

formApp.controller('FormController',
// We inject the xlatService in out controller.
    ['$scope', ..., 'xlatService',
     function($scope, ..., xlatService) {
  ...
// So we can create a $scope function that can be linked
// to the click of a change-language button.
  $scope.setCurrentLanguage = function(language) {
    xlatService.setCurrentLanguage(language);
  };

Now, everything is neatly factored out and the xlat functionality doesn’t mess up our other modules and controllers.

Adding functionality: interpolation

AngularJS itself has interpolation as one of its core functions. It’s used to translate AngularJS expressions in {{ ... }} to text. Luckily, AngularJS exposes this functionality to the programmer as the $interpolate service. In a certain sense,
$interpolate will be our AngularJS, client-side alternative to Java’s MessageFormat. But how do we get the parameters to our filter function? AngularJS’s filters support filter arguments, to be presented after a colon following the filter name.

As an example, consider the label

'AGE_MAX': 'Age cannot be higher than {{years}}.'

Whenever the server returns an AGE_MAX message, it will return as well the value of the years parameter. We’ll make sure the server returns a JSON message array like this:

[ { "label": "AGE_MAX", "parameters": { "years": 130 } } ]

To support interpolation, we’ll include the following in the HTML:

<ul>
  
  <li data-ng-repeat="m in messages">
    {{m.label | xlat:(m.parameters)}}
  </li>
</ul>

We have to modify our xlat filter to support the parameters argument:

// Still just a proxy, but now including both the standard
// and additional arguments.
xlat.filter('xlat', ['xlatService', function(xlatService) {
  return function(label, parameters) {
    return xlatService.xlat(label, parameters);
  };
}]);

And finally, the xlatService will need to support this as well. Here, we’ll inject the $interpolate service and modify our xlat function:

xlat.factory('xlatService', ['$interpolate', function($interpolate) {
  // ...
  return {
    // ...
    xlat: function(label, parameters) {
      if(parameters == null || $.isEmptyObject(parameters)) {
        // No parameters, so we don't have to worry about
        // interpolation.
        return tables[currentLanguage][label];
      } else {
        // We got parameters. We'll provide our text to the
        // $interpolate service, that will return a function.
        // Applying the parameters to this function will give
        // us the actual text.
        return $interpolate(
                 tables[currentLanguage][label])(
                   parameters);
      }
    }    
  };
}]);

After building this, I found it remarkably simple. There is not much going on here besides wiring together the filter syntax with the existing $interpolate service.

Adding functionality: pluralization

Pluralization (and related stuff like ‘genderization’) is a pretty complex topic if you think it through. The readme of the messageformat.js project gives a nice impression of the complexity involved. I wanted to do something powerful enough to handle most of the cases, yet simple enough to to be implemented in just a couple of lines of code.

The solution I’ve chosen is that the value contained in a translation table for a particular label, may be a function rather than a text. If it’s a function, the xlat function will evaluate this function against the parameters. The result will be treated as the new label. Some example data to see it in action:

'INCORRECT_FIELDS': function(parameters) {
  if(parameters.n == 1) return 'INCORRECT_FIELDS_SINGULAR';
  else return 'INCORRECT_FIELDS_PLURAL';
},
'INCORRECT_FIELDS_SINGULAR': '1 input field was incorrect.',
'INCORRECT_FIELDS_PLURAL': '{{n}} input fields were incorrect.'

The intention here is to correctly distinguish between “field was” and “fields were” depending on the number of fields. To use this in our xlat function, we simply add the following three lines of code:

// If our supposed text is actually a function, we will apply
// this function to the parameters to obtain a new label. The
// text belonging to this label may be a function as well, so
// we keep following our functions until we have something that
// is not a function.
while($.isFunction(tables[currentLanguage][label])) {
  label = tables[currentLanguage][label](parameters);
}

Conclusion

This completes our ‘tour’ of the code in the example project. The code you’ll find in there is slightly more elaborate than this, as it has been made resilient against the case of missing texts for labels (in that case, the label itself will be shown). Also, the example project includes the server-side stuff making the form actually work as well; this part is straightforward Java / Spring MVC and hasn’t been discussed here.

I’ve really come to like the idea of dealing with i18n client-side in AngularJS. The combination of the power of AngularJS and that of JavaScript as a dynamic language supporting function values, makes this surprisingly easy. Functionally, we get something great as well: language can change on the fly while keeping all other state of the web page. Given that it’s so easy, and that this area always has many application-specific details, I would generally prefer to build it myself rather than use a pre-existing framework.

Interested to learn more? Join us at the training HTML5 Single-Page Applications with AngularJS

17 Responses

  1. June 9, 2014 at 22:03 by ebourmalo

    Thank you for sharing this, good to read feedbacks about the angular-translate module and this home-made alternative 🙂

  2. June 16, 2014 at 11:56 by Pascal Precht

    Hey, thanks for your article 🙂

    I’m the creator of angular-translate and I wonder what of your mentioned topics are not covered by angular-translate. Because all these things like switching the language at runtime, requesting the currently used language etc. is fully supported by angular-translate 🙂

    Would love to hear your feedback on what’s missing! 🙂

  3. June 19, 2014 at 06:52 by Frans van Buul

    Hi Pascal,
    Thanks for creating angular-translate. We have launched an application for one of our customers a couple of weeks ago, that uses it. (It’s for international students so internationalization was important.)

    I think all of the things mentioned in the blog are in fact present in angular-translate. Plus a lot more. However, I do feel that the feature-richness of angular-translate also makes it quite complex to use, whilst doing this for yourself in Angular may be quite simple (depending on how much functionality you need). Therefore, in some cases I would prefer this home made option.

    • June 19, 2014 at 10:27 by Pascal Precht

      I get your point your point. Maybe you can make it a bit more clear in especially this sentence ” angular-translate deals with this variety by being very configurable and modular. The result is that I often find myself having to implement a custom implementation of some angular-translate component to get it to do what I need it to do; and that is somewhat tricky.”

      Also, in angular-translate everything is pluggable, so you don’t need to use *everything* for just a small task. Just use the core and stick with it.

      I would also love to get feedback on “feature-richness of angular-translate makes it quite complex to use” while taking a look at the current APIs. Because we put a lot of energy in designing a very easy to use API, which is also what made angular-translate so popular.

  4. August 15, 2014 at 12:37 by Stephane

    Hello Frans,

    Thank you for that article. I implemented it and it worked in no time. One little glitch seen in the source code of the downloaded i18n-angular.zip archive, is an undefined variable called ‘input’ on line 25 of the xlat.js file.

    Also, I share the being puzzled by that sentence of yours regarding angular-translate being modular and somewhat therefore complex 🙂

    I will now see if I can make it more modular, with loading a language resources table file for each module of my domains.

    Kind Regards,

    Stephane Eybert

  5. August 15, 2014 at 14:05 by Stephane

    @Pascal, Hello,

    Would the angular-translate allow for language resources being defined in a file per domain module ?

    Kind Regards,

    Stephane Eybert

  6. August 16, 2014 at 17:47 by Stephane

    Hello,

    After trying different routes, I must confess I failed to find the way to set up several languages resource definition files, one per domain module. I tried calling the service from the .config() methods of the modules only to learn it was not possible to inject services in this method. I then tried to do this with the .run() method, which was allowed, but it would replace the current resource language content.
    I see Pascal’s translate seems to offer this. I’ll give it a go.

    Cheers,

    Stephane

  7. August 29, 2014 at 18:55 by Bluesoft News #15 - Labs Bluesoft

    […] Internationalization with AngularJS http://blog.trifork.com/2014/04/10/internationalization-with-angularjs […]

  8. January 20, 2015 at 18:43 by Vince

    Hi,

    This looks great, but how does it scale? i.e. what kind of performance impact would be expected for a “real” site that may have several thousand lines of text?

    Thanks!
    Vince

  9. January 20, 2015 at 21:28 by Bobby

    Great article! I found it really helpful on my recent project. Others might also find this one helpful: http://www.thebhwgroup.com/blog/2015/01/translation-localization-angularjs-part-2/

  10. February 8, 2015 at 14:46 by Rafael

    Thank you for the insights. I’m studying i18n and i10n solutions with angular and this article was a really good overview of existent solutions 😉

  11. March 13, 2015 at 16:38 by Andrey Koppel

    This won’t change language on fly. The translation stays the same after change language.

  12. March 14, 2015 at 08:01 by Andrey Koppel

    That is because i’m using Angular 1.3.5, and in the example Angular 1.2.12 is used. Some inner Angular problem.

  13. April 7, 2015 at 14:18 by Masamune

    Hi,

    I got some trouble running your tutorial as well.
    The filter seems to apply only once & doesn’t get updated when I “setCurrentLanguage()”.

    Even trying with a really simple example this seems broken..

    .controller(‘TestController36’, [ ‘resourcesManager’, function ( resourcesManager ) {

    $scope.upResource = function() {
    resourcesManager.changeResources();
    };
    }])

    .factory(“resourcesManager”,
    [
    function ( ) {
    var resources = “base resource”;

    return {
    getResource: function () {
    alert(‘getResource ‘ + resources);
    return resources;
    },

    changeResources: function () {
    resources = “new ressource”;
    alert(‘changeResources ‘ + resources);
    }
    };
    }
    ]
    )

    .filter(“myFilter”,
    [“resourcesManager”,
    function (resourcesManager) {
    return function () {
    alert(‘myFilter’);
    return resourcesManager.getResource();
    };
    }
    ]
    )

    Then I call in the template ( {{ wtv | myFilter }} ) and then call (ng-click=”upResource()” )

    At init it is properly remplaced, but doesn’t update when upResource() is fired.

    Any idea of what the problem could be ?

  14. April 7, 2015 at 14:19 by Masamune

    Sorry about indentation, it dropped on submit 🙁

  15. April 7, 2015 at 14:30 by Masamune

    Also just tried with .filter only (no service & resources nested). Seems KO aswell : view doesn’t update, despite console.log( $filter(‘xlatRS’)(‘FIRST_NAME’) ); still return Voornaam:

    I really don’t get it

  16. April 7, 2015 at 14:37 by Masamune

    Did the trick :
    (since angular 1.3.5 ?) Adapt return :

    function filterFn(label) {
    return tables[$rootScope.currentLanguage][label];
    }
    filterFn.$stateful = true;
    return filterFn;

    Thanks : https://stackoverflow.com/questions/27402326/angularjs-1-3-async-filter-not-working/