Monday, March 3, 2014

Configurable Translation Strategies in Polymer


I think that I am nearly done with my exploration of internationalizing Polymers, but for one last thing. I would like to expose the ability for developers using my i18n-capable Polymer to add their own localization.

In the past, I have tried to do this with <link> configuration import:
    <link rel="import" href="packages/i18n_example/hello-you.html">
    <link rel="import" href="packages/i18n_example/hello-you-en.json">
    <link rel="import" href="packages/i18n_example/hello-you-fr.json">
    <link rel="import" href="packages/i18n_example/hello-you-es.json">
I may end up with a similar approach, but rather than starting with the solution, tonight, I start with the API I would like to support.

I continue working with the sample <hello-you> Polymer that is currently localized in three languages including French:



Given a developer has already written an Italian localization file as hello-you-it.json:
{
  "hello": "Ciao",
  "colorize": "Colorare",
  "how_many": "Quanto?",
  "instructions": "Presentarsi per un'esperienza personalizzata straordinaria!",
  "language": "Lingua",
  "balloon_zero": "Non ho palloncini rossi.",
  "balloon_one": "Ho un palloncino rosso.",
  "balloon_plural": "Ho __count__ palloncini rossi."
}
The <hello-you> Polymer needs a minimal way to support adding this (and possibly other) localizations. I start with:
  <body>
    <div class="container">
      <hello-you 
         locale="it"
         extraLocales="it:Italian,pl:Pig Latin"></hello-you>
    </div>
  </body>
The locale attribute, which defines the initial locale, already works in <hello-you> and its translation strategy. Now I need to support this extraLocales attribute in both as well.

Since loading locale data is the responsibility of the <polymer-translate> strategy employed by <hello-you>, it will need to load these extra locale JSON files. Since it is loading them, it might as well parse that attribute, so I bind the attribute from <hello-you> to <polymer-translate>:
<link rel="import" href="polymer-translate.html">
<polymer-element name="hello-you">
  <template>
    <!-- hello-you templates stuff here... -->
    <polymer-translate id="l10n"
                       locale={{locale}}
                       extraLocales={{extraLocales}}
                       locales={{locales}}
                       labels={{labels}}></polymer-translate>
  </template>
  <script type="application/dart" src="hello_you.dart"></script>
</polymer-element>
That <polymer-translate> “instance” is getting a little crowded. I was already pushing down the current locale for the initial translation. I am now pushing down the extra locales that the <hello-you> developer wants to support. I also need to define the out-of-the-box locales that are supported, which I define locally as the locales property and push down to <polymer-translate> as well. Lastly, the labels are what <polymer-translate> updates to localize <hello-you>. No too horrible—one way to specify the current locales, two ways to define the list of supported locales, and one way to retrieve the translated labels—still it bears watching so that it does not get overwhelming.

Next I teach <polymer-translate> how to honor extra locales. Once the <polymer-translate> instance has been added to the <hello-you> view—and the extraLocales attribute is bound to <hello-you>'s attribute of the same name—I add the extra locales:
@CustomTag('polymer-translate')
class PolymerTranslate extends PolymerElement {
  // ...
  @published String extraLocales;

  PolymerTranslate.created(): super.created();
  enteredView() {
    super.enteredView();

    _addExtraLocales();
    _loadTranslations();
   // ...
  }
  // ...
}
Doing so is a matter of splitting the string up appropriately:
@CustomTag('polymer-translate')
class PolymerTranslate extends PolymerElement {
  // ...
  @published String extraLocales;

  PolymerTranslate.created(): super.created();
  enteredView() {
    super.enteredView();

    _addExtraLocales();
    _loadTranslations();
   // ...
  }

  _addExtraLocales() {
    if (extraLocales == null) return;

    extraLocales.split(',').forEach((pair){
      var p = pair.split(':');
      locales.add({'short': p[0], 'full': p[1]});
    });
  }
  // ...
}
By adding the short/full Map to the locales list, I ensure that _loadTranslations can retrieve the extra locales in addition to the out-of-the-box ones:
  _loadTranslations() {
    locales.forEach((l10n){
      _loadLocale(l10n['short']);
    });
  }
Since this is data bound with locales back in <hello-you> I am also assured that the list of known locales will display in the drop-down list in that template:
<link rel="import" href="polymer-translate.html">
<polymer-element name="hello-you">
  <template>
    <!-- hello-you templates stuff here... -->
    <p>
      {{labels['language']}}:
      <select value="{{locale}}">
        <option template repeat="{{l10n in locales}}" value="{{l10n['short']}}">
          {{l10n['full']}}
        </option>
      </select>
    </p>
    <polymer-translate id="l10n"
                       locale={{locale}}
                       extraLocales={{extraLocales}}
                       locales={{locales}}
                       labels={{labels}}></polymer-translate>
  </template>
  <script type="application/dart" src="hello_you.dart"></script>
</polymer-element>
With that, I can localize <hello-you> in Italian:



Or even Pig Latin:



That wound up being a little more work than I originally anticipated, but it very well may be a reasonable approach. Ultimately, I am exposing an attribute on my Polymer that drives behavior in the strategy employed by that Polymer. That information needs to get from the Polymer (<hello-you>) to the strategy (<polymer-translate>) somehow and data binding is the obvious solution to that in Polymer.

The number of bound attributes on the strategy does not matter too much since it is internal and hidden from the developer working with my Polymer. Still, a large number of attributes tends to indicate an object that does too much, which is the very problem the strategy should be solving. In this case, I think I am OK with two conceptual settings (current locale and supported locales) plus the return value. But I need to be careful about adding anything else.


Day #1,043

No comments:

Post a Comment