Internationalization Support in Ember.js


#1

One thing I’m really proud of is that when we launched Discourse, we had first class Internationalization (i18n) support ready to be used. Our first release only English, but thanks to our community we have 18 localizations of our software in progress! Here’s what Discourse looks like in Simplified Chinese:

Discourse in Chinese

On the server side, Discourse uses Rails' built in i18n support. It has been around for a long time and works easily so I won’t go into that. Check out the documentation for your server side framework of choice for more.

I18n in Ember.js

Our client side application is written in Ember.js, which doesn’t have built in support for i18n. However, it’s not difficult to add it in.

We use i18n-js, a project whose goal is to bring Rails translation support to Javascript. Don’t worry if you don’t use Rails on the server side. You can use all of the code in this post outside of Rails if you like. The Javascript code in 1i8n-js is all you’ll need.

Once you’ve included i18n-js in your project, you will have access to an I18n object in your javascript code to perform translations with. The first thing you’ll need to do is include a translations.js file that includes all your translations. Here’s how a simple one could look:

I18n.translations = {
  en: {
    hello: 'hello',
    cookieCount: {
      one: 'You have {{count}} cookie.',
      other: 'You have {{count}} cookies. Yum!'
    }
  },

  fr: {
    hello: 'bonjour'
    cookieCount: {
      one: 'Vous avez {{count}} biscuit.',
      other: 'Vous avez {{count}} biscuits. Le Yum!'
    }
  }
};

And then if you wanted to output a translation you can use the i18n.t function:

console.log(I18n.t('hello'));   // outputs hello because the default locale is `en`

I18n.locale = 'fr';
console.log(I18n.t('hello'));   // outputs bonjour

In an Ember app though, you’ll want to be able to access those translations in your handlebars templates. To do this, you’ll need to define a helper. You can just copy and paste this code into your app:

Ember.Handlebars.registerHelper('i18n', function(property, options) {
  var params = options.hash,
      self = this;

  // Support variable interpolation for our string
  Object.keys(params).forEach(function (key) {
    params[key] = Em.Handlebars.get(self, params[key], options);
  });

  return I18n.t(property, params);
});

Now your templates are ready to be translated:

<h1>{{i18n hello}}</h1>

<p>{{i18n cookieCount count=user.cookies.length}}</p>

Note that the I18n library is smart enough to notice when you supply a parameter named count to select the correct pluralization for a key. If the user has one cookie it won’t add that pesky “s.”

I18n support is so easy to add that I recommend it for just about every web project unless you’re absolutely sure you’ll never need it in another language. The Internet is a lot bigger than your home country, go forth and make it easier to translate!


This is a companion discussion topic for the original entry at http://eviltrout.com/2013/11/24/i18n-in-ember.html

#2

Why did you choose a key-value system over the gettext approach? I mean, this:

{{i18n "Original string in English"}}

would be way easier to write and read than

{{i18n some.arbitrary.message.code}}


#3

We wanted to use the same translation format that Rails used on the server side.

Additionally, I am not a fan of using the english as the key. It is easier to write, but if you ever edit the english (and we do frequently) you have to rename all the english keys in all other languages.


#4

A great article and simple implementation. Kudos!


#5

To be fair, if you edit the English, its previous translations are no longer valid and need to be redone (or at least reevaluated). What you do in the meantime is a question of tradeoffs... is it better to show the old (wrong) text? Or just the English? Or "Translation Missing"?

Using English keys forces you to pick the second or third option, which in turn encourage you to get your translation process super streamlined so that stuff never stays untranslated for very long :)


#6

If you change the english string, the other languages should be updated too, and gettext does that pretty good.

Anyway, I'm not here to begin a discussion about what system is the best, that's a personal choice ;-) We use Django as the server side engine, and it uses gettext for translations, so we'd like to use the same system in the client side. My question is if it's possible to do that with Ember. There are some libraries, like Jed, to achieve gettext integration in JS, but, as far as I know, there's no easy way to use them with Ember and/or Handlebars.


#7

The gettext approach also gets ridiculously long/unreadable with long texts (help bubbles, legal texts, etc)


#8

Right, I think a lot of this is just preference.

If there's a good JS library for gettext I imagine you can wrap it the same way I wrapped i18n-js :) If the text is too long to put in a helper, you could create a block helper like {{#i18n}}this is a longer text blah blah{{/i18n}}.


#9

Great article. I'm wondering if / how you implemented translations as attributes of HTML nodes and within handlebar declarations. A good example is a placeholder on a input tag or an {{Ember.TextField}}. AFAICT, both of these areas have nuances that add complexity beyond this article. Or did you manage to avoid or otherwise work around it?


#10

Thanks for the response. So Discourse's philosophy is to just subclass anything that needed a translatable attribute? Were there really that few occurrences? Do you recall any other instances where you needed to do this other than placeholders?

The ember-i18n project (https://github.com/jamesarosen... contains a broader implementation to handle translatable attributes and auto-translatable properties, but I'm impressed that a project the size of Discourse has managed to get away with a minimalist approach. Nice work.


#11

Sorry, the URL in my last question did not work properly: https://github.com/jamesarosen...


#12

Behind the scenes do you use the same zh-CN.yml file (compiled to JS) for both client and server, or are client and server translations kept apart?


#13

Fair enough.
I often feel inclined to DRY up my translations,
but then end up regretting it.


#14

Thank you, it helps

I18n.translations = {
  en: {
    hello: 'hello',
    cookieCount: {
      one: 'You have {{count}} cookie.',
      other: 'You have {{count}} cookies. Yum!'
    }
  },

  fr: {
    hello: 'bonjour' // miss a ',' here
    cookieCount: {
      one: 'Vous avez {{count}} biscuit.',
      other: 'Vous avez {{count}} biscuits. Le Yum!'
    }
  }
};

#15

Javascript code in 1i8n-js

link is broken. Here is a working one


In the translations.js after ‘bonjour’ you are missing a comma.


How do you tell ember which localization you want to use?


#16

Thanks, I’ve updated the link!

The I18n library has the ability to choose a locale like so: I18n.locale = 'en'; So when our Javascript downloads we set it to the appropriate locale for the web site.


#17

Nice!

I’m wondering, do you support live language switch? From what I’ve seen Ember i18n library doesn’t support automatic update of the rendered text, so it looks like you have to do a full reload of the app to make the change effective, how do you handle that in discourse?


#18

We do a full refresh of the app in that case. It’s worth it just to load a new bundle of translations (we definitely don’t serve all 22 languages to every user!)


#19

Hi Evil I want to apply internationalization on all page when I select particular language.So how can i implemented and also apply on all placeholder


#20

Hi, the example with the cookies count:

<p>{{i18n cookieCount count=user.cookies.length}}</p>

it’s not bound to the value of user.cookies.length. Is there a way to make the string update when the count changes?