Localizing Your AngularJS App
original link :http://codingsmackdown.tv/blog/2012/12/14/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:
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.
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:
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:
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:
Now that we have defined our interface, let's implement the service and discuss what each function does.
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.
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:
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:
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:
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.
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.
- '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 >
- <div >
- <h2 ng-bind="'_FormControllerTitle_' | i18n"></h2>
- </div>
- <div >
- <form name="myForm" >
- <div >
- <input ng-model="person.FirstName" required id="FirstName" name="FirstName" placeholder="{{'_FirstNameLabel_' | i18n}}" />
- </div>
- <div >
-
- </div>
- <div >
- <input ng-model="person.LastName" required id="LastName" name="LastName" placeholder="{{'_LastNameLabel_' | i18n}}"/>
- </div>
- <div >
-
- </div>
- <div >
- <input ng-model="person.Email" required id="Email" name="Email" placeholder="{{'_EMailLabel_' | i18n}}"/>
- </div>
- <div >
-
- </div>
- <div >
- <textarea ng-model="person.Bio" required id="Bio" name="Bio" placeholder="{{'_BioLabel_' | i18n}}"></textarea>
- </div>
- <div >
-
- </div>
- <div >
- <button ng-click="savePerson()" ng-bind="'_SaveButtonLabel_' | i18n"></button>
- <button 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.
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.