JavaScript
Article
By Ahmad Ajmi

Multilingual Support for AngularJS

By Ahmad Ajmi

There are some cases in which providing a multilingual support is required. Sometimes it could be a good idea to provide support for different languages into the application you’re building and offer your users the possibility to view the content in different idioms. In this tutorial I’ll show you how to add a multilingual support to any AngularJS application.

We’ll build a single page application that requires a multilingual support with more than one language using AngularJS, so the user can switch instantly between languages without refreshing the page. In that case we need to do more things to our application, including translating its text, switching instantly between different languages, or changing the layout direction (RTL to LTR).

All the code developed in this article is available on GitHub.

Environment Setup

In the example I’m going to show you, I’ll use Bower and Gulp to make our development environment as more automated and flexible as possible. If they are not installed on your system yet or if you have never used them in your development workflow, I highly recommend to install and start learning more about them. Here is a list of articles that could be useful for this purpose:

As a first task, let’s set up Bower by running bower init in the command line inside a project directory that we’ll call multilingualwithangular. bower init will interactively create a manifest file called bower.json which will include some information about the project as well as a list of the previously installed front-end dependencies.

The next step is to install the initial required packages.

bower install angular angular-translate --save

Let’s set up Gulp and install these basic packages. First we need to run the command npm init and follow few simple steps to create a package.json file which will include some information about the project and how to manage Node.js modules.

Next, we’ll install Gulp within the project:

npm install gulp --save-dev

We’ll also need some Gulp dependencies for JavaScript and Sass and other automation tools.

npm install gulp-sass gulp-uglify gulp-concat run-sequence browser-sync --save-dev

At this point, we have to create an empty gulpfile.js configuration file within the project directory. It’ll be used to define our Gulp tasks such as JavaScript and Sass. You can take a look at the complete configuration file in my GitHub repository.

In the JavaScript task we’ll add two files, angular and angular-translate, plus the main JavaScript file inside a /js directory. Then, we’ll concatenate them together and use a library for Node.js called Uglify to compress and reduce the size of our file.

'use strict';

var gulp         = require('gulp');
var sass         = require('gulp-sass');
var concat       = require('gulp-concat');
var uglify       = require('gulp-uglify');
var runSequence  = require('run-sequence');
var browserSync  = require('browser-sync');

gulp.task('js', function(){
  return gulp.src([
    './bower_components/angular/angular.js',
    './bower_components/angular-translate/angular-translate.js',

    './js/app.js'])
    .pipe(concat('app.min.js'))
    .pipe(uglify())
    .pipe(gulp.dest('./js'))
});

gulp.task('serve', function() {
  browserSync({
    server: {
      baseDir: "./"
    }
  });
});

gulp.task('build', [], function() {
  runSequence('js');
});

gulp.task('default', ['build'], function() {});

Once done, we can run the gulp build task we’ve previously created. It’ll run the js task and then generate a /js/app.min.js file that will be included in a simple HTML file.

<!DOCTYPE HTML>
<html>
<head>
  <title>Multilingual AngularJS</title>
  <meta charset="utf-8">
</head>

<body>
  <script src="js/app.min.js"></script>
</body>
</html>

To open the project in a localhost environment, run gulp serve and then this will automatically open a browser tab directed to localhost:3000.

Adding Translation Using Angular-Translate

With these first configuration tasks in place, it’s time to take a step forward and add the translation support for the application text. We’ll work with Arabic and English as our main languages. They are completely different languages in regard of grammar, syntax, and in their writing directions (Right-to-Left Arabic and Left-to-Right English).

angular-translate is an AngularJS module that we can use to translate the text. It provides many interesting features like filters, directives, and asynchronous loading of i18n data.

First of all, let’s set up AngularJS and configure it with angular-translate

// js/app.js

var app = angular.module('Multilingual', ['pascalprecht.translate']);

app.config(['$translateProvider', function($translateProvider) {

  $translateProvider
  .translations('ar', {
    'HELLO': 'مرحبا'
  })
  .translations('en', {
    'HELLO': 'Hello'
  })
  .preferredLanguage('ar');

}]);

Then, let’s slightly modify the HMTL:

<html ng-app="Multilingual">

Then run gulp build from the command line to build the new changes in the JavaScript file. In the previous code snippet we have:

  • Created an Angular module called Multilingual.
  • Injected the angular-translate module as a dependency into our App as pascalprecht.translate.
  • Injected $translateProvider in the .config() method.
  • Registered the translation tables in different languages using the .translations() method and setting the language key such as en or ar as the first parameter.
  • Set the preferred language using .preferredLanguage() method, (this is important as we use more than one language, so we can teach angular-translate which one to use on first load).

Let’s see an example of angular-translate using the translate filter

<h2>{{ 'HELLO' | translate }}</h2>

Having too many filters in a view sets up too many watch expressions as described in the translate-directive documentation. A better and faster way to implement it is using the translate directive. Another reason to go with the directive is that there is a chance that the user will see the raw {{ 'HELLO' | translate }} before our template rendered by AngularJS while it’s being loading.

The way we can use the directive is to pass the translation ID as an attribute value of the translate directive.

<h2 translate="HELLO"></h2>

Sometimes we may need to know if we have missed some translation IDs. angular-translate-handler-log helps us solving this problem providing a very good method called useMissingTranslationHandlerLog() which logs warnings into the console for any missing translation ID. To use it we have to first install it. You can do it with Bower:

bower install angular-translate-handler-log --save

Then, update the JavaScript Gulp task:

gulp.task('js', function(){
  return gulp.src([
    './bower_components/angular/angular.js',
    './bower_components/angular-translate/angular-translate.js',

    // New file
    './bower_components/angular-translate-handler-log/angular-translate-handler-log.js',

    './js/app.js'])
    .pipe(concat('app.min.js'))
    .pipe(uglify())
    .pipe(gulp.dest('./js'));
});

Finally, run gulp build using this method directly on $translateProvider as:

$translateProvider
  .translations('ar', {
    'HELLO': 'مرحبا'
  })
  .translations('en', {
    'HELLO': 'Hello'
  })
  .preferredLanguage('ar')
  .useMissingTranslationHandlerLog();

If we missed up the translation for HELLO, thanks to this method we’ll get a warning message that says “Translation for HELLO doesn’t exist”.

Load Translation Files Asynchronously

Instead of adding translation data for different languages directly in the .config() method, there is another way to load them in an asynchronous and lazy loading. Actually, there are several ways to achieve this task, but in this tutorial we’ll use just the angular-translate-loader-static-files extension.

First we need to install the extension with Bower:

bower install angular-translate-loader-static-files --save

Once installed, we need to update the Gulp task with the extension file path and then run gulp build.

gulp.task('js', function(){
  return gulp.src([
    './bower_components/angular/angular.js',
    './bower_components/angular-translate/angular-translate.js',
    './bower_components/angular-translate-handler-log/angular-translate-handler-log.js',

    // New file
    'bower_components/angular-translate-loader-static-files/angular-translate-loader-static-files.js',

    './js/app.js'])
    .pipe(concat('app.min.js'))
    .pipe(uglify())
    .pipe(gulp.dest('./js'));
});

At this point, we need to create a /translations directory and add the languages translation files. The structure will look like the following:

translations
├── ar.json
└── en.json

Inside the ar.json file, write the content reported below:

{
  "HELLO": "مرحبا",
  "BUTTON_LANG_AR": "العربية",
  "BUTTON_LANG_EN": "الإنجليزية",
  "WELCOME_MESSAGE": "مرحباً في موقع AngularJS المتعدد اللغات"
}

On the contrary, in the en.json file save the following content:

{
  "HELLO": "Hello",
  "BUTTON_LANG_AR": "Arabic",
  "BUTTON_LANG_EN": "English",
  "WELCOME_MESSAGE": "Welcome to the AngularJS multilingual site"
}

Now, we can use the useStaticFilesLoader method to tell angular-translate which language files to load using a specific pattern by using the following approach:

prefix - specifies file prefix
suffix - specifies file suffix

And here’s how the JavaScript file changes:

// js/app.js

app.config(['$translateProvider', function($translateProvider) {

  $translateProvider
  .useStaticFilesLoader({
    prefix: '/translations/',
    suffix: '.json'
  })
  .preferredLanguage('ar')
  .useMissingTranslationHandlerLog();
}]);

If we want to add a prefix to the files, we can rename each of them using a prefix (in this case, locale-):

translations
├── locale-ar.json
└── locale-en.json

By applying this change, we have to update the app.js file as follows:

// js/app.js

app.config(['$translateProvider', function($translateProvider) {

  $translateProvider
  .useStaticFilesLoader({
    prefix: '/translations/locale-',
    suffix: '.json'
  })
  .preferredLanguage('ar')
  .useMissingTranslationHandlerLog()
}]);

Here angular-translate will concatenate our code as {{prefix}}{{langKey}}{{suffix}}, and then load /translations/locale-en.json file for example.

Switching between Different Languages

So far we’ve seen how to work with text translations for two languages. Nevertheless, we can’t still switch to the other language from the browser at runtime. To do this, we need to add a button for every language to switch from it.

<div ng-controller="LanguageSwitchController">
  <button ng-show="lang == 'en'" ng-click="changeLanguage('ar')" translate="BUTTON_LANG_AR"></button>
  <button ng-show="lang == 'ar'" ng-click="changeLanguage('en')" translate="BUTTON_LANG_EN"></button>
</div>

We can also create some $rootScope properties and use them on our HTML code to set up the initial layout direction and the lang attribute in the first load, binding them later whenever the language changes.

// js/app.js

app.run(['$rootScope', function($rootScope) {
  $rootScope.lang = 'en';

  $rootScope.default_float = 'left';
  $rootScope.opposite_float = 'right';

  $rootScope.default_direction = 'ltr';
  $rootScope.opposite_direction = 'rtl';
}])

angular-translate provides a handy method called use that takes a parameter and sets the language for us based on the passed parameter. Moreover, we’ll listen to the $translateChangeSuccess event, which is fired once a translation change is successful, to ensure the language has changed. Then, we can modify the $rootScope properties based on the selected language:

// js/app.js

app.controller('LanguageSwitchController', ['$scope', '$rootScope', '$translate',
  function($scope, $rootScope, $translate) {
    $scope.changeLanguage = function(langKey) {
      $translate.use(langKey);
    };

    $rootScope.$on('$translateChangeSuccess', function(event, data) {
      var language = data.language;

      $rootScope.lang = language;

      $rootScope.default_direction = language === 'ar' ? 'rtl' : 'ltr';
      $rootScope.opposite_direction = language === 'ar' ? 'ltr' : 'rtl';

      $rootScope.default_float = language === 'ar' ? 'right' : 'left';
      $rootScope.opposite_float = language === 'ar' ? 'left' : 'right';
    });
}]);

And also apply the following change to the markup:

<html lang="{{ lang }}" ng-app="Multilingual">

In my article titled Using Helper Classes to DRY and Scale CSS, you can see another example of using these directional properties in HTML as helper classes:

<div class="text-{{ default_float }}"></div>

Remember the Language

Up to this point, we’ve built the switching language feature and we’re able to change the language to use our favourite one. The next step is to let the application remember the language we choose, so the next time we launch it we don’t have to switch to that language again.

We’ll teach our application to remember the language using the browser localStorage to store the selected language and we’ll use angular-translate-storage-local extension for this purpose. As you can imagine, the next step is to install it. We’ll do it with Bower:

bower install angular-translate-storage-local --save

Running this command, we’ll also install angular-cookies and angular-translate-storage-cookie as dependencies. Once installed, we need to update the Gulp task with the new files running gulp build:

gulp.task('js', function(){
  return gulp.src([
    './bower_components/angular/angular.js',
    './bower_components/angular-translate/angular-translate.js',
    './bower_components/angular-translate-handler-log/angular-translate-handler-log.js',
    'bower_components/angular-translate-loader-static-files/angular-translate-loader-static-files.js',

    // New files
    './bower_components/angular-cookies/angular-cookies.js',
    './bower_components/angular-translate-storage-cookie/angular-translate-storage-cookie.js',
    './bower_components/angular-translate-storage-local/angular-translate-storage-local.js',

    './js/app.js'])
    .pipe(concat('app.min.js'))
    .pipe(uglify())
    .pipe(gulp.dest('./js'));
});

With this code in place, the next steps are:

  • Adding ngCookies as a dependency.
  • Telling $translateProvider to use localStorage via useLocalStorage()

Here is how we need to proceed:

var app = angular.module('Multilingual', [
  'pascalprecht.translate',
  'ngCookies'
  ]);

app.config(['$translateProvider', function($translateProvider) {
  $translateProvider
  .useStaticFilesLoader({
    prefix: '/translations/',
    suffix: '.json'
  })
  .preferredLanguage('ar')
  .useLocalStorage()
  .useMissingTranslationHandlerLog()
}]);

angular-translate will store the initial language as we set by preferredLanguage() with the key NG_TRANSLATE_LANG_KEY . It’ll assign the language as its value in the browser localStorage and then update it every time the user switches the language. When the user opens the application, angular-translate will retrieve it from localStorage.

Local Storage

--ADVERTISEMENT--

Working with Layout Direction

We have reached the presentation part. If you’re working with two languages with the same writing directions (for instance English and French), the configuration is complete. If one of the language direction is RTL and the other is LTR instead, we need do some extra work to adjust some layout scenarios.

Let’s say this is the CSS code for the LTR language (English):

.media-image { padding-right: 1rem; }

When it comes to the RTL language, the above code should be mirrored to be padding-left instead of padding-right:

.media-image { padding-left: 1rem; }

However, this is not a good practice at all since it’s time consuming and involves code repetitions:

[lang='ar'] .media-image {
  padding-right: 0;
  padding-left: 1rem;
}

To solve this issue we need to write some CSS code and allow the support for both the RTL language and the LTR one in an effective, automated, and dynamic way. With such approach, we won’t have to repeat or override CSS rules. I encourage you to read my article titled Manage RTL CSS with Sass and Grunt to learn more about this technique and how to use it in your projects.

We’ll implement it in this tutorial using Gulp and adding a Sass task that takes ltr-app.scss and rtl-app.scss. We’ll import the main Sass file in addition to direction specific variables inside them:

gulp.task('sass', function () {
  return gulp.src(['./sass/ltr-app.scss', './sass/rtl-app.scss'])
  .pipe(sass())
  .pipe(gulp.dest('./css'));
});

// Update the build task with sass
gulp.task('build', [], function() {
  runSequence('js', 'sass');
});

The sass/ltr-app.scss file should be as follows:

// LTR language directions

$default-float:       left;
$opposite-float:      right;

$default-direction:   ltr;
$opposite-direction:  rtl;

@import 'style';

And this is the code of sass/rtl-app.scss:

// RTL language directions

$default-float:       right;
$opposite-float:      left;

$default-direction:   rtl;
$opposite-direction:  ltr;

@import 'style';

Finally, this is an example of what sass/style.scss looks like:

body { direction: $default-direction; }

.column { float: $default-float; }

.media-image { padding-#{$opposite-float}: 1rem; }

With all this code in place, you can run gulp build and the Sass task will generate two files. css/rtl-app.css will have the code listed below:

/* css/rtl-app.css */

body { direction: rtl; }

.column { float: right; }

.media-image { padding-left: 1rem; }

The css/ltr-app.css file will have the content reported below:

/* css/ltr-app.css */
body { direction: ltr; }

.column { float: left; }

.media-image { padding-right: 1rem; }

The next and final step is to use these generated files dynamically, based on the current language. We’ll use the $rootScope‘s default_direction property to set the direction during the first load and then bind it when we change the language.

<link ng-href="css/{{ default_direction }}-app.css" rel="stylesheet">

Conclusions

As we’ve seen, using angular-translate is the way to go when it comes to AngularJS translation. It offers a lot of handy filters, directives and interesting tools to use. We have covered the translation process in many different ways, exploring how to switch between two languages. We’ve also discussed how to store a selected language in user browser storage and how to work with CSS to make the presentation layer more responsive with language directions.

I hope you enjoyed this tutorial. I’ve created a GitHub repo for this article and you can check out the code here. Feel free to share your comments in the section below.

Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account