Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Server-side internationalization for Backbone and Handlebars

I'm working on a Grails / Backbone / Handlebars application that's a front end to a much larger legacy Java system in which (for historical & customizability reasons) internationalization messages are deep in a database hidden behind a couple of SOAP services which are in turn hidden behind various internal Java libraries. Getting at these messages from the Grails layer is easy and works fine.

What I'm wondering, though, is how to get (for instance) internationalized labels into my Handlebars templates.

Right now, I'm using GSP fragments to generate the templates, including a custom tag that gets the message I'm interested in, something like:

<li><myTags:message msgKey="title"/> {{title}}</li>

However, for performance and code layout reasons I want to get away from GSP templates and get them into straight HTML. I've looked a little into client-side internationalization options such as i18n.js, but they seem to depend on the existence of a messages file I haven't got. (I could generate it, possibly, but it would be ginormous and expensive.)

So far the best thing I can think of is to wedge the labels into the Backbone model as well, so I'd end up with something like

<li>{{titleLabel}} {{title}}</li>

However, this really gets away from the ideal of building the Backbone models on top of a nice clean RESTful JSON API -- either the JSON returned by the RESTful service is cluttered up with presentation data (i.e., localized labels), or I have to do additional work to inject the labels into the Backbone model -- and cluttering up the Backbone model with presentation data seems wrong as well.

I think what I'd like to do, in terms of clean data and clean APIs, is write another RESTful service that takes a list of message keys and similar, and returns a JSON data structure containing all the localized messages. However, questions remain:

  1. What's the best way to indicate (probably in the template) what message keys are needed for a given view?
  2. What's the right format for the data?
  3. How do I get the localized messages into the Backbone views?
  4. Are there any existing Javascript libraries that will help, or should I just start making stuff up?
  5. Is there a better / more standard alternative approach?
like image 307
David Moles Avatar asked Jan 08 '13 00:01

David Moles


2 Answers

I think you could create quite an elegant solution by combining Handelbars helpers and some regular expressions.

Here's what I would propose:

  1. Create a service which takes in a JSON array of message keys and returns an JSON object, where keys are the message keys and values are the localized texts.
  2. Define a Handlebars helper which takes in a message key (which matches the message keys on the server) and outputs an translated text. Something like {{localize "messageKey"}}. Use this helper for all template localization.
  3. Write a template preprocessor which greps the message keys from a template and makes a request for your service. The preprocessor caches all message keys it gets, and only requests the ones it doesn't already have.
    • You can either call this preprocessor on-demand when you need to render your templates, or call it up-front and cache the message keys, so they're ready when you need them.
    • To optimize further, you can persist the cache to browser local storage.

Here's a little proof of concept. It doesn't yet have local storage persistence or support for fetching the texts of multiple templates at once for caching purposes, but it was easy enough to hack together that I think with some further work it could work nicely.

The client API could look something like this:

var localizer = new HandlebarsLocalizer();

//compile a template
var html = $("#tmpl").html();
localizer.compile(html).done(function(template) { 
  //..template is now localized and ready to use 
});

Here's the source for the lazy reader:

var HandlebarsLocalizer = function() {

  var _templateCache = {};
  var _localizationCache = {};

  //fetches texts, adds them to cache, resolves deferred with template
  var _fetch = function(keys, template, deferred) {
    $.ajax({
      type:'POST',
      dataType:'json',
      url: '/echo/json',
      data: JSON.stringify({
        keys: keys 
      }),
      success: function(response) {
        //handle response here, this is just dummy
        _.each(keys, function(key) { _localizationCache[key] = "(" + key + ") localized by server";  });
        console.log(_localizationCache);
        deferred.resolve(template);
      },
      error: function() {
        deferred.reject();
      }
    });
  };

  //precompiles html into a Handlebars template function and fetches all required
  //localization keys. Returns a promise of template.
  this.compile = function(html) {

    var cacheObject = _templateCache[html],
        deferred = new $.Deferred();

    //cached -> return
    if(cacheObject && cacheObject.ready) { 
      deferred.resolve(cacheObject.template);
      return deferred.promise();
    }
    //grep all localization keys from template
    var regex = /{{\s*?localize\s*['"](.*)['"]\s*?}}/g, required = [], match;
    while((match = regex.exec(html))) {
      var key = match[1];
      //if we don't have this key yet, we need to fetch it
      if(!_localizationCache[key]) {
        required.push(key);
      }
    }

    //not cached -> create
    if(!cacheObject) {
      cacheObject = {
        template:Handlebars.compile(html),
        ready: (required.length === 0)
      };   
      _templateCache[html] = cacheObject;
    }

    //we have all the localization texts ->
    if(cacheObject.ready) {
      deferred.resolve(cacheObject.template);
    } 
    //we need some more texts ->
    else {
      deferred.done(function() { cacheObject.ready = true; });
      _fetch(required, cacheObject.template, deferred);    
    }

    return deferred.promise();
  };

  //translates given key
  this.localize = function(key) {
    return _localizationCache[key] || "TRANSLATION MISSING:"+key;
  };

  //make localize function available to templates
  Handlebars.registerHelper('localize', this.localize);
}
like image 151
jevakallio Avatar answered Oct 12 '22 13:10

jevakallio


We use http://i18next.com for internationalization in a Backbone/Handlebars app. (And Require.js which also loads and compiles the templates via plugin.)

i18next can be configured to load resources dynamically. It supports JSON in a gettext format (supporting plural and context variants). Example from their page on how to load remote resources:

var option = { 
  resGetPath: 'resources.json?lng=__lng__&ns=__ns__',
  dynamicLoad: true 
};

i18n.init(option);

(You will of course need more configuration like setting the language, the fallback language etc.)

You can then configure a Handlebars helper that calls i18next on the provided variable (simplest version, no plural, no context):

// namespace: "translation" (default)
Handlebars.registerHelper('_', function (i18n_key) {
    i18n_key = Handlebars.compile(i18n_key)(this);
    var result = i18n.t(i18n_key);
    if (!result) {
        console.log("ERROR : Handlebars-Helpers : no translation result for " + i18n_key);
    }
    return new Handlebars.SafeString(result);
});

And in your template you can either provide a dynamic variable that expands to the key:

<li>{{_ titleLabeli18nKey}} {{title}}</li>

or specify the key directly:

<li>{{_ "page.fancy.title"}} {{title}}</li>

For localization of datetime we use http://momentjs.com (conversion to local time, formatting, translation etc.).

like image 2
Risadinha Avatar answered Oct 12 '22 13:10

Risadinha