15
votes

This question is about templating and localizing, using require.js and underscore templates through backbone.js. The application will need to be localised on the fly.

Before embarking down a path that later proved problematic, is there a better solution than the one I'm considering - I'm concerned about speed and memory with repeatedly merging and processing the language array. Assume that are a 2-3 thousand language strings.

Current approach (which works, but looks processor heavy):

  1. Using the I18N bundling approach, create language "includes" that will essentially contain the translated elements for all the templates
  2. Merge this object/array of elements with model attributes (from backbone) and pass the merged lot into the underscore template

.

define(['backbone', 'models/model', 'text!template.html', 'i18n!my/nls/translatedbits'],
  function(Backbone, MyModel, TemplateText, TranslationObject) {
  var View = Backbone.View.extend({
    model: {},

    initialize : function(params) {
      this.model = new MyModel();
    },

    render : function(callBack) {
      // Get the model attributes
      var templateParams = _.clone(this.model.attributes);
      // Bolt on the tranlsated elements (established from require.js I18N plugin)
      templateParams.t = TranslationObject;
      // Pass the lot ot the template
      var template = _.template(TemplateText, this.model.attributes);
      $(this.el).html( template );
      return this;
    }

  });
  return View;
  }
);

Then the template will read

<%= modelAttribute1 %> <%= t.translationString1 %>

Is there a better solution or a better templating engine? [Better for this purpose - mustache may have other advantages, but can it localize more easily, or can it cache localised results allowing model attributes to be passed in later?]

Note that languages may need to be changed "on the fly" - and that's another concern I have with I18N plugin. I may end up getting the transations by JSON request through a template model but this still requires a merge of objects, which is what I'm trying to avoid.

2
How about pre-compiling templates into JavaScript files based on language strings (You've a JavaScript file, containing the template strings, per language - One for English, another one for French.)? When user updates his/her language preference, load a different JavaScript template file to handle all the "static" translation, but leave the model attributes behind, since those are dynamic.DashK
@DashK - thanks. There are two methods I've considered along those lines. One gets a bit fiddly with require, but I considered creating a template model and then loading each one in as required (through require), pre-parsing and caching etc and then recalling the template from the model into the view(s). Another option is to pre-compile server-side (php or node.js) and recall the template through JSON, but still need that caching model in the middle. But I'm thinking there must be a better, more standard way that? Unless these are the only options? Thanks for the suggestion.Robbie

2 Answers

13
votes

Here's what I am currently doing (just open-sourced since it seems useful to others)

underi18n is a very minimal lib for doing i18n on templates and code.

It provides:

  • Simple conversion of gettext catalogs to json format.
  • Support for variable substitution in translation strings.

It does not deal with pluralization.

From the README:

Catalogs

under18n uses a simple JSON format for catalogs, following the standard gettext format. In the following example,

{
    'Developer': 'Προγραμματιστής',
    'Role ${role} does not exist in ${context}': 'Ο ρόλος ${role} δεν υπάρχει στο ${context}'
}

we have two translation strings, the second one with two variables, role and context. A simple python script is provided to help you convert standard .mo files to this JSON format.

Usage

Create a MessageFactory from a json i18n catalog:

var t = underi18n.MessageFactory(catalog);

You can now translate inline:

t('Developer') // returns "Προγραμματιστής"

t('Role ${role} does not exist in ${context}', {role: 'διαχειριστής', context: 'πρόγραμμα'})
// Returns "Ο ρόλος διαχειριστής δεν υπάρχει στο πρόγραμμα"

Templates

Typically variables in templates are indicated with some delimiter. In mustache for instance {{ var }} is used whereas <%= var %> is default for underscore. We use the same approach to indicate translatable strings. You can specify the delimiters for translatable strings as a RegExp, as well as the left/right delimiters used by your template language of choice in under18n.templateSettings. By default this is following underscore conventions:

templateSettings: {
    translate: /<%_([\s\S]+?)%>/g,
    i18nVarLeftDel: '<%=',
    i18nVarRightDel: '%>'
}

so, <%_ i18n %> are set to denote translatable strings and <%= var %> is used to denote variables inside a template.

You can translate a template by calling under18n.template, for example using underscore, you can do

var templ = _.template(under18n.template(myTemplate, t));

Example

Given the following catalogs, factories and template for english and greek and assuming an underscore template,

var test_en = {
        'files_label': 'Files',
        'num_files': 'There are ${num} files in this folder'
    },

    templ = '<h1><%= title %></h1>' +
            '<label><%_ files_label %></label>' +
            '<span><%_ num_files %></span>',

    t_en = underi18n.MessageFactory(test_en);
    t_el = underi18n.MessageFactory(test_el);

the template can by constructed by,

var toRender = _.template(underi18n.template(templ, t_en));
toRender({title: 'Summary', num: 3});

would yield

<h1>Summary</h1>
<label>Files</label>
<span>There are 3 files in this folder</span>

AMD loading

under18n will register as an anonymous module if you use requireJS.

I hope this solves your problem, let me know if not, I was planning to release it at some stage, but hey better now than never ;)

9
votes

For completeness, the solution that we came up that felt to most optimised was:

  1. When a template was requested from the server, a cookie determined the language and the correct template was delivered.

  2. Used PHP back end to pre-parse the templates; these were then stored in memcached in the correct language

  3. The language template, once requested, was then cached by the browser and internally in a backbone model so it could be rapidly re-used by JavaScript.

Reasons:

  • faster JS (far fewer regular expression replaces). We never benchmarked, but it is only logical when you remove the functions completely.
  • saved transferring a HUGE language file to the client