Creating multilingual support using AngularJS
"Create multilingual support for the web site". I just got this task for a client project, and I think it’s fair to assume that others will get this task in the future, so this blog post shows how I implemented this for my project. Because the web site is running as an AngularJS application, the localization had to be implemented using AngularJS / client side localization.
NB!: This blog post is a part of the of official AngularJS documentation
Contents
- Definition of terms
- Getting the current culture from the server to the client
- Support for datetime, number and currency
- Support for multiple languages (label texts)
- UPDATE: Simple plunker demo has been addded
Definition of terms
Scott Hanselman has formulated a good definition of the following terms [1]:
- Internationalization (i18n*) - Making your application able to support a range of languages and locales.
- Localization (L10n*) - Making your application support a specific language/locale.
- Globalization - The combination of Internationalization and Localization.
- Language - For example, Spanish generally. ISO code "es".
- Locale - Mexico. Note that Spanish in Spain is not the same as Spanish in Mexico, e.g. "es-ES" vs. "es-MX" (locale id).
* The number 18 in i18n and 10 in L10n reflects the number of letters between the first and last letter [2].
It's also useful knowledge to separate the CurrentCulture and CurrentUICulture properties of CultureInfo in .NET:
- CurrentCulture = Numbers, dates, currency etc.
- CurrentUICulture = UI localization/translation
Getting the current culture from the server to the client
First task was to get a hold of the CurrentCulture of the application on the client side. Hanselman's article also reflects on several ways of doing this, but in my project the path was already set, they stored the locale id in a cookie. Getting a cookie value in AngularJS is straightforward:
- Include the "angular-cookies.js" :
<script src="angular-cookies.js"></script>
- Include the directive for cookies in your app :
var app = angular.module('myApp', ['ngResource', 'ngCookies']);
- Include the cookies object in the controller :
app.controller('myController',['$scope', '$cookies', function ($scope, $cookies){
The cookie looked like this in the F12-tools resource view:
![cookie](/content/images/2014/10/cookie.png)In my Angular controller I can now access this by simply writing:
$cookies.lang
Support for datetime, number and currency
Currently, AngularJS supports i18n/l10n for datetime, number and currency filters [3]. The locale files are included in the Angular package:
![angular_locales](/content/images/2014/10/angular_locales.png)You can only include one of these or everyone but the last will be overwritten. And changing it on-the-fly is not supported by Angular. So you can include one file like this:
<html> <head> <script src="angular.js"></script> <script src="i18n/angular-locale_en.js"></script> </head> ... </html>
(Or combine them for better performance).
For multilingual support you need to create some semi-hacky logic to choose which one to include:
<script> var language = {get this from Thread.CurrentThread.CurrentUICulture} if(language == "en"){ $.getScript("i18n/angular-locale_en.js"); } else if (language == "da") { $.getScript("i18n/angular-locale_da.js"); } else{ $.getScript("i18n/angular-locale_no.js"); } </script>
When you have included a locale-file, you can make use of the built-in filter options. For example date has some localizable formats [4]:
- 'medium': equivalent to 'MMM d, y h:mm:ss a' for en_US locale (e.g. Sep 3, 2010 12:05:08 pm)
- 'short': equivalent to 'M/d/yy h:mm a' for en_US locale (e.g. 9/3/10 12:05 pm)
- 'fullDate': equivalent to 'EEEE, MMMM d,y' for en_US locale (e.g. Friday, September 3, 2010)
- 'longDate': equivalent to 'MMMM d, y' for en_US locale (e.g. September 3, 2010)
- 'mediumDate': equivalent to 'MMM d, y' for en_US locale (e.g. Sep 3, 2010)
- 'shortDate': equivalent to 'M/d/yy' for en_US locale (e.g. 9/3/10)
- 'mediumTime': equivalent to 'h:mm:ss a' for en_US locale (e.g. 12:05:08 pm)
- 'shortTime': equivalent to 'h:mm a' for en_US locale (e.g. 12:05 pm)
Which can be used like this in the HTML template:
{{periodStart | date:'mediumDate'}}
Renders as "Oct 11, 2013".
Support for multiple languages (label texts)
There's no built-in way to handle string translation in AngularJS. So I googled the web without finding a good and simple way to implements multi-language support. There is a viable 3rd party Angular module called "angular-translate" [5] that seems to do the trick, but for this project I wanted something really simple. Also, my scope was only to translate the label texts on the site. My solution was to follow the common "resource-file approach", by creating separate JSON-files holding each translation, and a "translationService" that reads the correct file and populates a variable holding the translation array, making this useable for common Angular data binding.
The 7 steps:
- Add JSON files organized as a key-value dictionary. The keys are common for each file. The naming convention is "translation_" + locale id + ".json".
- The English ("en") file will look like this:
{ "COLOR" : "Color", "HELLO" : "Hello", "HELLO_WORLD" : " Hello World!" }
The Norwegian ("nb-no") equivalent:{ "COLOR" : "Farge", "HELLO" : "Hallo", "HELLO_WORLD" : "Hallo verden!" }
And yes, I like capitalization for the keys. - Set JSON as MIME type in web.config (this can also be configured in IIS) :
<system.webServer> <staticContent> <mimeMap fileExtension=".json" mimeType="application/json" /> </staticContent> </system.webServer>
If JSON isn't set as a MIME type, you will get a 404.3 error when trying to retrieve the file. When the MIME type is set, you can access the file with the browser: ![browserresult](/content/images/2014/10/browserresult.png) - Create the translation service :
app.service('translationService', function($resource) { this.getTranslation = function($scope, language) { var languageFilePath = 'somepath_' + language + '.json'; $resource(languageFilePath).get(function (data) { $scope.translation = data; }); }; };
- The controller needs to call the translation service :
translationService.getTranslation($scope, $cookies.lang);
- Data bind to the translation array in the HTML code :
{{translation.HELLO_WORLD}}
Or use ng-bind:<ANY ng-bind="translation.HELLO_WORLD"></ANY>
- And the HTML will display "Hello World!" given that the locale id is "en".
Optional: Add local cache in terms of session storage in the translation service:
app.service('translationService', function ($resource) {this.getTranslation = function ($scope, language) { var path = 'somepath_' + language + '.json'; var ssid = 'someid_' + language; if (sessionStorage) { if (sessionStorage.getItem(ssid)) { $scope.translation = JSON.parse(sessionStorage.getItem(ssid)); } else { $resource(path).get(function(data) { $scope.translation = data; sessionStorage.setItem(ssid, JSON.stringify($scope.translation)); }); }; } else { $resource(path).get(function (data) { $scope.translation = data; }); } };
});
Conclusion:
With minimum time use and code, I have a working multilingual site. This is a basic approach, but maybe enough to handle the needs of the client?
Other approaches
If the scope is extended to include translation of more than label texts, or if you would like to have similar syntax as the one Angular supports for dates, currencies etc. - the answer would be to use filters. And possible offer localization as a dependency injected service like the built in support for i18n. Then the data binding syntax could become something like this:
{{HELLO_WORLD | translate }}
I'll leave the elaboration for a future blog post.
References
[2] - http://www.w3.org/2001/12/Glossary#I18N
[3] - http://docs.angularjs.org/guide/i18n
[4] - http://docs.angularjs.org/api/ng.filter:date
[5] - http://pascalprecht.github.io/angular-translate/