How to make i18n with Handlebars.js (mustache templates)?

主宰稳场 提交于 2019-12-02 14:10:01

I know this has been answered, but I'd like to share my simple solution. To build on Gazler's solution using I18n.js (which we use with our project at work), I just used a very simple Handlebars helper to facilitate the process to do the localization on the fly:

Handler

Handlebars.registerHelper('I18n',
  function(str){
    return (I18n != undefined ? I18n.t(str) : str);
  }
);

Template

<script id="my_template" type="x-handlebars-template">
    <div>{{I18n myVar}}</div>
</script>

The primary advantage of this is that there's no expensive pre/post processing on the entire json object. Not to mention if the incoming json has nested objects/arrays, the time spent looking for and parsing for them might get expensive if the object is huge.

Cheers!

Gazler

https://github.com/fnando/i18n-js is a ruby gem that will create an internationalization file from your config/locales folder. However if you are not using rails, you can find the javascript used on its own here.

You then simply store the translations in a nested object..

I18n.translations = {"en":{"date":{"formats":{"default":"%Y-%m-%d","short":"%b %d","long":"%B %d, %Y"}}}};

Something that may also be of use to you that I use on my projects is a patch to mustache that automatically translates strings in the format @@translation_key@@

i18nize = function (result) {
    if (I18n) {
      var toBeTranslated = result.match(/@@([^@]*)@@/gm);
      if (!toBeTranslated) return result;
      for(var i = 0; i < toBeTranslated.length; i++) {
        result = result.replace(toBeTranslated[i], I18n.t(toBeTranslated[i].replace(/@/g, "")));
      }
    }
    return result;
};

You then call i18nize after render to allow you to put translations in your templates instead of passing them through.

Beware of patching mustache as you will not be able to port your templates to standard mustache implementations. However in my case, the benefits offered outweighed this issue.

Hope this helps.

Based on @poweratom 's answer :

Only with ember.js , same with options passed to I18n.js.

It will magically reload if computed properties are used.

Ember.Handlebars.helper "t", (str, options) ->
  if I18n? then I18n.t(str, options.hash) else str

Template:

{{t 'sharings.index.title' count=length}}

Yml:

en:
  sharings:
    index:
      title: To listen (%{count})

The question is answered but their may be a case where you do not want to depend on any i8n lib and use completely your own. I am using my own inspired from https://gist.github.com/tracend/3261055

dam1

With NodeJs / Express :

  • node-i18n ( detect the Accept-Language header )

      app.use(i18n.init); 
    
  • Sample translation file

    {   
     "hello": "hello",   
     "home-page": {
       "home": "Home",
        "signup": "Sign Up"  
     } 
    }
    
  • In Express controller

    ...
    data.tr = req.__('home-page');
    var template = Handlebars.compile(source);
    var result = template(data);
    
  • Handlebars Template

        <li class="active"><a href="/">{{tr.home}}</a></li>
    

for those not using any JS framework http://i18next.com looks promising too

just create handlebars helper to call translations like here http://i18next.com/pages/doc_templates.html

As already established, Handlebars does not have an established method for internationalization, even in 2019.

The existing answers don't illustrate the more complicated/important aspects of client-side internationalization such as grammar rules, or they create a dependency on YML and/or Ember, or require server-side rendering (nodejs).

Building on @poweratom's answer (which in turn builds on @Glazer's), one can register a helper which allows a pass-through of the Handlebars parameters (which of course in JS properly default to undefined).

Handlebars.registerHelper('i18n',
  function(str,o1,o2,o3){
    return new Handlebars.SafeString((typeof(i18n) !== "undefined" ? i18n(str,o1,o2,o3) : str));
  }
);

Using this with i18njs.com (npm/roddeh-i18n), we can add translations using client-side only JSON/JavaScript:

i18n.translator.add({
  "values":{
    "Yes": "はい",
    "No": "いいえ",
    "It is %n": [[0,null,"%nです"]],
    "Do you want to continue?": "続けたいですか?",
    "Don't worry %{name}": "%{name}を心配しないでください",
    "%{name} uploaded %n photos to their %{album} album": "%{name}は彼の%{album}アルバムに写真%n枚をアップロードしました"
  },
  "contexts":[
    {
      "matches": { "gender": "male" },
      "values": { "%{name} uploaded %n photos to their %{album} album": [[0,null,"%{name}は彼の%{album}アルバムに写真%n枚をアップロードしました"]] }
    },
    {
      "matches": { "gender": "female" },
      "values": { "%{name} uploaded %n photos to their %{album} album": [[0,null,"%{name}は彼女の%{album}アルバムに写真%n枚をアップロードしました"]] }
    }
  ]
});

We can create a handlebars template where we pass-through parameters to the library. For example, formatting a number (i.e. "%n") requires the first parameter to be the path to the number. So to get the count from an object {"count":3} we could reference the path "./count" or just "count". Conditional matches require the last parameter to be the path to the object where the matches will be found; usually just the root object ".".

<script id="messagestemplate" type="text/x-handlebars-template">
  <p>
    {{i18n 'Do you want to continue?'}} {{i18n 'Yes'}}<br>
    {{i18n 'Don\'t worry %{name}' . }}<br>
    {{i18n 'It is %n' count}}<br>
    {{i18n '%{name} uploaded %n photos to their %{album} album' count . .}}
  </p>
</script>

And finally the template can be rendered as normal with Handlebars:

var userData = {
  gender: "male",
  name: "Scott",
  album: "Precious Memories",
  count: 1
};

var templateSource = $("#messagestemplate").html();
var messagesTemplate = Handlebars.compile(templateSource);
var renderedMessages = messagesTemplate(userData);

$('#target-message').html(renderedMessages);

Here's a more complete example:

// Using http://i18njs.com (npm/roddeh-i18n)

// Includes:
//   cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js
//   rawgit.com/components/handlebars.js/master/handlebars.js
//   cdn.jsdelivr.net/npm/roddeh-i18n@1.2.0/dist/i18n.min.js


// REGISTER I18N HELPER   {{i18n 'Text to translate'}}

Handlebars.registerHelper('i18n',
  function(str,o1,o2,o3,o4,o5){
    return new Handlebars.SafeString((typeof(i18n) !== "undefined" ? i18n(str,o1,o2,o3,o4,o5) : str));
  }
);


// REGISTER THE TEMPLATE

var templateSource = $("#atemplate").html();
var template = Handlebars.compile(templateSource);

function updateMessage(data) {
  $('#target-message').html(template(data));
}


// ADD TRANSLATIONS

function setLanguage(lang) {
  // Spanish
  if (lang == 'es') {
    i18n.translator.reset();
    i18n.translator.add({
      "values":{
        "Yes": "Si",
        "No": "No",
        "Do you want to continue?": "¿Quieres continuar?",
        "Don't worry %{name}": "No te preocupes %{name}",
        "It is %n": [[0,null,"Es %n"]],
        "%{name} uploaded %n photos to their %{album} album":[
            [0, 0, "%{name} ha subido %n fotos a su album %{album}"],
            [1, 1, "%{name} ha subido %n foto a su album %{album}"],
            [2, null, "%{name} ha subido %n fotos a su album %{album}"]
         ]
      }
    });
  }

  // Japanese
  else if (lang == 'jp') {
    i18n.translator.reset();
    i18n.translator.add({
      "values":{
        "Yes": "はい",
        "No": "いいえ",
        "It is %n": [[0,null,"%nです"]],
        "Do you want to continue?": "続けたいですか?",
        "Don't worry %{name}": "%{name}を心配しないでください",
        "%{name} uploaded %n photos to their %{album} album": "%{name}は彼の%{album}アルバムに写真%n枚をアップロードしました"
      },
      "contexts":[
        {
          "matches":{ "gender":"male" },
          "values":{ "%{name} uploaded %n photos to their %{album} album": [[0,null,"%{name}は彼の%{album}アルバムに写真%n枚をアップロードしました"]] }
        },
        {
          "matches":{ "gender":"female" },
          "values":{ "%{name} uploaded %n photos to their %{album} album": [[0,null,"%{name}は彼女の%{album}アルバムに写真%n枚をアップロードしました"]] }
        }
      ]
    });
  }

  // Default Language (English)
  else {
    i18n.translator.reset();
    i18n.translator.add({
      "values":{
        "Yes": "Yes",
        "No": "No",
        "Do you want to continue?": "Do you want to continue?",
        "Don't worry %{name}": "Not to worry %{name}",
        "It is %n": [[0,null,"It's %n"]],
        "%{name} uploaded %n photos to their %{album} album":[
            [0, 0, "%{name} uploaded %n photos to their %{album} album"],
            [1, 1, "%{name} uploaded %n photo to their %{album} album"],
            [2, null, "%{name} uploaded %n photos to their %{album} album"]
         ]
      }
    });
  }
}


// SET DEFAULT LANGUAGE TO BROWSER/SYSTEM SETTINGS

var browserLanguage = (navigator.languages && navigator.languages[0] || navigator.language || navigator.userLanguage || navigator.browserLanguage || navigator.systemLanguage || 'en').split('-')[0];

setLanguage(browserLanguage);


// RENDER THE TEMPLATE WITH DATA

var userData = {
  gender: "female",
  name: "Scott",
  album: "Precious Memories",
  count: 1
};

updateMessage(userData);


// USER-TRIGGERED LANGUAGE SELECTION

// note: the array around browserLanguage is important when setting radio buttons!
$("input[name=lang]")
  .val([browserLanguage])
  .click(
    function() {
      var lang = $('input[name=lang]:checked').val();
      setLanguage(lang);
      updateMessage(userData);
    }
  );
<script src="https://cdn.jsdelivr.net/npm/roddeh-i18n@1.2.0/dist/i18n.min.js"></script>
<script src="https://rawgit.com/components/handlebars.js/master/handlebars.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>

<h1>i18n with Handlebars</h1>

<label><input type="radio" name="lang" value="en"> English</label><br>
<label><input type="radio" name="lang" value="es"> Espanol</label><br>
<label><input type="radio" name="lang" value="jp"> Japanese</label>

<div id="target-message"></div>

<!--
  NOTE: The helper {{i18n ...}} is just a passthrough for
    the i18n library. Parameters come from the single object
    passed into the handlebars template. Formatting a
    number (i.e. "%n") requires the first parameter to be
    the path to the number.  For example, count from the
    object {"count":3} could be referenced by the path
    "./count" or just "count".  Conditional matches require
    the last parameter to be the path to the object where
    the matches will be found; usually just the root object ".".

    see:
      handlebarsjs paths:   https://handlebarsjs.com/#paths
      i18n formatting:      http://i18njs.com/#formatting
-->

<script id="atemplate" type="text/x-handlebars-template">
  <p>
    {{i18n 'Do you want to continue?'}} {{i18n 'Yes'}}<br>
    {{i18n 'Don\'t worry %{name}' . }}<br>
    {{i18n 'It is %n' count}}<br>
    {{i18n '%{name} uploaded %n photos to their %{album} album' count . .}}
  </p>
</script>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!