Localizing templates using require.js, backbone and underscore

11,895

Solution 1

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

Solution 2

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 ;)

Share:
11,895
Robbie
Author by

Robbie

Technical Director, specialising in LAMP environment, for an Australian and US mobile gaming company (smgstudio.com). Aiming to make each game better and more inventive than the last.

Updated on June 15, 2022

Comments

  • Robbie
    Robbie almost 2 years

    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.

  • Robbie
    Robbie over 11 years
    +1 for the details - thanks (it probably deserves more!). I'm going to have to test it out and feed back if it appears cleaner. Thanks.
  • ggozad
    ggozad over 11 years
    Thank you! Feedback more than welcome, this has been in production for a while and behaving well. Note that you can if you want compile on the server too!
  • Robbie
    Robbie over 11 years
    this looks like a search and replace function, using strings (objects) that are "in hand". We can pull those strings/ object in via requires I18N bundles, but it still looks to have the same disadvantages as simply running it through underscore - no caching and a lots of large objects being repeatedly parsed? Or have I missed something (sorry if I have!)? Thanks.
  • ggozad
    ggozad over 11 years
    The way I avoid completely client-side calculation is to prepare the i18n-ized template on the server! So your client just loads the localized template ready. This way there is no parsing and no large objects, in fact you don't even need underi18n any more.
  • Robbie
    Robbie over 11 years
    That's sort of where my thoughts were leading me. Doesn't solve the problems / issues problems. Thanks for the response, though.
  • ggozad
    ggozad over 11 years
    I am not sure what the problems are. There are no expensive operations and no need for additional caching. As a bonus you gain variables in i18n. But maybe I have not understood the actual problem.