I\'m currently using Handlebars.js (associated with Backbone and jQuery) to make a web app almost totally client side rendered, and I\'m having issues with the internationalisat
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})
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.
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
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!
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>
As already established, using Handlebars for internationalization means you will have to register a custom helper to link to the i18n library of your choice. Most i18n libraries don't have this "glue" available out of the box, but it's pretty easy to add.
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.
Handlebars.registerHelper('i18n',
function(str){
return new Handlebars.SafeString((typeof(i18n) !== "undefined" ? i18n.apply(null, arguments) : str));
}
);
The existing answers use other libraries, but I prefer i18njs.com (npm/roddeh-i18n) because it has better support for the more important aspects of client-side internationalization such as grammar rules (and it doesn't create a dependency on YML and/or Ember, and doesn't require server-side rendering with nodejs).
With the helper registered above, we can add translations using just client-side 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枚をアップロードしました"]] }
}
]
});
Now, any handlebars template we create can be internationalized by just passing-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){
return new Handlebars.SafeString((typeof(i18n) !== "undefined" ? i18n.apply(null, arguments) : 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>