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.