Adding translation using angular-translate to an angularjs app

This post adds internationalisation (i18n) to the app using the angular-translate library.  This library allows “on-the-fly” translation of content, and permits dynamically changing languages with automatic page refresh.

I’m using the sample league app that was built in this series, building on the code base from github that existed at the end of tutorial 7.  This commit also included the devise authentication from this post, or hit the tutorials menu at the top and select the Rails 3 tutorial.

We’ll start off by adding the angular-translate module to our project, we’ll also add the partial file loader, which allows us to load translations a page at a time:

bower install angular-translate --save-dev
bower install angular-translate-loader-partial --save-dev

Add the relevant files into build.config.js, as follows:

      'vendor/angular-grid/build/ng-grid.js',
      'vendor/angular-translate/angular-translate.js',
      'vendor/angular-translate-loader-partial/angular-translate-loader-partial.min.js'

Create a common service for translation, this will configure our translation code, in particular it will configure the location of our translation files. Create a directory src/common/translation, and a file src/common/translation/translation.js:

angular.module('angularTranslateApp', ['pascalprecht.translate'])
  .config(function($translateProvider, $translatePartialLoaderProvider ) {
    $translateProvider.useLoader('$translatePartialLoader', {
      urlTemplate: '/UI/assets/translation/{lang}/{part}.json'
    });

  $translateProvider.preferredLanguage('en-AU');
});

This is configuring the translation module, telling it that we want to use the partial loader to get our files, and that we want our translation files to live under assets/translation/{language}/{partial name}.json. We’re choosing to name our languages using the internet naming conventions – so we’ll have en-US, en-AU, fr-FR etc. Our directory will look something like:

  translations/
    en-AU/
      team.json
      club.json
    fr-FR/
      team.json
      club.json

We also need to add this module to the app.js dependencies:

angular.module( 'league', [
  'templates-app',
  'templates-common',
  'common.error_handling',
  'common.authentication',
  'pascalprecht.translate',
  'angularTranslateApp',
  'league.home',
  'league.about',
  'league.club',
  'league.team',
  'league.login',
  'ui.state',
  'ui.route'
])

We then move on to create some translation files, create the directory src/assets/translation/en-AU/, and within it the file team.json:

{
  "Team_Title": "Team Functions",
  "Team_Informational_Message": "This is the team maintenance function",
  "Team_Filter_Title": "Filter",
  "Team_New_Button": "New Team"
}

This is a simple json hash, with each translation key and the associated decode. We duplicate this file for each of the languages we want to support, putting it in the directory associated with that language.

Update team.tpl.html to have translations:

<div>
  <h1>{{"Team_Title" | translate}}</h1>
  <p>{{"Team_Informational_Message" | translate}}</p>
</div>

<div class="body">
  <strong>{{"Team_Filter_Title" | translate}}:</strong><input type="text" ng-model="filterOptions.filterText" />
  <div class="gridStyle" ng-grid="gridOptions"></div>
  <button ng-click="newTeam()" class="btn btn-primary" >{{"Team_New_Button" | translate}}</button>
</div>

This is using the translate filter to translate our key to the relevant decode, given the language we’ve configured (at the moment just our default language).

Finally, we’ll tell our team.js module to get the translation strings for us (and add translation stuff as a dependency):

.controller( 'TeamCtrl', function TeamController( $scope, TeamRes, $dialog, $stateParams, $translate, $translatePartialLoader ) {
  $scope.teams = TeamRes.query();
  $translatePartialLoader.addPart('team');
  $translate.refresh();

  $scope.club_id = parseInt($stateParams.club_id, 10);

Save and run that, you should have a teams page similar to before.

Create now a french translation of the same, so create the directory assets/translation/fr-FR/, and copy the team.json file into it. Edit the team.json file to have french translations:

{
  "Team_Title": "Fonctions d'équipe",
  "Team_Informational_Message": "Il s'agit de la fonction de maintenance de l'équipe",
  "Team_Filter_Title": "Filtrez",
  "Team_New_Button": "nouvelle équipe"
}

Go to common/translation/translation.js, and change the default language to fr-FR. You should get a translated teams page.

You’ll note that we’ve translated the things that are on the html page itself, but the column headers on the grid are not translated. We need to translate these in code, since they’re configured in the javascript. The combination of the partial loader and ngGrid makes this difficult. The grid renders before the translation file is available, and doesn’t automatically update, and worse still, the process for updating column titles after they have been rendered is complex.

The advice is that we need to set the columnDefs array to a variable, then replace that variable entirely (not update it in place). We also get a very specific syntax for linking the columnDefs to that variable on our scope.

Modify the team.js controller as follows:

.controller( 'TeamCtrl', function TeamController( $scope, TeamRes, $dialog, $stateParams, $translate, $translatePartialLoader ) {
  $scope.teams = TeamRes.query();
  $translatePartialLoader.addPart('team');
  $translate.refresh();

  $scope.club_id = parseInt($stateParams.club_id, 10);

  if($scope.club_id) {
    $scope.filterOptions = {
      filterText: 'club_id:' + $scope.club_id
    };
  }
  else {
    $scope.filterOptions = {
      filterText: ''
    };
  }

  $scope.setColumnDefs = function(){
    var columnDefs = [
      {field: 'id', displayName: $translate('Team_id_Column')},
      {field: 'name', displayName: $translate('Team_name_Column')},
      {field: 'captain', displayName: $translate('Team_captain_Column')},
      {field: 'club_id', displayName: $translate('Team_club_id_Column'), groupable: true, visible: false},
      {field: 'club_name', displayName: $translate('Team_club_name_Column'), groupable: true},
      {field: 'date_created', displayName: $translate('Team_date_created_Column'), cellFilter: "date:mediumDate"},
      {displayName: $translate('Team_Edit_Column'), cellTemplate: '{{"Team_Edit_Button" | translate}} '},
      {displayName: $translate('Delete'), cellTemplate: '{{"Team_Delete_Button" | translate}} '}
    ];
    $scope.columnDefs = columnDefs;
  };

  $scope.setColumnDefs();

  $scope.gridOptions = {
    data: 'teams',
    columnDefs: 'columnDefs',
    multiSelect: false,
    filterOptions: $scope.filterOptions,
    showColumnMenu: true,
    showGroupPanel: true,
    groups: ["club_name"]
  };

  $scope.$on('$translateChangeSuccess', function() {
    $scope.setColumnDefs();  
  });

What we’re doing is creating a function that sets the column defs, that function uses the translation routines. We call that on entry, it will fail to translate the column headers. We then listen for the translation success event, which is fired once the translation files have loaded, and we call again to update the column headers.

Also update our australian and french translations:

{
  "Team_Title": "Team Functions",
  "Team_Informational_Message": "This is the team maintenance function",
  "Team_Filter_Title": "Filter",
  "Team_id_Column": "Team Id",
  "Team_name_Column": "Team Name",
  "Team_captain_Column": "Captain",
  "Team_club_id_Column": "Club Id",
  "Team_club_name_Column": "Club Name",
  "Team_date_created_Column": "Date Created",
  "Team_Edit_Column": "Edit",
  "Team_Delete_Column": "Delete",
  "Team_New_Button": "New Team",
  "Team_Edit_Button": "Edit",
  "Team_Delete_Button": "Delete"
}
{
  "Team_Title": "Fonctions d'équipe",
  "Team_Informational_Message": "Il s'agit de la fonction de maintenance de l'équipe",
  "Team_Filter_Title": "Filtrez",
  "Team_id_Column": "équipe Id",
  "Team_name_Column": "Nom de l'équipe",
  "Team_captain_Column": "Capitaine",
  "Team_club_id_Column": "Club Id",
  "Team_club_name_Column": "Nom du club",
  "Team_date_created_Column": "Date de création",
  "Team_Edit_Column": "Modifier",
  "Team_Delete_Column": "Supprimer",
  "Team_New_Button": "nouvelle équipe",
  "Team_Edit_Button": "Modifier",
  "Team_Delete_Button": "Supprimer"
}

The final thing we want to do in our translation is to tie the language to the user. We’ll permit the language to be stored on the user table, setting it when the user registers, and we’ll use that language whenever someone logs on.

Start off by adding language to the rails user model:

rails generate migration AddLanguageToUser

And edit the migration to add the column:

class AddLanguageToUser < ActiveRecord::Migration
  def change
    add_column :users, :language, :string
  end
end

Then migrate it:

rake db:migrate

Modify the app/login/login.js in the register method:

  $scope.register = function() {
    $scope.submit({method: 'POST', 
                   url: '../users.json',
                   data: {user: {email: $scope.register_user.email,
                                 password: $scope.register_user.password,
                                 password_confirmation: $scope.register_user.password_confirmation,
                                 language: $scope.register_user.language}},
                   success_message: "You have been registered and logged in.  A confirmation e-mail has been sent to your e-mail address, your access will terminate in 2 days if you do not use the link in that e-mail.",
                   error_entity: $scope.register_error});
  };

And the submit function to set the language on login (this is a bit ugly, we could refactor to make this more elegant):

  $scope.submit = function(parameters) {
    $scope.reset_messages();

    $http({method: parameters.method,
           url: parameters.url,
           data: parameters.data})
      .success(function(data, status){
        if (status == 201 || status == 204){
          parameters.error_entity.message = parameters.success_message;
          $scope.reset_users();
          if(parameters.url=='../users/sign_in.json' || parameters.url=='../users/sign_up.json') {
            $translate.use(data.user.language);
          }
        } else {
....

You’ll also need to add $translate to the dependencies on the login module:

.controller( 'LoginCtrl', function LoginController( $scope, $http, $translate ) {

And the unit tests will fail unless you update them, you need to update any sign_in calls to return an object that includes a language:

        scope.httpBackend.expect('POST', '../users/sign_in.json', '{"user":{"email":"test@example.com","password":"apassword"}}').respond(201, '{"user":{"language": "en-US"}}');

We need to put the language field on the login.tpl.html page, towards the bottom of the register block:

  <div ng-class="{error: register_error.errors.language}">Language: <input ng-model="register_user.language" />
    <div ng-show="register_error.errors.language">
      <div ng-repeat="field_error in register_error.errors.language">{{field_error}}</div>
    </div>
  </div>

And finally to make the language field accessible on the user.rb model:

  attr_accessible :email, :password, :password_confirmation, :language

You may also want to change index.html to replace the about block with a login block, since you’re going to be logging in and out testing this:

  <div ng-class="{error: register_error.errors.language}">Language: <input ng-model="register_user.language" />
    <div ng-show="register_error.errors.language">
      <div ng-repeat="field_error in register_error.errors.language">{{field_error}}</div>
    </div>
  </div>

Register a new user with en-AU as their language. The teams page should be in Australian. Register a user with french, the teams page for them should be in French.

The code for this section of the tutorial is on github at translation.

Advertisements

15 thoughts on “Adding translation using angular-translate to an angularjs app

  1. Pingback: AngularJS and Rails CRUD application using ng-boilerplate and twitter bootstrap: Tutorial Index | technpol

  2. Pascal, you’re more than welcome. You were amazingly helpful as I worked through the initial integration, contributing back is the least I can do.

  3. Awesome article. Just two things need to be changed. First … the bower command is: bower install angular-translate-loader-partial (wrong package name) + it’s $translate.use and in your article $translate.uses 🙂

  4. Thanks for reading trkisf2. The angular loader is a typo, fixed. The use v’s uses is actually a recent upgrade, I’ve updated the body since probably everyone new will be using the newer version. I hope to do an update on this one day soon, I have some newer tricks and traps, but it’s on my list to get around to. Good luck.

  5. Pingback: AngularJS Localization | Techie Note

  6. Pingback: AngularJS and Rails CRUD application using ng-boilerplate and twitter bootstrap | Gatelockservice

  7. Hi, thanks for article

    I want to ask how to $translatePartialLoader.addPart() globally so i dont need to add them for each my controller 🙂

    Thanks

  8. For me, I built an i18n service, and it initialised a base set of parts up front. But to me the point of partial loader is that you load incrementally – if you’re just going to default load them all right up front, you’re better using the static loader.

  9. Great article, however it’s a bummer that you have to prefix all your labels with some identifier (i.e: Team_, Club_) so that all your keys are unique.

  10. Yeah, in my real app I wrote a directive for each widget, and that directive did auto prefixing based on the entity name.

    It’s mostly for uniqueness in testing. In theory you don’t have to do it, but then you end up with pages with similar labels, and it’s harder to debug when things go wrong in your test cases.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s