AngularJS, code.explode, Coding

Localizing Your AngularJS Apps: Update

In my previous article on Localization, Localizing Your AngularJS App, I provided a simple service and filter that allows you to provide language translations based on a translation file. Initially this worked for about 50% of our transalation needs, however once we started looking at the overall performance of our AngualrJS App it was apparent that I needed to tweak things a bit.

This article covers the additional changes that has been made to my angularjs-localizationservice project out on GitHub.

The new service, has the following new features:

  • A new directive to help improve performance and handle dynamic localized resource strings
  • Updated the localize service to allow changing of the language on the fly
  • Fixed an issue with getLocalizedResourceString to be more efficient and simpler

Filters Degrade Performance

One of the first things we found out when we had a large page to provide translations for, is that the more filters you use on a page the slower your page becomes and performance will degrade significantly. This is mainly due to the fact that the filters are being re-evaluated with every watch cycle. So, I created a directive that provided the translation functionality.

.directive('i18n', ['localize', function(localize){
        var i18nDirective = {
            restrict:"EAC",
            updateText:function(elm, token){
                var values = token.split('|');
                if (values.length <= 1) {
                    // construct the tag to insert into the element
                    var tag = localize.getLocalizedString(values[0]);
                    // update the element only if data was returned
                    if ((tag !== null) && (tag !== undefined) && (tag !== '')) {
                        if (values.length > 1) {
                            for (var index = 1; index < values.length; index++) {
                                var target = '{' + (index - 1) + '}';
                                tag = tag.replace(target, values[index]);
                            }
                        }
                        // insert the text into the element
                        elm.text(tag);
                    };
                }
            },

            link:function (scope, elm, attrs) {
                scope.$on('localizeResourcesUpdates', function() {
                    i18nDirective.updateText(elm, attrs.i18n);
                });

                attrs.$observe('i18n', function (value) {
                    i18nDirective.updateText(elm, attrs.i18n);
                });
            }
        };

        return i18nDirective;
    }]);

So let’s discuss what’s going on in this new directive.

First off, we have set a dependency on the localize service so we can look up the translated strings. I have also restricted the directive to Elements, Attributes and Classes, this should pretty much cover everything you need in an HTML app.

Second, I added the updateText function which is responsible for looking up the localized text and update the attached element’s text. This function first splits the passed in token on ‘|’ boundaries in order to handle dynamic strings formatted as “First Name: {0}, Last Name: {1}”. If there are additional values passed in token such as “_WELCOME_STRING_|Jim|Lavin” the resulting string would end up as “First Name: Jim, Last Name: Lavin” based on the previous example.

If a valid string is returned from the localize service, the updateText function will parse the returned string for dynamic values and then update the elment’s text value. If the localize service does not have a matching resource string, it will not update the element.text value.

Finally, The link function sets up two observers. The first observer will call the updateText function whenever the localize Service broadcasts the ‘localizeResourcesUpdates’ message. This allows you to change languages on the fly without having to refresh the web page. We will discuss changing languages on the fly later in this article. The second observer will call the updateText function whenever the i18n attribute changes. This allows you to dynamically change the translated text by binding the value to your controller’s model.

You can now use the directive as shown below:

<div class="container-fluid">
    <div class="row-fluid">
        <h2 data-i18n="_HomeControllerTitle_"></h2>
    </div>
    <div class="row-fluid">
        <div class="well">
            <div class="span1" data-i18n="_NameHeader_"></div>
            <div class="span2" data-i18n="_EmailHeader_"></div>
            <div class="span3" data-i18n="_BioHeader_"></div>
            <div class="span1"><a href="#/new"><span class="icon icon-plus"></span></a></div>
        </div>
        <div class="row-fluid" ng-repeat="person in People">
            <div class="span1" ng-bind="person.FirstName + ' ' + person.LastName"></div>
            <div class="span2" ng-bind="person.Email"></div>
            <div class="span3" ng-bind="person.Bio"></div>
            <div class="span1"><a href="#/edit/{{$index}}"><span class="icon icon-edit"></span></a></div>
        </div>
    </div>
</div>

Notice the data-i18n attributes that contain the tokens for the resource strings. The nice thing about moving to the directive is that since I am using observers to watch the translation token values, the directive will only be called during the link phase, when the language changes or when the attribute value changes, keeping execution down to a minimum.

Updates to the Localize Service

Based on feedback from the GitHub community and tweaks to handle an initial load issue, I’ve made some updates to the localize service and will highlight them below.

    .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');
            },

            setLanguage: function(value) {
                localize.language = value;
                localize.initLocalizedResources();
            },

            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 = '';

                // 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, function(element) {
                            return element.key === value;
                        }
                    )[0];

                    // set the result
                    result = entry.value;
                }
                // return the value to the call
                return result;
            }
        };

        // return the local instance when called
        return localize;
    } ]);

First, I added a setLanguage function that allows you to set the culture of the app on demand. It will change the internal language variable and call initLocalizedResources to load the correct language resource file from the server.

Second, based on an issue filed by Stein Desmet, I have changed the getLocalizedString function to pass a function to the $filter method which determines if the localized resource’s key matches the value passed in. This is needed since by default the AngularJS $filter function uses substring matching which required a lot of extra code to find the matching value if an array of matches were returned. The new method returns a single match and there is no need for the extra code, simplifying the overall solution.

The Wrap Up

The changes to the localization service and components makes it a great resource when you need to have an AngularJS app that supports multiple languages. Hopefully you’ll find it as useful as I have in my projects.

Good Luck!

Standard
Loading Facebook Comments ...

Got a comment? I'd love to hear it!