Saturday, February 22, 2014

Strategy to Internationalize Polymers


I like the i18next JavaScript library for internationalization, but I'm not I like it as the internationalization solution for Polymer. So I begin to wonder if I need to find the solution.

Let's face it, even if I find a great JavaScript solution for i18n and Polymer, I am still out in the cold with Dart. The whole point of Patterns in Polymer is to identify approaches that work in both—to identify solutions that transcend the two languages. So, instead of finding the solution, my current thinking is to identify a solution that covers 80% of the cases, but also makes it easy to swap in a custom solution if Polymer programmers prefer another approach. With that in mind...

Today, I am going to move all of the translation work into a separate object—specifically a separate Polymer. Currently, my <hello-you> Polymer is responsible for updating labels and doing “hello you” kind of work like collection name, number of balloons currently possessed and the preferred language:



The actual locale setting still needs to reside in <hello-you>, but I ought to be able to push everything else down into a custom <polymer-translate> element. Two things need to change in the Polymer definition of <hello-you>—the <script> tag pulling in the i18next library now belongs in the my proposed <polymer-translate> and I need to import the definition of said <polymer-translate>. So this:
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="hello-you" attributes="locale">
  <template>
    <!-- ... -->
    <script type="text/javascript" src="../bower_components/i18next/i18next.js"></script>
  </template>
  <script src="hello_you.js"></script>
</polymer-element>
Becomes:
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="polymer-translate.html">
<polymer-element name="hello-you" attributes="locale">
  <template>
    <!-- ... -->
  </template>
  <script src="hello_you.js"></script>
</polymer-element>
I also need a way to communicate the current locale from the parent <hello-you> down into the <polymer-translate> child. The easiest way of accomplishing this is to add <polymer-translate> as a child of <hello-you>'s template and bind the locale variable of <hello-you> to the locale attribute of polymer-translate:
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="polymer-translate.html">
<polymer-element name="hello-you" attributes="locale">
  <template>
    <!-- ... -->
    <polymer-translate locale={{locale}}></polymer-translate>
  </template>
  <script src="hello_you.js"></script>
</polymer-element>
With that, any time the user changes the value of locale in <hello-you>, polymer-translate will know about it immediately.

The elided stuff inside of the <hello-you>'s <template> is typical Polymer bound variables and methods:
    <!-- ... -->
    <div id="app">
      <!-- [l10n] hello is a label -->
      <h2><span>{{hello}}</span> {{your_name}}</h2>
    </div>
    <p>
      <input value="{{your_name}}">
      <!-- [l10n] colorize is a label -->
      <input type=submit value="{{colorize}}!" on-click="{{feelingLucky}}">
    </p>
    <!-- ... -->
I note here there this approach does not delineate between locale bound variables and application bound variables—that is they look the same at a glance. The hello and colorize variables are really localized label variables (e.g. Hello, Colorize). The your_name variable is an application variable (the user updates it and the Polymer presents it) and the feelingLucky variable is bound to a method. But the only indication that they serve different purposes are clues from the context in which they are used.

If I could have gotten i18next's jQuery integration working inside of the shadow DOM, I might have used i18next's data attributes. In the absence of that I only note that an improvement is likely needed. Anyhow...

What I need next is for my <polymer-translate> custom element to read the labels from the same i18next source and update its parent element (<hello-you>). To do that, I initialize the i18next library when <polymer-translate> is ready:
Polymer('polymer-translate', {
  ready: function() {
    i18n.init(
      {preload: ['en', 'fr']},
      this.t.bind(this)
    );
  },
  // ...
});
That's just normal i18next initialization—I preload the English and French translations and, once those are loaded, call the t() method to translate the labels in the associated element.

For now, I require the parent Polymer to pass a reference to itself down to this <polymer-translate>:
Polymer('hello-you', {
  // ...
  ready: function() {
    this.i18n = this.shadowRoot.querySelector('polymer-translate');
    this.i18n.el = this;
  },
  // ...
});
Whether <polymer-translate> should directly update properties in the parent is debatable. I use this approach so that <hello-you> need not be responsible for watching for attribute changes. The translate method is then:
Polymer('polymer-translate', {
  observe: { 'locale': '_changeLocale' },
  ready: function() { /* ... */ },
  t: function() {
    if (this.el === null) return;

    var el = this.el;
    el.hello = i18n.t('app.hello');
    el.colorize = i18n.t('app.colorize');
    // ...
    el.count_message = i18n.t(
      "balloon",
      { count: el.count ? parseInt(el.count, 10) : 0 }
    );
  },
  _changeLocale: function() {
    i18n.setLng(this.locale, this.t.bind(this));
  }
});
The main advantage here being that, when a change to the locale is observed, the _changeLocale() method can invoke translate, which immediately updates the labels in <hello-you>.

So, in the end, all of the i18n work is out of my Polymer. Better still, my <polymer-translate> need only support setting the element to be translated and the t() method:
Polymer('hello-you', {
  // ...
  observe: {
    'count': '_updateLabels'
  },
  ready: function() {
    this.i18n = this.shadowRoot.querySelector('polymer-translate');
    this.i18n.el = this;
  },
  _updateLabels: function() {
    this.i18n.t();
  },
  // ...
});
No matter how I refactor or extend <polymer-element>—no matter what strategy I employ to perform translations—my Polymer can remain the same. And yup, it still works switching locales via the dropdown menu:



I still have a few outstanding questions to answer on this approach, but so far this shows promise. Over the next two days I will likely try this approach in Dart and try to swap out the i18n scheme used here in JavaScript. Hopefully both of those efforts will expose any flaws that might exist.


Day #1,034

No comments:

Post a Comment