AngularJS From Beginning: Localization - Part 12

I am here to continue the discussion around AngularJS. Today, we will discuss about the i18N support in AngularJS. Also in case you have not had a look at our previous articles of this series, go through the following links:

In this article, I will discuss about localization support of AngularJS. We cover the Angular constructs that help us localize an application and some libraries that extend multi-locale support in Angular.

Building multilingual apps

English is not the first language for a sizable population of the world, therefore building an app that just renders in English may not be a very wise idea. Imagine, with just some content localization, how much we can gain in terms of application reach. Internationalization is all about designing our app so that anyone in any region of the world can use the app in their native locale. Before we dive deeper into this topic, why not address a common confusion between what internationalization (I18n) and what localization (L10n) is?

Internationalization (I18n) is the process of making sure that the application has elements that can help it render locale-specific content, in other words, the application is adaptable to different locales. Localization (L10n) on the other hand, is the actual process of adapting parts of the application to specific locales. These could be the text shown, the date, the time, the current formatting, and other such content that is affected by locale change.

It is a normal tendency to just overlook this topic and believe that the app we are building does not need to handle I18n concerns. But even apps targeted at an English language audience need to manage locale variations that are country-specific. We may be able to work with the text content, but we still need to handle date, number, and currency variations.

For example, en-us (language code, English: United States) and en-gb (English: United Kingdom) have variations. The US date format is MM/DD/YY whereas for the UK, it is DD/MM/YY. The same holds true for currency formatting too.

Angular I18n support

AngularJS comes with support for I18n and L10n for date, number, and currency. No surprises here! Angular's filters currency, date, and number are locale-aware. To make these filters work according to a specific locale, we need to do some locale-specific configurations. AngularJS comes with more than 250 locale files that contain rules for formatting currency, dates, or numbers in specific locales. We need to include the locale-specific script file for every locale that we want to support. For example, say we want to support the German locale. We first include the locale-specific script file in our main page (index.html).

Every one of these script files declare a module ngLocale and contains configurations specific to that locale. To utilize the locale specific module, we need to reference the ngLocale module in our application, 
  1. angular.module('app',['ngLocale'])  

And that's it! We have altered the currency, date, and number filters behavior in accordance with the German locale. If we read some documentation on Angular I18n support (https://docs.angularjs.org/guide/i18n), we will soon realize that it has some notable limitations. There are two major shortcomings:

  • Firstly, we cannot change the current locale at runtime without a complete page reload
  • Secondly, there is no service/guidance available for localizing text content, which is a major disappointment

A good framework attracts great developers and great developers fill the gaps left by the framework. There are some good community projects to make Angular localization easy. We are going to discuss two of those here: one that targets the dynamic locale change, and the other that helps with content localization.

Locale changes using angular-dynamic-locale

To tackle the first limitation related to changing the locale on the fly, there is a community project angular-dynamic-locale (http://lgalfaso.github.io/angular-dynamic-locale/). This module allows us to dynamically change the locale once the application has been bootstrapped. Installing and using angular-dynamic-locale is easy.

Using angular-translate for text translations

Content translation may seem simple but there are some technical challenges here too. To understand these challenges, you need to understand how the process of content translation works. The task of content/text translation begins with identifying what content should be translated. Such content can broadly be classified in two categories, 

  • Fixed string literals: In any HTML page, there are parts that are dynamic and parts that are fixed. If we hardcoded strings anywhere in the view, those are fixed-string literals and are potential candidates for localization.

  • Dynamic string literals: Any string/text fragments retrieved from the server and rendered in the view are dynamic string literals. The exercise name shown during workout execution is a good example of this. 

Localizing fixed string literals is comparatively easy. Depending upon the number of locales to support, the content is generated for each locale and is made available to a tool/library. The library then based on the locale requested embeds the locale-specific content in the HTML. Once the translations are registered, we are ready to use these translations in our app. There are three ways to use them:

Using the $translate service:

We can retrieve a specific translation in the controller using the $translate service. Although not used much, it allows us to get the localized version of a translation key. For example:

  1. $translate('START.HEADER').then(function (header) {  
  2. $scope.header = header;  
  3. });   

The retrieval syntax is promise-based, and there is a reason for this. The library also allows asynchronous loading of localization content from remote locations.

Using the translate filter: Replace the literal string with a translation key and apply the translate filter; we are done:

  1. <h1>Ready for a Workout?</h1> // Replace this  
  2. <h1>{{'START.HEADER'|translate}}</h1> // With this  

Using the START.HEADER key, the translate filter replaces the interpolation with the localized version of the content from the registered translations. Remember, every translation filter adds to the number of watches on the page. The static content now becomes dynamic and for a large page there may be a performance impact due to this. A better but restricted approach is to use a directive for translations.

Using the translate directive: The translate directive does something similar to the translate filter. The directive replaces the inner content of HTML on which it is declared with the translated text. This is how we use directives for translations:

  1. <h2 translate>START.WORKOUTS</h2> <-- OR --> <h2 translate="START.WORKOUTS"></h2>  

The nice thing about the translate directive is that it supports interpolations too, allowing a dynamic translation key. For example, look at this:

  1. <h2 translate>{{scopeVariable}}</h2> OR <h2 translate="{{scopeVariable}}"></h2>  

Prefer the directive version over filter version as the directive does not create a watch-like filter. But remember, the translate directive may not work everywhere and in such scenarios, the filter is the only choice. A good example of this is an attribute that needs localization:

  1. <input type="text" placeholder="{{'START.SEARCH'|translate}}" />  
 The directive version cannot handle this.

So far, we have looked at how to define translations and how to use them in views and otherwise. The last thing that you need to understand is how the $translate service selects the default locale and how we specify which locale to use for the translation. We can set the default locale during the configuration stage using this: 

  1. $translateProvider.preferredLanguage('en');   
This sets the default locale to en (English) and if the locale is never set during app execution, the en locale is used. To actually change the locale any time during the app execution, we can call the following code:
  1. $translate.use('de'); // Change locale to German  

Angular-translate is a mature library and we have just scratched the surface in terms of its capabilities. Look at the angular-translate documentation to learn more about the library.

Now for the demonstrate the above concept, I write down the below code,
 
MyApp.js 
  1. var MyApp = angular.module('MyApp', ['translate']);  
  2.   
  3. MyApp.run(function ($rootScope, Language) {  
  4.     $rootScope.Language = Language;  
  5. })  
  6.   
  7. // Service definition  
  8. MyApp.factory('Language'function ($translate) {  
  9.     var rtlLanguages = ['en-ar'];  
  10.   
  11.     var isRtl = function () {  
  12.         var languageKey = $translate.proposedLanguage() || $translate.use();  
  13.         for (var i = 0; i < rtlLanguages.length; i += 1) {  
  14.             if (languageKey.indexOf(rtlLanguages[i]) > -1)  
  15.                 return true;  
  16.         }  
  17.         return false;  
  18.     };  
  19.     return {  
  20.         isRtl: isRtl  
  21.     };  
  22. });  
  23.   
  24. MyApp.config(function ($translateProvider) {  
  25.     $translateProvider.translations('en', {  
  26.         HEADLINE: 'Hello there, This is my awesome app!',  
  27.         INTRO_TEXT: 'And it has i18n support!',  
  28.         BUTTON_TEXT_EN: 'english',  
  29.         BUTTON_TEXT_DE: 'german',  
  30.         BUTTON_TEXT_AE: 'Arabic'  
  31.     })  
  32.     .translations('en-de', {  
  33.         HEADLINE: 'Hey, das ist meine großartige App!',  
  34.         INTRO_TEXT: 'Und sie untersützt mehrere Sprachen!',  
  35.         BUTTON_TEXT_EN: 'englisch',  
  36.         BUTTON_TEXT_DE: 'deutsch',  
  37.         BUTTON_TEXT_AE: 'Arabisch'  
  38.     })  
  39.     .translations('en-ar', {  
  40.         HEADLINE: 'هذا هو اختبار التدويل!',  
  41.         INTRO_TEXT: 'إضفاء الطابع المحلي على القيام به!',  
  42.         BUTTON_TEXT_EN: 'الإنجليزية',  
  43.         BUTTON_TEXT_DE: 'ألماني',  
  44.         BUTTON_TEXT_AE: 'العربية'  
  45.     },  
  46.     { rtl: true });  
  47.     $translateProvider.useSanitizeValueStrategy('escaped');  
  48.     $translateProvider.preferredLanguage('en');  
  49. });  
Index.html
  1. <!DOCTYPE html>  
  2. <html ng-app="MyApp" ng-controller="TranslateController" ng-class="{'rtl':Language.isRtl()}" dir="{{(Language.isRtl())?'rtl':'ltr'}}" lang={{Lang}}>  
  3. <head>  
  4.     <title></title>  
  5.     <script src="angular.min.js"></script>  
  6.     <script src="angular-translate.js"></script>  
  7.     <script src="MyApp.js"></script>  
  8.     <script src="Index.js"></script>  
  9.     <script>  
  10.         var locale = JSON.parse(sessionStorage.getItem('Locale'));  
  11.         if (locale) {  
  12.             document.write('<script src="../scripts/i18n/angular-locale_' + locale + '.js"><\/script>');  
  13.         }  
  14.     </script>  
  15.   
  16. </head>  
  17. <body>  
  18.     <h1>Localization</h1>  
  19.     <div>  
  20.         <button ng-click="changeLanguage('en-de')" translate="BUTTON_TEXT_DE"></button>  
  21.         <button ng-click="changeLanguage('en')" translate="BUTTON_TEXT_EN"></button>  
  22.         <button ng-click="changeLanguage('en-ar')" translate="BUTTON_TEXT_AE"></button>  
  23.     </div>  
  24.     <div>  
  25.         <h2>{{ 'HEADLINE' | translate }}</h2>  
  26.         <p>{{ 'INTRO_TEXT' | translate }}</p>  
  27.     </div>  
  28.     <div>  
  29.         Number : {{106.89 | number}}  
  30.         <br />  
  31.         Currency : {{1123.00 | currency}}  
  32.         <br />  
  33.         Today's Date :- {{TodayDate | date : "fullDate"}} (Full Date)<br />  
  34.                         {{TodayDate | date : "longDate"}} (Long Date)<br />  
  35.                         {{TodayDate | date : "shortDate"}} (Short Date)<br />  
  36.     </div>  
  37. </body>  
  38. </html>  
Index.js
  1. MyApp.controller('TranslateController'function ($translate, $scope, $window) {  
  2.     if (sessionStorage.getItem("Locale") == null || sessionStorage.getItem("Locale") == undefined) {  
  3.         $scope.Lang = 'en';  
  4.         sessionStorage.setItem("Locale", JSON.stringify($scope.Lang));  
  5.     }  
  6.     else {  
  7.         $scope.Lang = JSON.parse(sessionStorage.getItem('Locale'));  
  8.     }  
  9.     $scope.selectedLanguage = $translate.use($scope.Lang); //default  
  10.     $scope.TodayDate = new Date();  
  11.   
  12.     $scope.changeLanguage = function (langKey) {  
  13.         $translate.use(langKey);  
  14.         sessionStorage.setItem("Locale", JSON.stringify(langKey));  
  15.         $window.location.reload();  
  16.     };  
  17. });  
The output of the above code is,
 
English Language  
 
German Language