angular-translate is a fantastic i18n (internationalisation) library with a super simple API with which we can transform an Angular application from monolingual to multilingual in a very short space of time.

All it requires is lifting the existing static language from your templates and placing it in a file named after its locale, where it will sit with all other locales which you intend to support in your website.

In this post we will discuss how to implement angular-translate and two of its native plugins:

  1. angular-translate-loader-url, which will allow us to store all our language files on the server and fetch them dynamically, rather than serving each locale with the application’s usual payload, which could be costly as these files will be quite large. We will accomplish this using Express running on a NodeJS server.
  2. angular-translate-storage-cookie, which persists a user’s preferred locale between sessions by setting a cookie on their machine. (You could instead use angular-translate-storage-local if you’d rather use HTML5’s local storage.)

As always you can find all the code used in this post over at our GitHub account, ready-to-go! Once you’ve downloaded the code remember to run both bower install and npm install.

If you aren’t interested in using angular-translate-loader-url and would prefer to serve all language files to the client at once, you can of course follow the great documentation over at angular-translate.github.io/docs and modify our seed application to your needs. Hint: each locale can be represented as an Angular constant, injected into a config block, and fed into $translateProvider#use.


First let’s look at how to configure angular-translate. We have access to an Angular provider $translateProvider which we will inject into our config method, like so:

/* module definition and configuration stuff... */
.config(function($locationProvider, $routeProvider, $translateProvider) {
    /* location and route configuration stuff... */
    $translateProvider.preferredLanguage('en');
});

So now angular-translate knows what language to load automatically, but we also have to tell it where to get the language files. This is where angular-translate-loader-url comes in. This plugin doesn’t require any extra configuration other than simply including its script file beneath angular-translate (or if like us you use Brunch then simply run brunch watch to compile a new vendor.js file), but it gives us an extra method $translateProvider#useUrlLoader. And we simply call it before defining our preferred language:

$translateProvider.useUrlLoader('/api/lang');
$translateProvider.preferredLanguage('en');

You’ll notice that this new method takes a single parameter – a string which represents the endpoint for grabbing the language files. Angular-translate-loader-url will call this URL with a single GET parameter, lang. For our locale en the call to our endpoint will be /api/lang?lang=en. Later we’ll see how listening to this GET parameter with Express allows us to choose the appropriate locale file to send to the client.

So with angular-translate-loader-url configured we can do the same with angular-translate-storage-cookie. This plugin requires the ngCookies library which does not come packaged with Angular as default, so we include this as a dependency when defining our Angular module (this happens before the config block that we were working with above):

angular.module(
    'i18nOutlandishApp', // Name of our application
    [
        // Dependencies:
        'ngRoute',
        'ngCookies',
        'pascalprecht.translate' // angular-translate
    ]
);

Once we have done this we can call the $translateProvider#useCookieStorage method to tell angular-translate that we want to store the user’s locale choice in a cookie. And that’s all! Super easy.

$translateProvider.useCookieStorage();
$translateProvider.useUrlLoader('/api/lang');
$translateProvider.preferredLanguage('en');

Now we can move onto structuring the locale files in our application. Because we are remotely fetching language files on-the-fly they need to be placed in the part of our application that is server-side, for us this is the /app folder. Inside /app we will have another folder /i18n to hold our language files:

/i18nOutlandishApp
└─ /app
   |  ...
   |
   └─ /i18n
   |    |  en.json
   |    |  es.json
   |    |  fr.json
   |
   └─ /routes
        |  api.js

And let’s take a look at the contents of our language files:

en.json

{
    "languageNames": {
        "en": "English",
        "es": "Spanish",
        "fr": "French"
    },
    "home": {
        "helloWorld": "Hello, World!"
    }
}

es.json

{
    "languageNames": {
        "en": "Inglés",
        "es": "Español",
        "fr": "Francés"
    },
    "home": {
        "helloWorld": "Hola Mundo!"
    }
}

fr.json

{
    "languageNames": {
        "en": "Anglais",
        "es": "Espagnol",
        "fr": "Français"
    },
    "home": {
        "helloWorld": "Bonjour Le Monde!"
    }
}

We can see by the keys that the structure of the files is consistent between the different locales. All that is different are the values of the key names, as this is what will be shown to the user. Before we start on how to display a desired language to the user let’s set up our API for fetching the language files themselves. For this we need an Express GET route.

Node allows us to require a JSON file and have it parsed into an object without further effort. As we saw above in the file structure diagram we will define the route inside /app/routes/api.js. And here it is:

app.get('/api/lang', function(req, res) {
    // Check endpoint called with appropriate param.:
    if(!req.query.lang) {
        res.status(500).send();
        return;
    }

    try {
        var lang = require('../i18n/' + req.query.lang);
        res.send(lang); // `lang ` contains parsed JSON
    } catch(err) {
        res.status(404).send();
    }
});

If the endpoint is called without the lang query parameter then we return a status 500, otherwise we attempt to load the JSON file whose name is the value of req.query.lang. If this file does not exist Node will throw an error. We catch the error and respond with a status 404, indicating that the locale was not found. Note: we can omit the .json extension from our call to require.

With our language endpoint defined we can now move on to providing the user some way of selecting a locale. Let’s create a directive, /angular/common/localeSelector, which we can place in any page where a choice of language is desired:

locale-selector.html

<select ng-change="setLocale()" ng-model="locale">
    <option value="en" ng-selected="locale == 'en'">
        {{ 'languageNames.en' | translate }}
    </option>
    <option value="es" ng-selected="locale == 'es'">
        {{ 'languageNames.es' | translate }}
    </option>
    <option value="fr" ng-selected="locale == 'fr'">
        {{ 'languageNames.fr' | translate }}
    </option>
</select>

locale-selector.js

angular
    .module('i18nOutlandishApp')
    .directive('localeSelector', function($translate) {
        return {
            restrict: 'C',
            replace: true,
            templateUrl: 'angular/common/localeSelector/locale-selector.html',
            link: function(scope, elem, attrs) {
                // Get active locale even if not loaded yet:
                scope.locale = $translate.proposedLanguage();

                scope.setLocale = function() {
                    $translate.use(scope.locale);
                };
            }
        };
    });

With this directive we can place our locale selector dropdown within any page by simply adding the following snippet:

<div class="locale-selector"></div>

Inside each of the option elements within our template we use an Angular binding expression which references the string within our selected locale to display and couples this with the translate filter, given to us by angular-translate. The translate filter takes the string within the expression and resolves it to the correct property within the active locale. This means when a user selects another language the names of each of the available locales changes to accommodate their new choice. Notice also that within the directive we inject $translate, not $translateProvider. As a provider the latter is used during configuration only.

We can use this same type of binding expression in our templates. For example, if our home page template contains the following static text:

<span>Hello, World!</span>

It can be changed to become:

<h1>{{ 'home.helloWorld' | translate }}</h1>

And now the template will display “Hello, World!” in whatever language was chosen by the user through our localeSelector directive! And when the chosen language changes, so too will all all the translate bindings, on-the-fly. For elements that require locale text both as content and in an attribute, such as the title attribute, we can amend our language files by giving the helloWorld property two children, helloWorld.title and helloWorld.text, for example, and then use it like so:

<h1 title="{{ 'home.helloWorld.title' | translate }}">
    {{ 'home.helloWorld.text' | translate }}
</h1>

Running the i18nOutlandishApp

  1. Navigate to the root of the web app.
  2. Run npm install and bower install.
  3. Run brunch watch.
  4. Run node run.js. (Running the server.js file will do nothing.)
  5. Navigate to http://localhost:8080 in your browser.

– Sam

[Abi wrote this bit] We are always looking for new developers so if you’ve read this blog, found it helpful and are interested to find out more about what we do, get in touch, we are always up for a pint and a chat in the pub team@outlandish.com