Friday, February 21, 2014

Switching I18next Locales in Polymer


At this point, I am using a few of the many i18next features in my example <hello-you> Polymer. One that I have yet to put to use is actually switching locales, so that is on tonight's docket.

I fancy the dynamic nature of Polymer a bit too much. Locales are not normally switched on the fly—rather they are determined on the server or at load time—but darn it, it's just too much fun to change one thing and have all of the labels on the screen change.

So I introduce a locale attribute to <hello-you> and bind it in a text field for quick access from the page:
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="hello-you" attributes="locale">
  <template>
    <!-- ... -->
    <p>
      <input value="{{locale}}">
    </p>
    <!-- ... -->
    <script type="text/javascript" src="../bower_components/i18next/i18next.js"></script>
  </template>
  <script src="hello_you.js"></script>
</polymer-element>
This gives me a locale <input> at the bottom of the Polymer:



I would like to observe the locale bound variable such that it drives the labels to change when the locale changes. After last night (and a bit of refactoring), I have an observer on the count attribute such that, when the user updates the number of balloons, all of the labels are redrawn, including the number of balloons possessed by the Polymer:

Polymer('hello-you', {
  count: 0,
  // ...
  observe: {
    'count': '_updateLabels'
  },
  // ...
  _updateLabels: function() {
    // Simple labels
    this.hello = i18n.t('app.hello');
    this.colorize = i18n.t('app.colorize');
    this.how_many = i18n.t('app.how_many');
    this.instructions = i18n.t('app.instructions');
    // Pluralization message
    this.count_message = i18n.t(
      "balloon",
      { count: this.count ? parseInt(this.count, 10) : 0 }
    );
  },
  // ...
});
The i18next library supports changing the current locale with the setLng() method. So, in addition to observing the count attribute, I observe locale. When locale changes, I invoke setLng() with the new locale. I also supply setLng() with a second argument—a callback to be invoked when the locale is ready:
Polymer('hello-you', {
  count: 0,
  locale: 'en',
  // ...
  observe: {
    'count': '_updateLabels',
    'locale': '_changeLocale'
  },
  // ...
  _changeLocale: function() {
    i18n.setLng(this.locale, this._updateLabels.bind(this));
  },

  _updateLabels: function() {
    // Simple labels
    this.hello = i18n.t('app.hello');
    this.colorize = i18n.t('app.colorize');
    this.how_many = i18n.t('app.how_many');
    this.instructions = i18n.t('app.instructions');
    // Pluralization message
    this.count_message = i18n.t(
      "balloon",
      { count: this.count ? parseInt(this.count, 10) : 0 }
    );
  },
  // ...
});
That callback is just my _updateLabels() method. In other words, when the i18next locale is ready, I update the labels in my Polymer (a bind() is required to keep the current Polymer's context in scope).

And that works! After defining a locales/fr/translation.json:
{
  "app": {
    "hello": "Bonjour",
    "colorize": "Coloriser",
    "instructions": "Présentez-vous une expérience personnalisée incroyable!",
    "how_many": "Combien de ballons?"
  },
  "balloon": "J'ai un ballon rouge.",
  "balloon_plural": "J'ai __count__ ballons rouges."
}
I can dynamically swap the locale by typing my desired locale:



I even fooled Chrome into thinking this was a French page. Nice!

This level of dynamic locale updating will likely never be needed, but it is nice to know that it is possible. I remain somewhat skeptical that i18next is warranted in Polymers. The pluralization is nice, but it seems like something that Polymer could handle without a separate library. Something to think about for tomorrow.


Day #1,033

No comments:

Post a Comment