Während ich in unserer AngularJS-Tipps-und-Tricks-Kolumn bei Heise Developer auf die Umsetzung mehrsprachiger AngularJS-Anwendungen mittels angular-translate eingehe, zeige ich hier eine Möglichkeit, in globalisierten Anwendungen verschiedene Zahlen- und Datumsformate zu unterstützen. Hierzu gibt es ja bereits ein paar einfache Filter, wie date oder number, in AngularJS. Diese helfen jedoch nur bei der Ausgabe von Daten. Möchte man eine ähnliche Unterstützung auch für Eingabeformulare haben, muss man zu anderen Frameworks greifen. Eines davon ist Globalize (https://github.com/jquery/globalize#0.x-fixes), welches auch in der Lage ist, Werte in Hinblick auf die Gepflogenheiten einer Sprache und/oder eines Landes zu parsen.
Als der vorliegende Text verfasst wurde, hat das AngularJS-Team angekündigt, mit der künftigen Version 1.4 mehr Möglichkeiten für die Internationalisierung von Single Page Applications out-of-the-box bieten zu wollen.
Überblick zu Gobalize
Das im Umfeld der populären JavaScript-Bibliothek jQuery entwickelte Globalisierungs-Framework Globalize stellt unter anderem Formatierungsinformationen für zahlreiche Kulturen zur Verfügung, wobei der Begriff Kultur in diesem Kontext als Kombination von Region und Sprache zu verstehen ist. Beispiele dafür sind Deutsch/Deutschland (de-DE), Deutsch/Österreich (de-AT), Deutsch/Schweiz (de-CH) oder Englisch/Großbritannien (en-GB).
Als der vorliegende Text verfasst wurde, lag Globalize in zwei Versionssträngen vor: 0.x und 1.x. Letzterer befand sich noch in einer Alpha-Phase, weswegen hier mit dem praxiserprobten Versionsstrang 0.x vorliebgenommen wird. Die beiden Versionsstränge unterscheiden sich in erster Linie durch die Art der Definition der unterstützten Kulturen. Während 0.x hierzu ein eigenes auf JSON-basiertes Format nutzen, nutzt 1.x die Dateien aus dem Unicode Common Locale Data Repository (CLDR, http://cldr.unicode.org/).
Um die hier betrachtete Version von Globalize via Bower zu installieren, verwendet der Entwickler die folgende Anweisung:
bower install globalize#0
Um Globalize zu nutzen, bindet der Entwickler das Skript globalize.js sowie die Skripte mit den Formatierungsinformationen für die zu unterstützenden Kulturen ein. Das Beispiel im nachfolgenden Listing referenziert zum Beispiel die Formatierungsinformationen für de-DE, de-CH und de. Letztere bezieht sich auf keine spezielle Region und kommt als Fallback für von der Anwendung nicht unterstützte deutschsprachige Regionen zum Einsatz. Im betrachteten Fall wäre Liechtenstein ein Beispiel für solch eine Region, zumal keine Datei mit Formatierungsinformationen für Deutsch/Liechtenstein eingebunden ist. Wäre auch die Datei globalize.culture.de.js nicht referenziert, würde Globalize in diesem Fall mit der von JavaScript standardmäßig verwendeten Kultur vorlieb nehmen und sich am amerikanischen Englisch orientieren.
<script src="~/Scripts/jquery.globalize/globalize.js"></script>
<script src="~/Scripts/jquery.globalize/cultures/globalize.culture.de-DE.js"></script>
<script src="~/Scripts/jquery.globalize/cultures/globalize.culture.de-CH.js"></script>
<script src="~/Scripts/jquery.globalize/cultures/globalize.culture.de-AT.js"></script>
<script src="~/Scripts/jquery.globalize/cultures/globalize.culture.de.js"></script>
Um Globalize auf eine Kultur einzustimmen, ist deren Bezeichner an die Funktion Globalize.culture zu übergeben:
Globalize.culture("de-AT");
Anschließend kann der Entwickler die Funktion Globalize.format zum Formatieren von Zahlen und Datumswerten verwenden. Dazu übergibt er an diese Funktion den zu formatierenden Wert und eine Formatierungszeichenfolge. Beispiele dafür finden sich im nächsten Listing. Eine detaillierte Übersicht über die unterstützten Formatierungszeichenfolgen findet sich in der Dokumentation von Globalize unter https://github.com/jquery/globalize.
// Zahl ohne Kommastellen
alert(Globalize.format(123456789.88, "n"));
// Zahl mit drei Kommastellen
alert(Globalize.format(123456789.888888, "n3"));
// Zahl mit fünf Kommastellen
alert(Globalize.format(3.888888, "n5"));
var firstContact = new Date(2063, 4, 5, 17, 3);
// Datumsformat der gewählten Kultur
alert(Globalize.format(firstContact, "d"));
// Zeitformat der gewählten Kultur
alert(Globalize.format(firstContact, "t"));
// Angegebenes Datumsformat
alert(Globalize.format(firstContact, "dd.MM.yyyy HH:mm"));
Neben dem Formatieren von Ausgaben unterstützt Globalize auch das Parsen von Eingaben. Dazu bietet es die Funktionen Globalize.parseInt, Globalize.parseFloat und Globalize.parseDate. Diese Funktionen nehmen einen String mit einer Zahl bzw. mit einem Datum im Format der gewählten Kultur entgegen und geben eine Zahl ohne Nachkommastellen (parseInt), eine Zahl mit Nachkommastellen (parseFloat) oder ein Date-Objekt (parseDate) zurück:
var date = Globalize.parseDate("5.4.2063");
var anInt = Globalize.parseInt("1.000");
var aFloat = Globalize.parseFloat("1.000,95");
Über einen optionalen zweiten Parameter kann der Entwickler an diese Funktionen eine Formatierungszeichenfolge übergeben.
Globalize-Wrapper für AngularJS bauen
Um Globalize in AngularJS-Anwendungen zu nutzen, empfiehlt es sich, hierfür Direktiven und Filter bereitzustellen. Dieser Abschnitt geht darauf ein. Wer die hier gezeigten Konstrukte einfach nutzen möchte, kann das Modul i18n aus dem angehängten Beispiel übernehmen und mit dem nächsten Abschnitt, der zeigt, wie die hier vorgestellten Konstrukte zu nutzen sind, fortfahren.
Das Beispiel im nachfolgenden Listing zeigt ein Modul i18n mit einer solchen Direktive. Sie richtet für ein Eingabefeld einen Parser sowie einen Formatter ein. Ersterer parst die Eingabe des Benutzers unter Berücksichtigung der konfigurierten Kultur und wandelt diese in eine Zahl um. Letzterer formatiert eine Zahl unter Berücksichtigung dieser Kultur für die Ausgabe im Eingabefeld. Hierzu kommt Globalize zum Einsatz. Der Parser meldet darüber hinaus auch via $setValidity einen eventuell entdeckten Fehler als Validierungsfehler an AngularJS.
Zusätzlich zum Parser und Formatter richtet die Direktive auch eine Ereignisbehandlungsroutine für das Ereignis i18n.cultureUpdated ein. Durch das Auslösen dieses Ereignisses kann die Anwendung über den Wechsel der konfigurierten Sprache informieren. Die betrachtete Ereignisbehandlungsroutine formatiert in diesem Fall den gebundenen Wert aus dem Model erneut unter Nutzung sämtlicher registrierter Formatter. Auf diese Weise ergibt es ein String, der den gebundenen Wert unter Berücksichtigung der neuen Kultur wiederspiegelt. Damit das jeweilige Eingabefeld diesen String anzeigt, übergibt ihn die betrachtete Ereignisbehandlungsroutine an die Funktion $setViewValue und ruft anschließend $render auf. Erstere legt den in der View anzuzeigenden Wert fest; letztere führt zum Aktualisieren des Eingabefelds mit diesem Wert.
var i18n = angular.module("i18n", []);
i18n.directive(gnumber, function ($log) {
return {
require: ngModel,
link: function (scope, elm, attrs, ctrl) {
var format = attrs.gnumber;
ctrl.$parsers.unshift(function (viewValue) {
var number = Globalize.parseFloat(viewValue);
if (!isNaN(number)) {
ctrl.$setValidity(gnumber, true);
return number;
}
else {
ctrl.$setValidity(gnumber, false);
return undefined;
}
});
ctrl.$formatters.push(function (value) {
return Globalize.format(value, format);
});
scope.$on("i18n.cultureUpdated", function() {
var value = ctrl.$modelValue;
for(var i = ctrl.$formatters.length-1; i>=0; i--) {
var formatter = ctrl.$formatters[i];
value = formatter(value);
}
ctrl.$setViewValue(value);
ctrl.$render();
});
}
};
});
Der Vollständigkeit halber zeigt das nächste Listing eine analoge Direktive, welche sich unter Verwendung von Globalize um Datums-Werte kümmert. Der Formatter prüft zusätzlich, ob das zu formatierende Datum tatsächlich in Form eines Date-Objekts oder als String vorliegt. Letzteres ist zum Beispiel der Fall, wenn das Datum in Form von JSON von einem HTTP-Service abgerufen wurde, zumal es in JSON keine direkte Möglichkeit zur Darstellung von Datumswerten gibt. Handelt es sich um einen String, wandelt der Formatter diesen in ein Date-Objekt um. Dazu nutzt er die Konstruktorfunktion Date.
i18n.directive(gdate, function ($log) {
return {
require: ngModel,
link: function (scope, elm, attrs, ctrl) {
var fmt = attrs.gdate;
ctrl.$parsers.unshift(function (viewValue) {
var d = Globalize.parseDate(viewValue);
if (d) {
ctrl.$setValidity(gdate, true);
return d;
}
else {
ctrl.$setValidity(gdate, false);
return undefined;
}
});
ctrl.$formatters.push(function (value) {
if (typeof value === "string") {
value = new Date(value);
}
var formatted = Globalize.format(value, fmt);
return formatted;
});
scope.$on("i18n.cultureUpdated", function() {
var value = ctrl.$modelValue;
for(var i = ctrl.$formatters.length-1; i>=0; i--) {
var formatter = ctrl.$formatters[i];
value = formatter(value);
}
ctrl.$setViewValue(value);
ctrl.$render();
});
}
};
});
Das Formatieren von Ausgaben mit Globalize übernimmt der Filter im nächsten Listing. Er nimmt den zu formatierenden Wert sowie das gewünschte Format entgegen und delegiert an Globalize.format. Da das Ergebnis dieses Filters nicht nur von diesen übergebenen Werten, sondern auch von der via Globalize konfigurierten Kultur abhängt, ist die hinter dem Filter stehende Funktion mit einer Eigenschaft $stateful, die den Wert true aufweist, zu versehen. Dies führt dazu, dass AngularJS den Filter im Zuge jeder Digest-Phase ausführt - unabhängig davon, ob sich die an den Filter übergebenen Werte geändert haben.
i18n.filter(globalize, function () {
var filterFunction = function (input, format) {
return Globalize.format(input, format);
};
filterFunction.$stateful = true;
return filterFunction;
});
Die Direktive im nächsten Listing kümmert sich um dieselbe Aufgabe, wie der zuvor beschriebene Filter. Allerdings gestaltet sich diese performanter, zumal sie die Ausgabe nur aktualisiert, wenn sich der gebundene Wert ändert oder die Anwendung über das Ereignis i18n.cultureUpdated darüber informiert, dass sich die via Globalize konfigurierte Kultur geändert hat. Die betrachtete Direktive legt fest, dass ein an das Attribut gformat übergebener Wert an die Scope-Eigenschaft gformat zu binden ist. Mehr Details dazu finden sich in Kapitel 13, welches auf die Möglichkeiten hinter benutzerdefinierten Direktiven eingeht. Die Funktion link, welche die Direktive aktiviert, definiert eine Funktion render. Diese delegiert an Globalize.format und übergibt dabei den an gformat gebundenen Wert sowie den Wert des Attributs format, welches das gewünschte Format der Ausgabe wiederspiegelt. Den auf diese Weise erhaltenen String platziert die Direktive mit der Funktion html als Inhalt jenes Elements, auf das die Direktive angewendet wird. Diese Funktion kommt auch schon innerhalb von link zur Ausführung. Außerdem registriert link diese Funktion als Handler für das Ereignis i18n.cultureUpdated, sodass sie beim Wechsel der konfigurierten Kultur erneut zur Ausführung kommt. Da diese Funktion auch nach einer Änderung des gebundenen Werts erneut anzustoßen ist, übergibt das betrachtete Beispiel die Funktion render auch unter Angabe der hierfür verwendeten Scope-Eigenschaft an $watch.
i18n.directive("gformat", function($log) {
return {
scope: {
"gformat": "=",
},
link: function(scope, elem, attrs) {
var render = function() {
var value = Globalize.format(scope.gformat, attrs.format);
elem.html(value);
};
render();
scope.$on("i18n.cultureUpdated", render);
scope.$watch("gformat", render);
}
};
});
Globalize-Wrapper in AngularJS-Anwendung nutzen
Die Beispiele in den nächsten Listings zeigen, wie der Entwickler die zuvor beschriebenen Konstrukte, welche an Globalize delegieren und sich im Modul i18n befinden, nutzen kann. Es richtet ein Modul demo ein, welches das das Modul i18n referenziert und legt in der an run übergebenen Funktion, die AngularJS nach dem Konfigurieren des Moduls zur Ausführung bringt, den Wert de-DE als zu nutzende Kultur fest. Der Controller richtet im Scope eine Eigenschaft model ein. Das dahinter stehende Objekt beinhaltet eine Eigenschaft culture, welche die vom Benutzer konfigurierte Kultur wiederspiegelt und eine Funktion setCulture, welche Globalize unter Verwendung dieser Kultur konfiguriert. Außerdem löst diese Funktion das Ereignis i18n.cultureUpdated aus, damit sich die Direktiven unter Verwendung der neuen Kultur aktualisieren.
var app = angular.module("demo", ["i18n"]);
app.run(function() {
Globalize.culture("de-DE");
});
app.controller("DemoCtrl", function($scope, $rootScope) {
$scope.model = {};
$scope.model.culture = Globalize.culture().name;
$scope.model.setCulture = function() {
Globalize.culture($scope.model.culture);
$rootScope.$broadcast("i18n.cultureUpdated");
}
$scope.model.date = new Date();
$scope.model.num = 13.7603;
});
Das Beispiel im nächsten Listing nutzt die zuvor vorgestellten Direktiven gdate und gnumber, um Werte aus dem Scope in formatierter Form an Eingabefelder zu binden. Außerdem verwendet es die Direktive gformat und den Formatter globalize, um Werte aus dem Scope formatiert auszugeben.
Über das Eingabefeld im oberen Bereich, welches an model.culture gebunden ist, kann der Benutzer eine andere zu nutzende Kultur erfassen. Durch Klick auf die daneben platzierte Schaltfläche, welche an die Funktion model.setCulture gebunden ist, kann der die Eingabe dieser Kultur bestätigen und diese aktivieren.
<div ng-app="demo" ng-controller="DemoCtrl">
<input ng-model="model.culture"><input type="button" value="Ok" ng-click="model.setCulture()">
<div>
<form name="form">
<div>
<input ng-model="model.date" gdate="d" name="date">
<span ng-show="form.date.$invalid">*</span>
</div>
<div>
<input ng-model="model.num" gnumber="n2" name="num">
<span ng-show="form.num.$invalid">*</span>
</div>
</form>
</div>
<div gformat="model.date" format="d"></div>
<div>{{ model.date | globalize:d }}</div>
</div>
Quellcode (1,2MB)