Sunday, February 23, 2014

A Dart Strategy for Polymer I18n


If Polymers are to be used across horizontal concerns (sites, apps, pages), then it stands to reason that internationalization is going to be an important use case. I am slowly settling on coming up with a strategy (or should I say Strategy) to accomplish that. Rather that identify a library solution for i18n, I think it makes sense to use a child Polymer that responds to the t() / translate method. I have a working implementation for that in JavaScript. Tonight, I would like to switch back to Dart to see if it will work.

I have every reason to expect that it will work, but that's usually when things go wonky on me…

As with the JavaScript version of my i18n example Polymer, my Dart example application is currently more than just a Polymer. It is a simple <hello-you> Polymer. Also, it is a localized Polymer. As I did with the JavaScript version, I would like to refactor it so that it is a simple <hello-you> Polymer that has a translator associated with it. In other words, I want a translation Strategy.

I begin to realize that the best way to associate other objects in Polymer is to add them to the Polymer definition's <template> instead of as a property of the backing class:
<polymer-element name="hello-you">
  <template>
    <!-- ... -->
    <!-- Lots of bound labels and such to be translated here -->
    <!-- ... -->
    <polymer-translate locale={{locale}}></polymer-translate>
  </template>
  <script type="application/dart" src="hello_you.dart"></script>
</polymer-element>
This allows the containing class to bind its locale variable so that the child object—<polymer-translate> which I will write next—automatically sees that variable change.

Like the JavaScript version, the Dart <polymer-translate> strategy needs to support the locale attribute and a translate method, t(). The HTML definition is UI-less and nothing more than a front for the backing class:
<polymer-element name="polymer-translate">
  <script type="application/dart" src="polymer_translate.dart"></script>
</polymer-element>
The backing class is where the attribute and t() then get defined:
@CustomTag('polymer-translate')
class PolymerTranslate extends PolymerElement {
  @published String locale = 'en';
  PolymerElement target;
  PolymerTranslate.created(): super.created();
  t() {
    // translate the target element here...
  }
}
The implementation details are then left up to the individual strategy. The important part of this approach is that the Polymer being translated needs to bind the locale attribute if it wants to be able to dynamically change translations and it needs to manually invoke the translation Polymer whenever is sees a change that needs a translation. For instance, in my <hello-you> Polymer, when the human user updates the number of balloons currently held, then an update t() translation is needed:
@CustomTag('hello-you')
class HelloYou extends PolymerElement {
  // ...
  @observable int count;
  @observable String count_message;
  // ...
  HelloYou.created(): super.created();

  ready() {
    super.ready();

    i18n = this.shadowRoot.querySelector('polymer-translate');
    i18n.target = this;

    changes.listen((list){
      list.
        where((r) => r.name == #count).
        forEach((_){ i18n.t(); });
    });
  }
  // ...
}
That does the trick. I can set the locale for two separate <hello-you> tags on a page:
      <hello-you locale="fr"></hello-you>
      <hello-you locale="es"></hello-you>
Which results in two dynamic, localized versions of the element:



I rather like this approach, though in Dart it definitely suffers from an inability to work with the built-in dart:intl approach to i18n. That might not be a bad thing considering the effort required for that code generation approach, but it would be nice to be able to support that as well.

An HttpRequest Strategy


For completeness, here is the HttpRequest based strategy for loading and applying localizations:

import 'package:polymer/polymer.dart';
import 'dart:html';
import 'dart:convert';

@CustomTag('polymer-translate')
class PolymerTranslate extends PolymerElement {
  @published String locale = 'en';
  PolymerElement target;
  PolymerTranslate.created(): super.created();

  Map translations = {};

  ready() {
    _loadTranslations();
  }

  _loadTranslations() {
    _loadLocale('en');
    _loadLocale('fr');
    _loadLocale('es');
  }

  _loadLocale(_l) {
    HttpRequest.
      getString('/packages/i18n_example/elements/hello-you-${_l}.json').
      then((res) {
        translations[_l] = JSON.decode(res);
        t();
      });
  }

  attributeChanged(String name, String oldValue, String newValue) {
    if (name == 'locale') t();
  }

  t() {
    if (!translations.containsKey(locale)) return;

    var t = translations[locale];
    target.hello = t['hello'];
    target.colorize = t['colorize'];
    target.instructions = t['instructions'];
    target.how_many = t['how_many'];
    target.language = t['language'];

    var count = 0;
    if (target.count.runtimeType == int) count = target.count;
    if (target.count.runtimeType == String) {
      count = target.count.isEmpty ? 0 : int.parse(target.count);
    }

    if (count == 0) target.count_message = t['balloons_zero'];
    else if (count == 1) target.count_message = t['balloons_one'];
    else target.count_message = t['balloons'].replaceAll('__count__', count.toString());
  }
}



Day #1,035

2 comments:

  1. Please do not use "t" as the name of the function. It makes the code more difficult to read. Please always use non abbreviated, meaningful names, like .translate() or .translation()

    ReplyDelete
    Replies
    1. That's a good suggestion. I see no downside to that and I agree that it's easier to read — I'll definitely use translate() in the book. Thanks!

      Delete