AngularJS i18n – Teil 2 – globalize

Im ersten Teil des Beitrags wurde ein Blick auf „angular-translate“ geworfen, das wohl meist genutzte Modul um AngularJS Anwendungen multilingual zu entwickeln. In diesem Beitrag dreht sich alles um die Formatierung und das Parsen von länderspezifischen Formaten. Eine weit verbreitete Bibliothek für diese Aufgabe ist:  

Globalize

Globalize stammt aus dem jQuery-Universum und kümmert sich in den verschiedensten Bereichen um die Internationalisierung von Ein- und Ausgaben. Aktuell liegt die Version 1.0.0 vor die sich maßgeblich von den Vorgängerversionen unterscheidet. Globalize in der Version 0.x nutzte noch ein eigenes JSON Format um seine Formatierungs-Regeln zu definieren, wohingegen in der aktuellsten Version CLRD (Unicode Common Locale Data Repository, http://cldr.unicode.org/)  als Basis genutzt wird. Damit besteht die vollständige Einbindung von Globalize aus zwei Bereichen: Globalize selbst (modulare JavaScript-Dateien) und CLRD Format-Definitionen (JSON Dateien)
Basis für das folgende Beispiel ist ein simples AngularJS Projekt das mit Hilfe von grunt konfiguriert und gebaut wird. Zu Beginn werden wir jedoch verstärkt auf die konkreten Schritte eingehen die nötig sind um Globalize zu nutzen. Ein vollständiges Beispiel mit grunt folgt am Ende.

Installation, „Bower und los?“

Bower als Packet Manager macht es jedem Entwickler einfach neue Abhängigkeiten zu installieren. In diesem Fall reicht ein einfaches „install“ aber leider nicht aus. Wie anfangs bereits erwähnt greift Globalize auf ein zentrales Repository zu welches die benötigten Formatierungsregeln anbietet. Diese, im JSON-Format vorliegenden, Informationen können jedoch nicht direkt über Bower bezogen werden sondern müssen separat installiert werden. Das muss zum Glück niemand von Hand erledigen. Um das automatische Herunterladen der Informationen zu aktivieren, registrieren wir zwei entsprechende Scripte in unserer Bower-Konfiguration („.bowerrc“) :

  "scripts": {
      "preinstall": "npm install cldr-data-downloader@0.2.x",
      "postinstall": "node ./node_modules/cldr-data-downloader/bin/download.js -i public_html/bower_components/cldr-data/index.json -o public_html/bower_components/cldr-data/"
  }

Es folgt die eigentliche Installation der Pakete mit:

bower install globalize --save

(Wichtig: die oben registrierten Skripte werden wirklich nur ausgeführt wenn eine Installation über Bower stattgefunden hat. Sollten die Abhängigkeiten zu Globalize bereits vorhanden gewesen sein werden die Skripte nicht durchlaufen. Die beiden Schritte können dann natürlich auch manuell durchgeführt werden. Nach einer erfolgreichen Installation sollten die „*.json“ Dateien im Angegebenen Export Ordner vorliegen: public_html/bower_components/cldr-data/)

Als nächstes folgt die Einbindung in unsere HTML Seite:

 <script src="bower_components/cldrjs/dist/cldr.js">
 <script src="bower_components/cldrjs/dist/cldr/event.js">
  
 <script src="bower_components/globalize/dist/globalize.js">
 <script src="bower_components/globalize/dist/globalize/number.js">
 <script src="bower_components/globalize/dist/globalize/date.js">

Zuerst werden die benötigten CLDR-Bibliotheken referenziert (um später die Definitionen zu laden). Dann folgen die Globalize-Module die genutzt werden sollen. Folgende Module stehen derzeit zur Verfügung (Referenz: https://github.com/jquery/globalize#pick-the-modules-you-need )

  • globalize, Core Bibliothek
  • globalize/currency, Formatieren von Währungen
  • globalize/date, Formatieren von Kalenderdaten
  • globalize/message, Übersetzen von Texten
  • globalize/number, Formatieren von Zahlen
  • globalize/plural, Übersetzung von Zahlen
  • globalize/relative-time, Übersetzungen für relative Zeitangaben
  • globalize/units, Formatieren von Einheiten

Damit haben wir nun die Funktionen um Globalize zu nutzen. Was nun noch fehlt ist das Laden der benötigen Daten. Je nachdem welche Funktionen verwendet werden sollen, müssen die entsprechenden CLRD-Daten geladen werden: https://github.com/jquery/globalize/blob/master/README.md#2-cldr-content

Zu diesem Zweck existiert die Funktion:

Globalize.load(...);

die als Parameter die JSON-Informationen entgegen nimmt.
Das Laden der Definitionen könnte in AgularJS so aussehen:

var language=‘de‘;  
var clrdRef=[
    'bower_components/cldr-data/supplemental/likelySubtags.json',
    'bower_components/cldr-data/supplemental/numberingSystems.json',
    'bower_components/cldr-data/supplemental/timeData.json',
    'bower_components/cldr-data/supplemental/weekData.json',
    'bower_components/cldr-data/main/'+language+'/numbers.json',
    'bower_components/cldr-data/main/'+language+'/ca-gregorian.json',
    'bower_components/cldr-data/main/'+language+'/timeZoneNames.json'                ];

clrdRef.forEach(function(url){
   $http.get(url).then(function(resp) {
       Globalize.load(resp.data);
   });
});

Das Laden dieser Konfigurationen für Globalize sollte natürlich vor der Verwendung stattfinden. Da es sich bei den Zugriffen um asynchrone http-Requests handelt macht es uns AngularJS leider nicht leicht. Würden wir diese Initialisierung in der run-Methode unserer Anwendung ausführen wird es vorkommen das die Requests noch nicht beendet sind, Globalize noch nicht initialisiert ist und Formatierungsaufgaben durch Globalize mit einem Fehler quittiert werden. Um dieses Problem zu umgehen gibt es zwei Möglichkeiten:

  • manuelles Boostraping unserer Anwendung
  • Einsatz von UI-Router und die Verwendung des „resolve“ Attributes

Das Module „ui-router“ welches sich um die Navigation innerhalb unserer Anwendung kümmert bietet eine Möglichkeit die in der View verwendeten Controller mit Daten zu versorgen. Das hat den Vorteil das wir unsere Controller nicht mit einer Vielzahl von asyncronen Requests füllen müssen. Ein typischer Anwendungsfall ist z.B. für eine Seite die Daten in tabelarischer Form anzeigt, diese bereits vor dem Aufruf unserer Controllers zu laden und per Injection zur Verfügung zu stellen. Der Vorteil besteht darin das die Navigation erst nach vollständigem Laden der  asyncronen Aufgaben (promise) ausgeführt wird. Wir müssen nun nur sicher stellen das ein solches resolve bei jedem unserer Naviagationen ausgeführt wird. Zu diesem zweck nutzen wir die Möglichkeit von „ui-route“ mehrstufige Templates zu deklarieren. (weitere Details über ui-route gibt es hier: https://github.com/angular-ui/ui-router )

  
  .config(function ($stateProvider, $urlRouterProvider) {  
    $stateProvider
            .state('app', {
                abstract: true,
                template: '< div ui-view></div>',
                resolve: {
                    _init_: function ($window, GlobalizeLanguageLoader) {
                        var lang = $window.navigator.language || $window.navigator.userLanguage;   
                        return GlobalizeLanguageLoader.loadLanguage(lang);
                    }
                }
            })
            .state('app.home', {
                url: '/home',
                templateUrl: 'modules/home/home.html'
            })

Einige Dinge fallen auf:

  • ein als „abstrakt“ gekennzeichneter State
    • damit stellen wir lediglich sicher das dieser State nicht direkt aufgerufen werden kann
  • eine Template Definition:    template: < div ui-view></div>
    • streng genommen bilden wir ein mehrstufiges Template. Unsere index.html enthält eine Direktive „ui-view“ dessen Inhalt  mit den konkreten Seiten gefüllt wird. In dem Fall des States „app“ füllen wir diesen Bereich lediglich mit einer weiteren ui-view-Direktive die dann durch die konkreten Navigationen den eigentlichen Inhalt anzeigt.
  • „resolve“ innerhalb unseres „app-state“
    • dieser Resolve wird für jeden abhängigen State ausgeführt
    • ! der deklarierte Service wird bei jeder Navigation ausgeführt. Aus dem Grund sollten wir sicher stellen das wir die Initialisierung von Globalize nur initial oder nach einer Änderung der Sprache durchgeführen.
  • Alle weiteren Seiten werden als Abhängige Zustände deklariert „app.[state-name]“
  • die eigentliche Konfiguration von Globalize haben wir in diesem Beispiel in einen Service ausgelagert (Implementierung ähnlich wie oben zu sehen)

Mit diesem Vorgehen stellen wir sicher das Globalize, unabhängig von der aufgerufenen „Seite“, initialisiert ist bevor die Anwendung selbst ausgeführt wird.

Kann es jetzt endlich los gehen?

Die eigentlichen Formatierungen sind, wer hätte es gedacht, dann sehr einfach. Globalize bietet diverse Funktionen an um Daten zu formatieren und zu parsen, hier nur einige Beispiele:

  var g = new Globalize(language);
  vm.number = g.numberFormatter()(10000.88);
  vm.numberRounded = g.numberFormatter({round: "ceil", maximumFractionDigits: 0})(10000.88);
  vm.date = g.dateFormatter()(new Date());
  vm.dateLong = g.dateFormatter({datetime: "long"})(new Date());

Es liegt nahe eigene Direktiven zu entwickeln welche diese Aufgaben übernehmen. Für ein Datum könnte eine solche Direkte so aussehen:

  .directive("glDate", function (GlobalizeLanguageLoader) {
      return {
          restrict: 'A',
          require: 'ngModel',
          link: function (scope, element, attr, ngModel)
          {
              ngModel.$formatters.push(function (value) {
                  if (value) {
                      return GlobalizeLanguageLoader.get().dateFormatter({date: "short"})(value);
                  }
              });  
              ngModel.$parsers.push(function (value) {
                  return GlobalizeLanguageLoader.get().dateParser({date: "short"})(value);
              });  
          }
      };
  });

(in diesem Beispiel wird erneut unsere Factory eingesetzt. Diese enthält wie zu sehen, neben der Initialisierung von Globalize, auch eine Instanz von Globalie die bereits mit einer Sprache instanziiert wurde (new Globalize(‚de‘)).

Zugegebenermaßen ist die Konfiguration von globalize in der Version 1.X nicht ganz unkompliziert. Wenn die anfänglichen Hürden aber erst einmal gemeistert sind stützen wir unsere Anwendung auf eine tausendfach bewerte Internationalisierungs-Datenbank. Bei der eigentlichen Integration in AngularJS ist lediglich die strikte asyncrone Verarbeitung noch eine kleine Hürde bevor es dank Parser und Formatter an die eigenen AngularJS Direktiven geht.

Hier gibt es das Projekt noch mal komplett…

 

Advertisements

Über Dominik Mathmann
Dominik Mathmann arbeitet als Berater, Entwickler und Trainer bei der GEDOPLAN GmbH. Auf Basis seiner langjährigen Erfahrung in der Implementierung von Java-EE-Anwendungen leitet er Seminare, hält Vorträge und unterstützt Kunden bei der Konzeption und Realisierung von Webanwendungen vom Backend bis zum Frontend. Sein derzeitiger Schwerpunkt liegt dabei auf der Entwicklung von JSF-Anwendungen. Er sieht die stärker werdende Position von JavaScript-Frameworks jedoch positiv und beschäftigt sich darüber hinaus mit Webframeworks wie AngularJS.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: