Skip to content
This repository has been archived by the owner on Jul 30, 2024. It is now read-only.

Latest commit

 

History

History
240 lines (178 loc) · 11.3 KB

README.md

File metadata and controls

240 lines (178 loc) · 11.3 KB

i18n-node in an AngularJS application

Caution

This was an incredibly useful, and fun project to work on during the early times of NodeJS and AngularJS. Today, this project hopefully has no purpose anymore and is officially archived as of 30-07-2024.

The problem

I have an application that uses Node and express. I use i18n-node for internationalization.

On the frontend, I use AngularJS and I have certain strings (in controllers, services and templates) which I also need to have translated. I could use angular-translate here, but I would really like to use the same resources which I have already generated with i18n-node on my backend.

The solution

Quick start

The solution is available in npm and bower packages for the backend and frontend respectively. So you can just...

  1. ...install them with:

    npm install i18n-node-angular
    bower install i18n-node-angular
  2. ...register the express extensions:

    Note: Up until 1.4.0 it was always possible to add previously unknown translation literals to the translation files. This is no longer supported due to security implications.
    The feature is only available in development environments where NODE_ENV is set to development.

    var i18n = require( "i18n" );
    var i18nRoutes = require( "i18n-node-angular" );
    
    // The order of these calls matters!
    app.use( i18n.init );
    app.use( i18nRoutes.getLocale );
    
    // This line should be removed with express 4.0
    app.use( app.router );
    i18nRoutes.configure( app );
  3. ...put the locale into the DOM (this is using jade):

    html( ng-app="yourApp", i18n-locale=i18n.getLocale() )
  4. ...use the translations in Angular:

    // Depend on i18n module
    var yourApp = angular.module( "yourApp", [ "ngRoute", "i18n" ] )
      .config( [ "$routeProvider", "$locationProvider", function( $routeProvider, $locationProvider ) {
                 $routeProvider
                   .when( "/", {
                            templateUrl : "examples",
                            controller  : IndexController
                            /* By enabling the resolver below, the i18n service won't be injected into
                             IndexController until the locale has been loaded from the server.
                             To enable the resolver, just remove these two characters ↓
                             */                                                       /*
                             ,resolve  : {
                               i18n    : [ "i18n", function( i18n ) { return i18n.i18n(); } ]
                             }
                             //*/
                          } );
                 $locationProvider.html5Mode( true );
               } ] )
      .factory( "MyService", function( i18n ) {
                  // Use i18n service injected into this service.
                  console.log( i18n.__( "My translation phrase" ) );
                } )
      .controller( "IndexController", [ "i18n", "$scope", IndexController ] );
    
    function IndexController( i18n, $scope ) {
      // Inject the service into the scope, so we can access __() and 'loaded'.
      $scope.i18n = i18n;
      // Try to instantly translate a phrase. This can fail, because the locale might not have been loaded yet.
      console.log( "Instant: " + i18n.__( "My translation phrase" ) );
      i18n.ensureLocaleIsLoaded().then( function() {
        // Chaining on the promise returned from ensureLocaleIsLoaded() will make sure the translation is loaded.
        console.log( "Insured: " + i18n.__( "My translation phrase" ) );
      } );
    }

Core functionality

  1. The users locale must be injected using the i18n-locale directive. The value of it is observed, so you can change the locale at any time.

  2. If you want to use the i18n service, you'd generally want to use the i18n.i18n promise in a resolver for your route. This way you don't have to use ensureLocaleLoaded all the time.

    In general, if you inject i18n into your controller, you probably want to have i18n : [ "i18n", function( i18n ) { return i18n.i18n(); } ] in your resolver. i18n.i18n returns a promise to return the service once the locale has been loaded. At that point in time, your controller will be instantiated and have the service injected; ready for you to use.

  3. In your view, pass your translatable string through the i18n filter. This will cause items to be automatically re-translated if the locale changes.

    If you want to use pluralization, pass the count into the i18n filter and supply the singular and plural terms as additional arguments, like {{2|i18n:"%s cat":"%s cats"}}.

    If you want to hide something until the translation is loaded, inject the i18n service into your scope and use i18n.loaded with ngShow.

Object Notation

If you're using i18n-node's object notation functionality, additional configuration is required if you're using a delimiter other than ..

Backend

Provide the delimiter in your i18nRoutes.configure call. For example:

i18nRoutes.configure( app, { directory : path.join( applicationRoot, "locales" ), objectNotation : "→" } );

Frontend

Provide the delimiter in your angular.module call, by using the i18nProvider. For example:

var yourApp = angular.module( "yourApp", [ "i18n" ] )
  .config( [ "i18nProvider", function( i18nProvider ) {
             i18nProvider.setObjectNotation( "→" );
           } ] );

How it works

To make this approach work, we have to make several changes to the application at hand. The final setup is as follows:

  1. Communicate the locale, which was detected by i18n-node on the backend, to the frontend through the DOM.
  2. Establishing express routes that allow for retrieval of translations.
  3. Accessing those routes from Angular.

Make the users locale known in the DOM

First of all, we want to place the locale that was determined by i18n-node into our DOM for easy retrieval later on. Of course, you could detect the locale on the client side, but this approach makes sure that the same locale is used on both the backend and the frontend. To do that, we first create a function that we can call when compiling our template.

app.use( function( req, res, next ) {
  res.locals.acceptedLanguage = function() {
    return i18n.getLocale.apply( req, arguments );
  };
  next();
} );

Now we can use that method in our template. In a jade template it would be as simple as:

body(data-language=acceptedLanguage())

Add the required express routes

Note: You can automatically create these routes through i18n-node-routes.js, which is available through npm (npm install i18n-node-angular).

We now define two new routes in our express application. The first route will provide our full i18n-node translation document and the second will translate a single phrase.

module.exports = function( app ) {
    app.get( "/i18n/:locale",          routes.i18n       );
    app.get( "/i18n/:locale/:phrase",  routes.translate  );
}

Let's look at the implementation of those routes.

routes.i18n

The content of our translation document can then later be used freely on the frontend. For convenience, we'll also implement a service which we'll check out further below.

/**
 * Sends a translation file to the client.
 * @param request
 * @param response
 */
exports.i18n = function( request, response ) {
  var locale = request.params.locale;
  response.sendfile( "locales/" + locale + ".json" );
};

routes.translate

The translate route could theoretically be used to dynamically load single translated phrases from the backend. We primarily use it to have previously unknown translation phrases added to our JSON files by i18n-node.

/**
 * Translate a given string and provide the result.
 * @param request
 * @param response
 */
exports.translate = function( request, response ) {
  var locale = request.params.locale;
  var phrase = request.params.phrase;
  var result = i18n.__( {phrase: phrase, locale: locale} );
  response.send( result );
};

Create an AngularJS service to access the translation

Note: You can automatically create the service (and more) through i18n-node-angular.js, which is available through bower (bower install i18n-node-angular).

We now create an AngularJS service, named i18n with a method to access translations, named __(), thus making the usage equivalent to that on the backend.

See i18n-node-angular.js for a complete example.

Use the service

We can now access translations easily, by injecting our i18n service.

function MyController( i18n ) {
    console.log( i18n.__( "My translation phrase" ) );
}

servicesModule.factory( "MyService", function( i18n ) {
    console.log( i18n.__( "My translation phrase" ) );
}

If a term wasn't translated yet, the service will invoke the /i18n/locale/phrase route and cause i18n-node to add it to the translation JSON file.

Templates

i18n-node-angular.js also defines a filter named i18n. This filter can be used to translate strings in templates. While a lot of strings that appear in page templates are probably already translated on the backend, strings appearing in templates in directives aren't usually passed through the same channel; this is where we use the filter.

angular.module( "foo", [])
  .directive( "bar", function() {
    return {
      template: "{{'My translation phrase'|i18n}}"
    };
  });

Ensuring the locale was loaded

It is possible that you might invoke i18n.__ before the translation map was loaded from the server. This will cause an error message to be written to your console, telling you that you need to call ensureLocaleIsLoaded.

Locally

ensureLocaleIsLoaded returns a promise. So you can simply wrap the call around your use of i18n.__ or include the call earlier in your load hierarchy. Whatever suits your needs best.

An example would be:

function MyController( i18n ) {
    i18n.ensureLocaleIsLoaded().then( function() { 
        console.log( i18n.__( "My translation phrase" ) ); 
    } );
}

servicesModule.factory( "MyService", function( i18n ) {
    i18n.ensureLocaleIsLoaded().then( function() { 
        console.log( i18n.__( "My translation phrase" ) ); 
    } );
}
Globally

Alternatively, you can also make the controller creation wait for the locale to be loaded. An example of such a configuration with the i18n service would be:

$routeProvider.
  when( "/", {
        templateUrl: "/partials/index",
        controller : IndexController,
        resolve    : { i18n : [ "i18n", function( i18n ) { return i18n.i18n(); } ] }
      } );