Die Intl-API – Internationalisierung im Browser

Tales from the Web side Sebastian Springer  –  0 Kommentare

Internationalisierung ist in der Web-Entwicklung ein Thema, das häufig leider immer noch viel zu kurz kommt. Für die meisten endet der Begriff Internationalisierung auch schon bei der Übersetzung von Texten. Das ist jedoch eine Problemstellung, die der Browser nach wie vor den etablierten Bibliotheken überlässt. Wo alle modernen Browser jedoch mittlerweile dank der Intl API durchgängig punkten können, sind Themen wie die lokalisierte Formatierung von Zahlen, Datums- und Zeitwerten. Neben diesen Angaben bringt die Intl API noch Features zum lokalisierten String-Vergleich sowie Regeln für die Behandlung von Ein- und Mehrzahl.

Alle JavaScript-Umgebungen, also die bekannten Browser, Node.js und sogar der Internet Explorer, unterstützen die Intl API in Form des globalen Intl-Objekts. Es enthält neben der getCanonicalLocales-Methode die folgenden Konstruktoren:

  • Collator: Klasse für sprachabhängigen String-Vergleich
  • DateTimeFormat: Formatierung von Datums- und Zeitwerten
  • NumberFormat: Formatierung von Zahlen wie Währungen oder Prozentwerten
  • PluralRules: Unterstützung bei der Behandlung von Ein- und Mehrzahl

Die Intl API setzt durchgängig auf Gebietsschema-Informationen zur Steuerung der einzelnen Features. Zu diesem Zweck akzeptieren die Konstruktoren der Schnittstelle Locale-Strings, die den BCP-47-Vorgaben folgen, also beispielsweise “de” oder “de-DE”. Erzeugt ein Script eine neue Instanz einer der Intl-Klassen, akzeptiert diese entweder ein Array von Locales, eine einzelne Locale oder keinen Wert. Ein ungültiger Wert führt dazu, dass der Browser einen RangeError auswirft.

Die getCanonicalLocales-Methode liefert für eine Eingabe einen gültigen Locale-Wert zurück und verhält sich dabei wie die Konstruktoren der Intl-Unterklassen. Den Wert “DE” wandelt die Methode in “de” um, ein Array mit den Werten “de”, “de-de” und “DE-DE” wird zu “de” und “de-DE” und die Eingabe “de-DEU” führt zu einem RangeError. Bei einem Array entfernt die getCanonicalLocales-Methode alle Duplikate.

Alle Intl-Klassen verfügen über eine statische Methode mit dem Namen supportedLocalesOf. Diese Methode akzeptiert einen Locale-String oder ein Array von Locales und liefert ein Array von Locales zurück, das die jeweilige Klasse direkt unterstützt, ohne auf Standard-Locale zurückzugreifen.

Collator: String-Vergleiche

JavaScript verfügt über die Array.prototype.sort-Methode zur Sortierung von Arrays. Diese Methode sortiert die einzelnen Elemente anhand ihrer UTF-16-Codewerte. Alternativ akzeptiert die Sort-Methode eine Vergleichsfunktion. Diese Vergleichsfunktion erhält bei jedem Aufruf zwei Werte, die miteinander verglichen werden. Ist der erste Wert kleiner als der zweite, gibt die Vergleichsfunktion eine Zahl kleiner als Null zurück, ist der erste Wert größer, wird eine Zahl größer Null zurückgegeben. Sind beide Werte gleich, ist der Rückgabewert der Vergleichsfunktion Null. Je nach Region, in der die Applikation ausgeführt wird, kann das Verhalten dieser Sortierung unterschiedlich sein. Die Collator-Klasse der Intl API nimmt Entwicklern diese Aufgabe ab und sorgt dafür, dass die Sortierung für die gewählte Region passend ist.

Die compare-Methode einer Collator-Instanz hat eine zur sort-Methode der JavaScript Arrays passende Signatur, akzeptiert also zwei Werte und gibt entweder 1, 0 oder –1 zurück. Das folgende Beispiel sortiert das Array mit den Werten “Birnen”, “Äpfel” und “Zitronen” für die Locale “de-DE”.

const col = new Intl.Collator('de');
const arr = [ 'Birnen', 'Äpfel', 'Zitronen' ];
console.log(arr.sort(col.compare)); // ["Äpfel", "Birnen", "Zitronen"]

Wie zu erwarten, ist das Ergebnis “Äpfel, Birnen, Zitronen”. Für die Locale “sv-SE”, also Schwedisch in Schweden, sieht das Ergebnis ganz anders aus, da hier das “Ä” nach dem “Z” einsortiert wird. Und das ist nur ein Beispiel, bei dem die Collator-Klasse hilfreich ist. Ein weiterer typischer Anwendungsfall ist das Sortieren von Zahlenwerten. Standardmäßig sortiert JavaScript folgendermaßen: 1, 132, 22. Die erste Ziffer ist hier also jeweils ausschlaggebend und nicht der Zahlenwert. Die Collator-Klasse sieht eine Lösung vor, nach der numerische Werte korrekt interpretiert werden können. Dieses Verhalten ist jedoch deaktiviert und muss bei der Erzeugung der Collator-Instanz zunächst aktiviert werden. Das erfolgt entweder über die Unicode-Erweiterung “kn” der Locale-Zeichenkette, also in diesem Fall beispielsweise “de-u-kn", oder über die numeric-Option, deren Wert auf true gesetzt wird. Solche Optionen akzeptieren alle Intl-Konstruktoren in Form eines Objekts als zweites Argument bei der Instanziierung.

const col = new Intl.Collator('de', {numeric: true});
const arr = [132, 1, 22 ];
console.log(arr.sort(col.compare)); // [1, 22, 132]

Zusätzlich zu diesen Optionen unterstützt der Collator noch weitere, beispielsweise wie Groß- und Kleinschreibung unterschieden werden sollen. Diese insgesamt flexibele Klasse sorgt dafür, dass für einen Stringvergleich kaum noch benutzerdefinierte Funktionen geschrieben werden müssen.

DateTimeFormat: Formatierung von Datum und Zeit

Eine der großen Schwächen der JavaScript Date-Klasse ist, dass sich die mit ihr erzeugten Objekte kaum formatiert ausgeben lassen. Dafür benötigt eine Applikation immer eine zusätzliche Bibliothek. Die Intl API löst zumindest einen Teil dieser Probleme. Die toString-Methode des Date-Objekts liefert den folgenden Wert: “Fri Jun 05 2020 10:15:00 GMT+0200 (Central European Summer Time)”. In einer Web-Applikation ist eine solche Zeichenkette jedoch wenig sinnvoll. Normalerweise sollen nur Datum oder Zeit in der korrekten Formatierung angezeigt werden – also beim Datum etwa 05.06.2020, 05/06/2020 oder 06/05/2020. Die Date-Klasse unterstützt eine solche Formatierung nicht direkt, sondern nur über einzelne Methoden wie getDay oder getMonth, die allerdings nur einstellige Ausgaben für den 5. beziehungsweise Juni liefern. Entwickler müssen hier also wieder selbst tätig werden. Die DateTimeFormat-Klasse der Intl API liefert mit ihrer format-Methode eine elegantere Lösung:

const dateTime = new Intl.DateTimeFormat('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});

console.log(dateTime.format(date)); // 05.06.2020

Eine Änderung der Locale auf “en-US” führt zur Ausgabe von “06/05/2020” und “en-GB” sorgt für “05/06/2020”. Das Options-Objekt der DateTimeFormat-Klasse entscheidet über das Aussehen und auch den Inhalt der einzelnen Elemente des Date-Objekts. Wird die day-Eigenschaft beispielsweise weggelassen, fällt auch die Tagesangabe bei der Ausgabe weg. Die meisten Eigenschaften des Options-Objekts unterstützen die Werte “2-digit”, also auf zwei Stellen aufgefüllt, und “numeric”, was je nach Wert eine einstellige, zweistellige oder bei der Jahreszahl eine vierstellige Ausgabe produziert. Bei der Formatierung von Zeitwerten verhält sich die DateTimeFormat-Klasse wie das Datum:

const dateTime = new Intl.DateTimeFormat('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});

console.log(dateTime.format(date)); //10:15:00

Beim Formatieren von Datum und Uhrzeit hilft die DateTimeFormat-Klasse, allerdings sind die Möglichkeiten eingeschränkt. Eine komplett freie Formatierung ist nicht vorgesehen. Es gibt aber die formatToParts-Methode, die jeden einzelnen Teil des Ergebnisses als ein separates Array-Element enthält:

const dateTime = new Intl.DateTimeFormat('de-DE', {
hour: '2-digit',
minute: '2-digit',
});

console.log(dateTime.formatToParts(date));

/* Ausgabe:
[{type: "hour", value: "10"},
{type: "literal", value: ":"},
{type: "minute", value: "15"}]*/

Mit den einzelnen Teilen des resultierenden Objekts lassen sich beliebige Formate realisieren. Hier ist jedoch umfangreiche Handarbeit erforderlich – es wird jedoch deutlich einfacher als mit dem nativen Date-Objekt von JavaScript.

NumberFormat: Zahlen formatieren

Die NumberFormat-Klasse kümmert sich, wie der Name andeutet, um das Formatieren von Zahlenwerten und zwar in drei verschiedenen Arten. Die style-Eigenschaft bestimmt, ob es sich um eine gewöhnliche Zahl (decimal), einen Währungswert (currency) oder einen Prozentwert (percent) handelt. Der Standardwert für die Option style ist “decimal” und useGrouping weist “true” auf, sodass immer ein Tausendertrennzeichen verwendet wird und die im Beispiel verwendete Option auch weggelassen werden könnte. Der nachfolgende Code gibt die Zahl 3141,59 mit Tausendertrenner und zwei Nachkommastellen aus:

const number = Math.PI * 1000; 
const numberFormatter = new Intl.NumberFormat('de-DE', {
useGrouping: true,
maximumFractionDigits: 2,
});

const formattedNumber = numberFormatter.format(number);

console.log(formattedNumber); // 3.141,59

Mit der Option useGrouping kann gewählt werden, ob der Tausendertrenner angezeigt werden soll. Die Änderung der Locale im Konstruktor auf den Wert “en-US” führt zur Ausgabe von 3,141.59.

Ein weiteres wichtiges Feature der NumberFormat-Klasse ist die Formatierung von Währungswerten. Dieses Feature wird durch die style-Eigenschaft mit dem Wert “currency” aktiviert. Zusätzlich dazu muss die Währung über die currency-Eigenschaft übergeben werden. Erfolgt dies nicht, wirft der Browser einen TypeError aus. Zur Auswahl der Werte stehen die ISO-4217-Währungscodes wie “EUR” oder “USD” bereit. Der nachstehende Code gibt den Wert €3,141.59 aus:

const number = Math.PI * 1000; 
const numberFormatter = new Intl.NumberFormat('en-GB', {
style: "currency",
currency: "EUR",
maximumFractionDigits: 2,
});

const formattedNumber = numberFormatter.format(number);

console.log(formattedNumber); // €3.141,59

Die currencyDisplay-Eigenschaft steuert die Anzeige des Währungssymbols. Der Standardwert “symbol” zeigt in diesem Beispiel das Euro-Symbol an. Weitere mögliche Werte sind “code” für den ISO-Code und “name” für den ausführlichen Namen der Währung.

PluralRules: Einzahl oder Mehrzahl, das ist hier die Frage

Eine Instanz der PluralRules-Klasse gibt mit der select-Methode für eine Zahl eine Zeichenkette zurück, die angibt, ob es sich um Ein- oder Mehrzahl handelt. Zugegebenermaßen ist diese Klasse für die deutsche Locale eher unspektakulär. Bei der type-Option mit dem Standardwert “cardinal” gibt die Methode für die Zahl 1 den Wert “one” und für alle übrigen den Wert “other” zurück. Dieser Typ steht für Mengen. Mit dem “ordinal”-Typ kann gezählt werden, was für die deutsche Locale durchgängig die Werte “other” zurückgibt. Anders sieht die Situation für die Locale “en-US” aus, wie folgendes Codebeispiel zeigt:

const arr = new Array(24).fill('').map((v, i) => i);
const pr = new Intl.PluralRules('en-US', {type: 'ordinal'});

arr.forEach(v => console.log(v, pr.select(v)));

Für alle Werte, die auf 1 enden, gibt die select-Methode “one”, für 2 den Wert “two”, für 3 den Wert “few” aus, und für alle weiteren Eingaben liefert select die Ausgabe “other” zurück. Eine Umstellung auf den “cardinal”-Typ liefert wieder “one” für 1 und “other” für die übrigen Werte zurück.

Fazit: hilft uns die Intl API wirklich?

Die Intl API bietet einige sinnvolle Erweiterungen für den JavaScript-Sprachstandard, um internationale Applikationen zu ermöglichen. Das Thema Übersetzungen spart die Schnittstelle aber komplett aus und überlässt es zusätzlichen Bibliotheken. Aber gerade die Formatierung von Datums-, Zeit- und Zahlenwerten löst eine Reihe von Standardproblemen auf elegante und solide Art.