Localizing Your AngularJS App

Overview

If you plan on being in the Web App development business for any amount of time, sooner or later you are going to be faced with building an app that supports multiple languages. When this time comes you’ll be faced with the localization challenge that so many developers before you have faced.

To solve this challenge some developers have built entire localization frameworks and libraries, while others have resorted to re-creating their entire site in the desired language and redirecting users based on their browser culture.

In this article, I’ll show you an easy way to use an AngularJS Service and Filter to pull localized strings from a resource file and populate the page content based on the user’s browser culture.

Architecture

Our solution is going to be based on a simple architecture. We will use localized resource files for each language we want to support. We will also have a default resource file that will be used to fall back to the site’s native language if a given user’s language is not supported.

We will build an AngularJS Service that will be responsible for checking the user’s browser culture and requesting the appropriate resource based on the language. If the resource file does not exist it will request the default resource file and use it.

The service will also provide a lookup method that will return a localized string for a given key from the loaded resource file.
Since the service may not be called directly by a controller or app module we’ll also provide a mechanism for the service to initialize itself, load the appropriate localized resource file and prepare itself to handle requests.

We will also build an AngularJS Filter that can be used in your HTML as a front-end to the localization service. Using the filter will help you keep your code clean and keep you controllers from having to know about the localization service.

To use the filter you can use an expression such as; {{‘_FormControllerTitle_’ | i18n}} if you want to inject the localized string directly into a tag or you can use the ng-bind=’_FormControllerTitle_’ | i18n” method to inject the localized string into an element, when AngluarJS compiles and links the DOM.

Finally let’s talk a bit about how we’ll store the localized data for the service. Since our service will be requesting the resource files once the app in bootstrapped, we need a place to store them. To keep the overall size of the service small I thought it was best not to embed the strings in the service class, but put them in directory off the root of the site named i18n. This follows the same pattern you see with several libraries where the localized resources are in a directory co-located with the module.

The files also have a specific file naming format; resources-locale_xx-yy.js where xx is the language identifier and yy is the country identifier. So resources-locale_en-US.js would mean the file is for English, United States and resources-locale_es_es.js would mean the file is for Spanish, Spain.

There is one more file naming convention we’ll use and that is for the default resource file. For the default file that will be loaded is a language resource file for language does not exist on your system it will be named resources-locale_default.js

The format of the language resource file is simple. Since we only really need a few pieces of information I’ve kept it to key, value and description. This way if you need to hand the file off to someone for translation they’ll have a general description of what the test is for.

This format will also help other developers on the project. When they are getting ready to add a new string resource they’ll be able to search the file to see if maybe there is already a string they can use. This comes in real handy for buttons, table headers, etc.

Below is an example of the file format:

[
    {
        "key":"_Greeting_",
        "value":"Site localization example using the resource localization service",
        "description":"Home page greeting text"
    },
    {
        "key":"_HomeTitle_",
        "value":"Resource Localization Service",
        "description":"Home page title text"
    }
]

Building the Service

So now that we have covered the architecture of our solution, let’s get to writing some code.

Let’s start by creating a new JavaScript file and calling it localize.js

Then let’s add the skeleton to define both the module and the service.

'use strict';

/*
 * An AngularJS Localization Service
 *
 * Written by Jim Lavin
 * http://codingsmackdown.tv
 *
 */

angular.module('localization', []).
    factory('localize', function ($http, $rootScope, $window, $filter) {

    });

We start off by calling angular.module which will define our module that we’ll name ‘localization’. Since our module does not depend on anything but the built in AngularJS service we will pass in an empty array to the method as well.

Then we chain a factory method off of the module to define our service which we’ll call ‘localize’ and add a function that will be used to define our service. You can see this in the code above.

Next we need to add the services the service is dependent on so Angular can inject them. This will be using the following services:

  • $http – This will be used to retrieve the localized resource file from the web server.
  • $rootScope – This will be used to broadcast a message once the localized resource has been retrieved and loaded by the system. I’m using this is in case a controller or other service might use the service directly and needs to know when the service is ready.
  • $window – This will be used to find out the culture of the user’s browser, which we’ll use to request the appropriate resource file from the web server. There is an Angular service called $locale which should provide this information, but currently it seems to be hard coded to en-us from what I’ve experimented with and from what I’ve read over at Google Groups. If anyone has gotten this to work, please leave me a comment so I can revise the code to use the proper service.
  • $filter – This will be used to filter the dictionary array and return back only those resource objects that has the desired key the user is looking for.

To add our dependencies and to ensure that any minification doesn’t muck them up, we are going to use the [] around our function to tell Angular what services we depend on. The revised code is below:

'use strict';

/*
 * An AngularJS Localization Service
 *
 * Written by Jim Lavin
 * http://codingsmackdown.tv
 *
 */

angular.module('localization', []).
    factory('localize', ['$http', '$rootScope', '$window', '$filter', function ($http, $rootScope, $window, $filter) {

    });

As you can see each one of the dependencies are specified as an array of strings with the service definition function last. This way Angular will know what to inject into our service without issue. We then also repeat the dependencies in our function declaration so they are visible to our service.

Now let’s define the service interface. We are going to expose two methods:

  • initLocalizedResources – Responsible for loading the localized resource file from the server.
  • getLocalizedString – responsible for returning a localized string based on the given key.

So now our service looks as follows:

'use strict';

/*
 * An AngularJS Localization Service
 *
 * Written by Jim Lavin
 * http://codingsmackdown.tv
 *
 */

angular.module('localization', []).
    factory('localize', ['$http', '$rootScope', '$window', '$filter', function ($http, $rootScope, $window, $filter) {

        initLocalizedResources:function() {

        },

        getLocalizedString:function(key) {

        }
    });

Now that we have defined our interface, let’s implement the service and discuss what each function does.

'use strict';

/*
 * An AngularJS Localization Service
 *
 * Written by Jim Lavin
 * http://codingsmackdown.tv
 *
 */

angular.module('localization', []).
    factory('localize', ['$http', '$rootScope', '$window', '$filter', function ($http, $rootScope, $window, $filter) {
    var localize = {
        // use the $window service to get the language of the user's browser
        language:$window.navigator.userLanguage || $window.navigator.language,
        // array to hold the localized resource string entries
        dictionary:[],
        // flag to indicate if the service hs loaded the resource file
        resourceFileLoaded:false,

        successCallback:function (data) {
            // store the returned array in the dictionary
            localize.dictionary = data;
            // set the flag that the resource are loaded
            localize.resourceFileLoaded = true;
            // broadcast that the file has been loaded
            $rootScope.$broadcast('localizeResourcesUpdates');
        },

        initLocalizedResources:function () {
            // build the url to retrieve the localized resource file
            var url = '/i18n/resources-locale_' + localize.language + '.js';
            // request the resource file
            $http({ method:"GET", url:url, cache:false }).success(localize.successCallback).error(function () {
                // the request failed set the url to the default resource file
                var url = '/i18n/resources-locale_default.js';
                // request the default resource file
                $http({ method:"GET", url:url, cache:false }).success(localize.successCallback);
            });
        },

        getLocalizedString:function (value) {
            // default the result to an empty string
            var result = '';
            // check to see if the resource file has been loaded
            if (!localize.resourceFileLoaded) {
                // call the init method
                localize.initLocalizedResources();
                // set the flag to keep from looping in init
                localize.resourceFileLoaded = true;
                // return the empty string
                return result;
            }
            // amke sure the dictionary has valid data
            if ((localize.dictionary !== []) && (localize.dictionary.length > 0)) {
                // use the filter service to only return those entries which match the value
                // and only take the first result
                var entry = $filter('filter')(localize.dictionary, {key:value})[0];
                // check to make sure we have a valid entry
                if ((entry !== null) && (entry != undefined)) {
                    // set the result
                    result = entry.value;
                }
            }
            // return the value to the call
            return result;
        }
    };
    // return the local instance when called
    return localize;
} ]);

To begin with we declare the local variable localize that will be used as a wrapper for our object. We then need a couple of internal variables so we can store local data the service will use.

  • language – stores the user’s browser language. We will use this value to build the Url in order to request the appropriate localized resource file.
  • dictionary – stores the localized resource file.
  • resourceFileLoaded – indicates if the service has loaded the localized resource file, and is used to self init the service if needed.

I’ve also added a callback function that will be used when our http request succeeds. It will take the data retrieved from the web server and store it in the dictionary, update the init flag and broadcast that the localized resource file has been loaded.

The initLocalizedResources function takes the language we got from the user’s browser and creates a Url we can use to request the localized resource file from the web server. We also provide an error callback function should the request fail. By default we’ll assume there is no localized resource file and will request the default resource file.

The getLocalizedString function is called by consumers of the service to get the localized string for a specific key. By default we’ll return an empty string if the dictionary has not been loaded or there is no entry in the dictionary for the key.

Next, the function checks to see if the service has been initialized, if not it calls the initLocalizedResources function and then sets the init flag so we do not go into a continuous loop.

Finally, the function checks the dictionary for the key using the $filter service to retrieve the objects that match the filter parameters of key = value and then we further reduce the returned values by only taking the first item in the array. If we have a valid entry then the function returns the value and processing is complete.

Building the Filter

Now that we’ve finished with the service, let’s build the filter so we can easily use the service in our HTML.

First we’ll start off by chaining the filter definition off the factory definition by appending .filter() to the end of method. We’ll then define our filter by setting it’s name to ‘i18n’ along with the filter definition function that will be used to return the filter when called.

Next since the filter will be making calls to the localization service on behalf of our app we need to add the ‘localize’ service to our dependency list and ensure we include it in the filter definition function declaration.

The rest of the code for the filter is pretty simple since all we are doing is passing through the request to the localize service. so we are just going to return a function that calls the localize service with the given input.

That pretty much all we have to do. The final code for both the service and filter is given below:

'use strict';

/*
 * An AngularJS Localization Service
 *
 * Written by Jim Lavin
 * http://codingsmackdown.tv
 *
 */

angular.module('localization', []).
    factory('localize', ['$http', '$rootScope', '$window', '$filter', function ($http, $rootScope, $window, $filter) {
    var localize = {
        // use the $window service to get the language of the user's browser
        language:$window.navigator.userLanguage || $window.navigator.language,
        // array to hold the localized resource string entries
        dictionary:[],
        // flag to indicate if the service hs loaded the resource file
        resourceFileLoaded:false,

        successCallback:function (data) {
            // store the returned array in the dictionary
            localize.dictionary = data;
            // set the flag that the resource are loaded
            localize.resourceFileLoaded = true;
            // broadcast that the file has been loaded
            $rootScope.$broadcast('localizeResourcesUpdates');
        },

        initLocalizedResources:function () {
            // build the url to retrieve the localized resource file
            var url = '/i18n/resources-locale_' + localize.language + '.js';
            // request the resource file
            $http({ method:"GET", url:url, cache:false }).success(localize.successCallback).error(function () {
                // the request failed set the url to the default resource file
                var url = '/i18n/resources-locale_default.js';http://codingsmackdown.tv/?p=104&preview=true
                // request the default resource file
                $http({ method:"GET", url:url, cache:false }).success(localize.successCallback);
            });
        },

        getLocalizedString:function (value) {
            // default the result to an empty string
            var result = '';
            // check to see if the resource file has been loaded
            if (!localize.resourceFileLoaded) {
                // call the init method
                localize.initLocalizedResources();
                // set the flag to keep from looping in init
                localize.resourceFileLoaded = true;
                // return the empty string
                return result;
            }
            // make sure the dictionary has valid data
            if ((localize.dictionary !== []) && (localize.dictionary.length > 0)) {
                // use the filter service to only return those entries which match the value
                // and only take the first result
                var entry = $filter('filter')(localize.dictionary, {key:value})[0];
                // check to make sure we have a valid entry
                if ((entry !== null) && (entry != undefined)) {
                    // set the result
                    result = entry.value;
                }
            }
            // return the value to the call
            return result;
        }
    };
    // return the local instance when called
    return localize;
} ]).
    filter('i18n', ['localize', function (localize) {
    return function (input) {
        return localize.getLocalizedString(input);
    };
}]);

A Sample App

So now we have a service and a filter, but we need to show how to use both in an app. So included in the project on GitHub is a sample app that uses the service and filter to populate all of the text displayed in both the index.html and two partials.

First we need to add a dependency to our app so, it will load our service and filter at bootstrap time. so we are going to add the name of the module, ‘localization’, into the app’s dependency list as shown in the code below:

angular.module('localizeApp', ['localization']).
    config(['$routeProvider', function ($routeProvider) {
    $routeProvider.
        when('/', {templateUrl:'partials/home.html', controller:HomeController}).
        when('/edit/:index', {templateUrl:'partials/form.html', controller:EditPersonController}).
        when('/new', {templateUrl:'partials/form.html', controller:NewPersonController}).
        otherwise({redirectTo:'/'});
}]);

Now when ever we need to pull a localized string, we can use the filter. There are two ways you can call the filter, inside of a ng-bind method and by enclosing it inside of {{ }}. Below is a example of how to use each:

<div class="container-fluid" >
    <div class="row-fluid">
        <h2 ng-bind="'_FormControllerTitle_' | i18n"></h2>
    </div>
    <div class="row-fluid">
        <form name="myForm" class="form-horizontal span5 well">
            <div class="row-fluid">
                <input type="text" ng-model="person.FirstName" required id="FirstName" name="FirstName" class="input-large" placeholder="{{'_FirstNameLabel_' | i18n}}" />
            </div>
            <div class="row-fluid">
                 
            </div>
            <div class="row-fluid">
                <input type="text" ng-model="person.LastName" required id="LastName" name="LastName" class="input-large" placeholder="{{'_LastNameLabel_' | i18n}}"/>
            </div>
            <div class="row-fluid">
                 
            </div>
            <div class="row-fluid">
                <input type="text" ng-model="person.Email" required id="Email" name="Email" class="input-large" placeholder="{{'_EMailLabel_' | i18n}}"/>
            </div>
            <div class="row-fluid">
                 
            </div>
            <div class="row-fluid">
                <textarea ng-model="person.Bio" required id="Bio" name="Bio" class="input-xxlarge" placeholder="{{'_BioLabel_' | i18n}}"></textarea>
            </div>
            <div class="row-fluid">
                 
            </div>
            <div class="row-fluid">
                <button class="btn-primary" ng-click="savePerson()" ng-bind="'_SaveButtonLabel_' | i18n"></button>
                <button class="btn-primary" ng-click="cancel()" ng-bind="'_CancelButtonLabel_' | i18n"></button>
            </div>
        </form>
    </div>
</div>

Remember, you must use the {{ }} notation when you are not passing the value to a angular directive, if you don’t then the compiler will not handle the expression correctly and you’ll end up with text like ‘_BioLabel_’ | i18n all over your web page.

Below are two examples of the filter in use, the first is using the default of U.S. English and the second is when you change Chrome’s language to display Spanish. Since I don’t speak Spanish, I’ve converted the resource strings into Pig Latin so you can see the difference.

Example_1 Example_2

The Wrap Up

So in this article, I covered how to build a simple service and filter that can be used to localize you application based on the user’s language settings. By using a filter to front-end the service you have separated the concerns and provided a cross cutting service that can be used by your app without any of the controllers needing to deal with the service.

I also covered the basics of defining a service and a filter, injecting dependencies into both the service and filter, as well as how to use a filter in your HTML markup and directives.

Although this service handles a good share of the localization challenge for you a more advanced version would take advantage of Angular’s ng-pluralize directive and $locale service to help handle the harder semantics of language pluralization and gender. Hopefully it will get you started on the way to localizing you AngularJS apps and by following tutorial you’ve learned a little bit more about AngularJS.

Complete source for this tutorial can be found on GitHub at https://github.com/lavinjj/angularjs-localizationservice

I hope this tutorial helps you get started writing AngularJS Services and Filters. Drop me a comment on other AngularJS topics you’d like to see more tutorials on.

Loading Facebook Comments ...

Leave a Reply to Maximilian Cancel reply

  1. Great work. I tried to create a similar solution but it’s not as detailed as yours.
    About $window and $locale: as far as I found out the surroundings for that ‘en-us’ is indeed kind of hard coded into ‘default’ angularJS if you don’t provide any localisation file from their i18n. If you include maybe ‘angular-locale_ar-eg.js’ in your index.html your $locale.id will change to ‘ar-eg’
    My big problem is still how to include these files dynamically.

  2. Pingback: AngularJS – i18n | Dev by MX

  3. Thank you Jim! Great and interesting article. Still it would be awesome to have a standard solution for i18n in JS frameworks. However, it took Rails also quite a while to include one.

  4. Great article! I’m currently using my own “home grown” approach but I prefer yours. My solution was to create a t(…) method similar to rails and it accepts an optional object for interpolation. So, given my locale resource…

    { “add-user-success”: “The user, {{name}} was successfully created.” }

    … and it’s called like so…

    {{t(“add-user-success”,userObject)}}

    … what’s the best way to accomplish this with your approach?

    • You could modify the filter to accept the text string and an optional data object. In the filter function find/replace the data object key with the data. For example data object = {email: ’email@adress.com’}, in i18n string ‘Your email address is {{email}}’ and then in filter function you replace {{email}} with data.email using a regex.

        • Actually, if you look at the code in GitHub. I have already provided a way to handle dynamic data like you are looking for. There are a series of directives that will take dynamic data and populate them. I just need to provide a new post that describes how to use them.

          Regards,

          Jim Lavin

  5. Hi Jim,
    Thanks for this great service.

    I’m trying to use it for tooltips to support i18n too – without success.
    If I set i18n text in a div tag (See below), I see it perfect.
    How can I set it in tooltip attribute ?

    Text is working perfect in div tag:

    Is this a good way to show i18n tooltip ?

    Thanks.

    • Check out my last post Localizing Your AngularJS App: Yet Another Update, I added a directive that allows you to set attributes on tags to address just that problem.

      Good Luck, Let me know if you need any more help.

      • Hi,

        I used the i18nAttr directive, and saw the generated HTML – YES – the tooltip was there 🙂

        But I didn’t get the tooltip 🙁
        It was just in the HTML – but not rendered on screen.
        What’s wrong ?

        After a quick research, I saw that when I set a manual tooltip, and hover with the mouse over it – the Angular-UI library with the Twitter bootstrap wrapped it with a type of tooltip-css.

        Something in the localizationService probably miss one of the Angular-UI phases of registering events ?
        I’ve no idea,
        Do you have ?

        Thanks.

  6. Pingback: Localizing Your AngularJS App | TechnoVeille

  7. Thanks for sharing, great article! One question though:

    How would I use this in javascript, programmatically? I tried using $filter(‘i18n’)(‘some-key’) which works if the language resources have been loaded. If they have not, it returns an empty string. I could listen for the event ‘localizeResourcesUpdates’, but I would like to avoid that if possible. The view seems to work somehow anyway, it doesn’t use the event. If I delay the resource loading, the view starts out empty and automatically populates when the language resources are available. How does that work? Can we do similar for the javascript filtering calls?

    Thanks in advance!

    • Jonas,

      Filters are re-evaluated every time the scope is digested, so once the resource files are loaded they should update automatically.

      If you want you can add a .run method to your main app modules that injects the localize service and then call initLocalizedResources which will force the service to pull the file before the app starts up.

      Hope that helps.

      Jim

  8. Pingback: The AngularJS Magazine – What’s New this Week? | enCaliber

  9. Pingback: AngularJS Highlights – Week Ending 29 September 2013 | SyntaxSpectrum

  10. Hi everyone,
    I’m relatively new to Angular JS and web-development in general. I’m trying to use your library in my application for a while now but I didn’t manage to get it running.
    I followed the tutorial/set up steps and debugged a lot. I think I found why it’s not working for me.

    In the “localize.js” file the following lines return an “undefined”, although the dictionary is set up correctly.
    “var entry = $filter(‘filter’)(localize.dictionary, function(element) {
    return element.key === value;
    }
    )[0];”

    Does someone know why this is happening? As already mentioned I’m still a ‘noob’ and now running low on ideas :/.

    Kind Greetings from Germany

    Nils

  11. Hi everyone,
    I’m new to Angular and web-development in general.

    I’m trying to include this localization feature into my app, but I’m stuck for quite some time now. I think the error in my case occurs in the following code:

    var entry = $filter(‘filter’)(localize.dictionary, function(element) {
    return element.key === value;
    }
    )[0];

    The dictionary is filled with the whole JSON-file, but the entry variable is set to “undefined” afterwards.

    Does someone know whats going on there?

    Kind regards from Germany

    Nils

    • When say ‘link to a string’ are you talking about an a tag or are you talking about a dynamic string that is is created using a tokenizer?

      If you are talking about the later, check the source code out on Github, we’ve added tokenization so you can have a default string Hello {0}, that gets turned into Hello Diego, whenever the library sees KEY|VALUE.

      Let me know if that is not what you are looking for.

  12. How do you localize strings from ng-grid using your services?

    For example, the ng-grid table has labels like “Total Items:” which needs to be translated according to the selected language. ng-grid is looking for {{i18n.ngTotalItemsLabel}}.

    • It might be better to not use the filter and try using one of the directives instead, that way ng-grid can generate the html and then the directives will do the translation.

  13. Pingback: angular-localization

  14. Pingback: How to localized AngularJS app – IndaSoft