Wir haben kürzlich eine Preview-Version unserer Polymer-basierten neuen Web UI veröffentlicht, die sich noch in ihrem Anfangsstadium befindet, aber bereits zahlreiche Funktionen enthält. Unsere neue Web UI stützt sich auf Nuxeo-UI-Elemente, die allgemeinen Bausteine, die zur Erstellung umfangreicher Web Apps für die Nuxeo Platform verwendet werden können. Sie bestehen aus mehreren benutzerdefinierten Elementen und Verhalten, die altbekannte aber komplexe Probleme lösen sollen. Heute behandeln wir eines davon: Lokalisierung.

Das Problem

Anfang des Jahres standen wir einem Problem gegenüber: wir mussten unsere Web UI in mehrere Sprachen lokalisieren. Unser Lokalisierungsansatz musste jedoch so flexibel sein, dass er für jedes Polymer-Element funktionierte, während Details wie das Laden von Nachrichten, Sprache und die Übersetzungsfunktion von der Anwendung übernommen und definiert werden sollten, die diese Elemente benutzte. Da Polymer keine serienmäßige Lösung für die Lokalisierung bereitstellen konnte, mussten wir eine eigene Lösung finden. Wir begannen damit, Polymer.Base eine allgemeine Funktion hinzuzufügen und diese mit allen TextContent, Eigenschaften und Attributen zu verbinden, für die Texte in Polymer-Elemente übersetzt werden musste.

<span>[[i18n('label.app.usersAndGroups')]]</span>

Dies funktionierte recht gut, zumindest so lange, wie wir nur eine Sprache nutzten. Sobald wir mit mehreren Sprachen experimentierten, mussten wir die Verbindungen für die verschiedenen Elemente mithilfe der Funktion neu bewerten, auch wenn sich weder die Kennzeichnungsschlüssel noch die Lokalisierungsfunktion geändert hatten, insbesondere wenn die Sprache durch ein externes Skript geändert wurde. Wir benötigten also mehr als das: eine allgemeine Funktion, die mit Datenbindung über verschiedene Elemente funktionieren würde, die jedoch auch außerhalb von Polymer-Elementen eingerichtet und verwendet werden konnte und trotzdem die Verbindungen der Elemente aktualisiert, die diese verwenden.

Die Lösung

Die Lösung für dieses Problem bestand darin, ein Polymer-Verhalten mit einer funktionsartigen Eigenschaft zu erstellen, dem die entsprechende allgemeine Funktion zugewiesen wird. (Ja! Obwohl es nirgends wirklich dokumentiert ist, können Eigenschaften in Polymer funktionsartig sein und verbunden sein.)
Diese Funktionseigenschaft kann dann von allen Polymer-Elementen in berechneten Verbindungen verwendet werden und ihr Verhalten erweitern. Das Verhalten sollte auch auf Änderungen in dieser allgemeinen Funktion achten, die durch ein anderes Element oder einen andren externen Code verursacht werden und eine Aktualisierung an allen berechneten Verbindungen und damit die Aktualisierung der Übersetzungen an allen Elementen durch Implementierung des Verhaltens auslöst.

Durch diese Vorgehensweise konnten wir das Lokalisierungsproblem lösen. Wir begannen, indem wir das eigenständige Skript nuxeo-i18n.jshinzufügten, das hauptsächlich eine Anpassung auf Anwendungsebene liefern sollte, etwa eine Definition der Übersetzungsfunktion, Wechsel der Sprache oder die Konfiguration neuer Strategien zum Laden von Nachrichten. Dieses Skript stellt eine allgemeine Übersetzungsfunktion bereit, die den Kennzeichnungsschlüssel und seine Standardwerte sowie die Anzahl der Parameter akzeptiert, die in dem daraus resultierenden String ersetzt werden können. Eine allgemeine Sprachvariable ist ebenfalls vorhanden, die die aktuelle Übersetzungssprache enthält.

window.nuxeo.I18n.translate = function (key, defaultValue) {
  var language = window.nuxeo.I18n.language || 'en';
  var value = (window.nuxeo.I18n[language] && window.nuxeo.I18n[language][key]) || defaultValue || key;
  var params = Array.prototype.slice.call(arguments, 2);
  for (var i = 0; i < params.length; i++) {
    value = value.replace('{' + i + '}', params[i]);
  } // improve this to use both numbered and named parameters
  return value;
};

Eine andere allgemeine Funktion ist dafür verantwortlich, die sprachspezifischen Ressourcen der aktuellen Sprache zu laden, die aus dem Polymer-Element oder aus einem anderen externen Skript aufgerufen werden können. Diese Funktion löst eine Benachrichtigung auf Dokumentebene aus, die das Laden einer neuen Sprachumgebung anzeigt (wir haben uns für eine flexible Lösung entschieden, um eine strikte Abhängigkeit von Polymer zu vermeiden).

window.nuxeo.I18n.loadLocale = function() {
  return window.nuxeo.I18n.localeResolver ? window.nuxeo.I18n.localeResolver().then(function() {
    document.dispatchEvent(new Event('i18n-locale-loaded'));
  }) : new Promise(function() {});
};

Die Funktion loadLocale stützt sich auf einen Resolver für die Sprachumgebung, also einem Objekt, das Ressourcen laden kann. Dadurch ist eine modulare und konfigurierbare Vorgehensweise beim Laden von Übersetzungsressourcen möglich und benutzerdefinierte Resolver können für verschiedene Konfigurationsarten bestimmt werden.
In der neuen Web UI verwenden wir einen Resolver für die Sprachumgebung, um JSON-Objekte über eine asynchrone XHR zu laden. Er ist wie folgt definiert:

window.nuxeo.I18n.localeResolver = new XHRLocaleResolver(msgFolder);

function XHRLocaleResolver(msgFolder) {
  return function() {
    return new Promise(function(resolve,reject) {
      var language = window.nuxeo.I18n.language || 'en';
      var url = msgFolder +  '/messages.' + language + '.json';
      var xhr = new XMLHttpRequest();
      xhr.open('GET', url, true);
      xhr.onreadystatechange = function() {
        if (xhr.readyState == 4 && xhr.status == 200) {
          window.nuxeo.I18n[language] = JSON.parse(this.response); // cache this locale.
          resolve(this.response);
        }
      };
      xhr.onerror = function() {
        console.error("Failed to load " + url);
        reject(this.statusText);
      };
      xhr.send();
    });
  }
}

Wir haben außerdem ein i18n Verhalten entwickelt, das auf dem nuxeo-i18n.js aufbaut, wo Elemente, nachdem sie dem Dokument-Knoten beigefügt wurden, dieses Verhalten erweitern und einen Event-Listener registrieren, um zu überprüfen, ob eine neue Sprachumgebung geladen wurde oder nicht. Dieser Listener führt eine Funktion aus, die eine allgemeine Übersetzungsfunktion zu einer funktionsartigen Eigenschaft i18n neu zuweist.

Nuxeo.I18nBehavior = {
  properties: {
    i18n: {
      type: Function,
      notify: true,
      value: function() {
        return window.nuxeo.I18n.translate;
      }
    }
  },

  attached: function() {
    this.localeLoadedHandler = this.refreshI18n.bind(this);
    document.addEventListener('i18n-locale-loaded', this.localeLoadedHandler);
  },

  detached: function() {
    document.removeEventListener('i18n-locale-loaded', this.localeLoadedHandler);
  },

  refreshI18n: function() {
    this.i18n = window.nuxeo.I18n.translate;
  }
};

Diese i18n Funktion kann dann wie im folgenden Beispiel mit einer Eigenschaft, einem Attribut oder einem Textinhalt eines Elements verbunden werden.

<span>[[i18n('label.app.usersAndGroups', 'Users & Groups')]]</span>

Alle Elemente, die in der neuen Web UI und in Nuxeo-UI-Elemente übersetzt werden müssen, implementieren dieses Verhalten. Damit dies alles funktioniert, muss zunächst einfach der Resolver der Sprachumgebung wie oben abgebildet in einem Skript festgelegt und dann die Sprache geändert werden, wodurch alle Kennzeichnungen aktualisiert werden:

window.nuxeo.I18n.language = 'en';
window.nuxeo.I18n.loadLocale();

Diese Vorgehensweise liefert die Logik, die hinter sich ändernden Sprachen steht und macht das Laden von Sprachumgebungen sehr einfach und modular. Es kontrolliert außerdem die Datenverbindung, egal ob Ressourcen aus einem Polymerelement oder aus einem externen JavaScript Code geladen werden.

Hinweis: Anfang Mai hat das Polymer-Team die AppLocalizeBehavior vorab veröffentlicht - ein Verhalten, das unserem Lokalisierungsverhalten sehr ähnlich ist, benutzerdefinierte allgemeine Funktionen jedoch nicht unterstützt und keine Logik besitzt, diese zu aktualisieren, sobald die Funktion extern geändert wurde.