Internationalization with AngularJS

by Frans van BuulApril 10, 2014


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>
  <!-- AngularJS will create the HTML s dynamically
   based on the ng-options attribute. It supports many
   different syntaxes, and we may simply use our filter
   within it. -->
  <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