zurück zum Artikel

Internationalisierung für Angular, Teil 1: Bordmittel

Daniel Schwab
Internationalisierung mit Angular-Bordmitteln

Bei der Konzeption und Umsetzung von Angular ab Version 2 lag der Fokus stets auf Performance. Auch im Bereich Internationalisierung hat diese Schwerpunktsetzung Spuren hinterlassen.

Internationalisierung, kurz I18n oder L10n für Localization, bezeichnet das Ziel, Anwendungen für unterschiedliche Kulturen, Sprachen und Regionen einfach anpassen zu können. Schon mit der ersten Version von Angular, besser bekannt als AngularJS, gab es für diese Aufgabe Mittel und Wege [1]. Ab Version 2 nutzt Angular jedoch einen ganz anderen Ansatz. Durch Aufruf des Kommandozeilen-Tools ng-xi18n, das über das Modul @angular/compiler-cli verfügbar ist, oder über die Angular CLI mit dem Befehl ng xi18n wird im HTML nach markierten Stellen gesucht, die das Framework übersetzen soll. Die daraus generierte XML-Datei integriert der Angular-Compiler nach der Übersetzung wieder in die Applikation.

Beim Dateiformat kann man sich zwischen dem XML Localisation Interchange File Format (XLIFF [2]) in Version 1.2 oder 2.0 (ab Angular 4.1) sowie XML Message Bundles (XMB) entscheiden. Im weiteren Verlauf wird das Dateiformat XLIFF (1.2), kurz xlf, genutzt.

Nachdem entweder über die Angular CLI ng xi18n oder direkt das Tool ng-xi18n im Projekthauptverzeichnis gestartet wurde und fehlerfrei durchgelaufen ist, erscheint im Projekt die Datei messages.xlf:

<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
</body>
</file>
</xliff>

Da noch keine Texte zur Übersetzung markiert wurden, ist der Inhalt noch überschaubar, zeigt aber schon den grundsätzlichen Aufbau der Datei und das XLIFF-Format. Im folgenden Quelltext kommt das HTML-Attribut i18n zum Einsatz. Es markiert dort die Texte einer Tabelle, genauer gesagt eines Tabellenkopfes, welche Flugdaten anzeigen soll:

<table class="table table-striped">
<thead>
<tr>
<th i18n>From</th>
<th i18n="Beschreibung des Feldes">To</th>
<th i18n="Spezielle Bedeutung|Beschreibung des ↵
Feldes">Passengers</th>
<th>
<ng-container i18n="@@eigene.id">Children</ng-container>
</th>
<th>
<!--i18n: Spezielle Bedeutung|Beschreibung des Feldes -->
Return Flight
<!--/i18n-->
</th>
<th i18n-title title="Booking Date">#</th>
</tr>
</thead>
</table>

Dem Attribut i18n lässt sich eine Beschreibung und, durch eine Pipe getrennt, eine Bedeutung als Zusatzinformation mitgeben. Es ist auch möglich, ab Version 4 von Angular durch @@ eine ID anzugeben. Diese Informationen tauchen zur Orientierung in der Übersetzungsdatei auf, haben aber sonst keinen Einfluss:

<th i18n>Text</th>
<th i18n="Beschreibung">Text</th>
<th i18n="Bedeutung|Beschreibung">Text</th>
<th i18n="@@id">Text</th>
<th i18n="Bedeutung|Beschreibung @@id">Text</th>

Um Texte auch ohne einen eigenen HTML-Tag zu markieren, können Entwickler ng-container einsetzen:

<ng-container i18n="Bedeutung|Beschreibung@@ID">Text</ng-container>

Als Alternative eignet sich ein HTML-Kommentar:

<!--i18n: Bedeutung|Beschreibung@@ID -->
Text
<!--/i18n-->

Um HTML-Attribute zu markieren, reicht es, sie per Bindestrich an i18n anzuhängen. Auch hier sind Beschreibung, Bedeutung und ID möglich:

<th i18n-title="Bedeutung|Beschreibung@@ID" title="Text">#</th>

Es zeigt sich bereits ein entscheidender Unterschied zu Libraries wie ngx-translate. Während in ihnen Referenzen vergeben werden, welche die Bibliothek später durch den eigentlichen Inhalt ersetzt, bleibt mit den neuen Tools der ursprüngliche Text erhalten. Dadurch wird er auch nach der Markierung angezeigt, selbst wenn keine Übersetzung in Form einer XML-Datei vorhanden ist.

Eine eigene Übersetzungsdatei in Englisch ist deshalb für die Beispieltabelle nicht zu erstellen. Das Angeben von Beschreibung, Bedeutung und ID über den Attributwert kann Übersetzern die Orientierung später wesentlich erleichtern.

Nach dem Fertigstellen der Markierung können Entwickler ihr bevorzugtes Tool erneut starten. Die erweiterte Datei messages.xlf sieht wie folgt aus:

<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" ↵
original="ng2.template">
<body>

<trans-unit id="67b19d1fe5d146b03b0b23cdd6fe54e35eec6e31" ↵
datatype="html">
<source>Passengers</source>
<target/>
<note priority="1" from="description">Beschreibung des ↵
Feldes</note>
<note priority="1" from="meaning">Spezielle Bedeutung</note>
</trans-unit>

<trans-unit id="eigene.id" datatype="html">
<source>Children</source>
<target/>
</trans-unit>

<trans-unit id="a82c8a41d9c47205f1193a6cbadc43b9091e5094" ↵
datatype="html">
...

</body>
</file>
</xliff>

Jede Markierung erhält ihren eigenen Bereich im XML. Der Tag <source> umfasst die ursprüngliche Fassung, die sich über <target> übersetzen lässt. Mit <note> können Nutzer die Angaben der Beschreibung und Bedeutung wiedergeben. Angaben mit @@ sind im HTML in XML an die Stelle der ID zu setzen (<trans-unit id="eigene.id" datatype="html">).

Die Einträge lassen sich in dem Zustand bereits übersetzen. Dazu müssen die Entwickler die Datei kopieren und im Projekt zum Beispiel mit dem Namen messages.de.xlf ablegen. Da es sich um XML handelt, ist es möglich, mit <![CDATA[]]> HTML-Elemente in die Übersetzung einzubringen:

<trans-unit id="67b19d1fe5d146b03b0b23cdd6fe54e35eec6e31" datatype="html">
<source>Passengers</source>
<target> <![CDATA[<i> Passagiere</i>]]></target>
<note priority="1" from="description">Beschreibung des Feldes</note>
<note priority="1" from="meaning">Spezielle Bedeutung</note>
</trans-unit>

Ein Nachteil automatisch generierter Übersetzungsdateien ist die Tatsache, dass Angular Übersetzungen nicht direkt erweitert. Übersetzer müssen die vorhandene Datei mit der neu generierten vergleichen und entsprechend anpassen.

Ein weiteres Problem der gezeigten Methode erschwert das: Da Angular eine Verbindung zwischen XML-Eintrag und HTML-Element erstellen muss, um später an der richtigen Stelle den Text zu ändern, benötigt es eine Referenz. Im XML ist diese über die ID (<trans-unit id="referenz" ...>) zu sehen. Wenn im HTML keine via @@ vergeben ist, generiert das Tool sie aus dem markierten Tag. Das hat zur Folge, dass Änderungen im Template an markierten Bereichen auch die ID ändert. Beim Migrieren der Änderungen in die eigene Übersetzung ist gesondert darauf zu achten.

Aus diesen Gründen ist eine solide Angabe einer Beschreibung und Bedeutung im Attribut sehr wichtig, sollten keine selbstdefinierten IDs zum Einsatz kommen.

In Fällen, in denen die Übersetzung von einer dynamischen Anzahl oder dem Wert des Inhalts abhängig ist, lässt sich mit spezieller Syntax arbeiten.

Die Tabelle, die im vorherigen Abschnitt ihren Tabellenkopf erhalten hat, zeigt nun Buchungseinträge durch das JavaScript-Objekt bookings an:

bookings = [
{
from: 'Graz',
to: 'Hamburg',
passengers: 3,
children: 1,
returnFlight: 'yes',
date: new Date('2017-04-26T12:00')
},
{
from: 'Graz',
to: 'Hamburg',
passengers: 6,
children: 4,
returnFlight: 'no',
date: new Date('2017-05-27T12:00')
}
]

Jeder Eintrag enthält Informationen über die Anzahl der Passagiere und wie viele davon Kinder sind. Je nach Buchung kann es sich um eine Person oder mehrere handeln. Das gilt auch für die Kinder. Für die Anwendung benötigen ihre Anbieter folglich unterschiedliche Übersetzungen:

<table class="table table-striped">
<tbody>
<tr *ngFor="let booking of bookings">
<td>{{booking.from}}</td>
<td>{{booking.to}}</td>
<td i18n="booking.passengers">{{booking.passengers}} ↵
{booking.passengers, plural, =1 {Passenger} other ↵
{Passengers}}</td>
<td i18n="booking.children">{booking.children, plural, =0 ↵
{No children} =1 {One child} =2 {Two children} other ↵
{More than two children}}</td>
<td i18n="booking.returnFlight">{booking.returnFlight, ↵
select, yes {Yes} no {No}}</td>
<td i18n="booking.date">{{booking.date | date: 'M/d/y'}}</td>
</tr>
</tbody>
</table>

Sobald ein Tag mit i18n markiert ist, können Entwickler durch eine spezielle Syntax unterschiedliche Formen für Ein- und Mehrzahl definieren. Der erste Parameter gibt die Anzahl an. Für ihn erwartet das Programm tatsächlich eine Zahl als Wert. Der zweite gibt die gewünschte Methode plural an. Sie vergleicht den Wert des ersten Parameters aus booking.children mit den Konfigurationen des letzten Parameters und gibt die entsprechende Übersetzung zurück.

Der letzte Parameter definiert die einzelnen Stadien. Die Liste lässt sich beliebig fortsetzen (=x). Alle nicht extra definierten Werte können Entwickler mit other abfangen. So kommen nun anhand der Anzahl der Kinder unterschiedliche Übersetzungen zurück:

<td i18n>
{booking.children, plural, =0 {No children} =1 {One child} =2 ↵
{Two children} other {More than two children}}
</td>

Die Übersetzung im XML ist identisch aufgebaut:

<trans-unit id="ce78c2334a8324384adb155b4c30e65cfd6978a2" datatype="html">
<target>
{booking.children, plural, =0 {Keine Kinder} =1 {Ein Kind} =2 {Zwei ↵
Kinder} other {Mehr als zwei Kinder}}
</target>
</trans-unit>

Der zweite Ansatz dynamischer Übersetzung nutzt die Methode select. Die Syntax bleibt in dem Fall gleich, jedoch nutzt man statt einer Zahl einen String, der von booking.returnFlight kommt:

{booking.returnFlight, select, yes {Yes} no {No}}

Auf dessen Basis ist es nun möglich, eine Alternative anzubieten:

<target>{booking.returnFlight, select, yes {Ja} no {Nein}}</target>

Damit die erstellte Übersetzung den Weg in die Applikation findet, ist die Datei zu integrieren. Das Nachladen von Übersetzungen, beispielsweise über ein Backend, ist nicht möglich während die Anwendung läuft. Je nach Projektaufbau geschieht die Integration entweder mittels Just-in-time- oder Ahead-of-time-Methode.

Angular selbst wird vor der eigentlichen Ausführung immer extra kompiliert beziehungsweise für die Nutzung optimiert. Bei Just in time (JIT) passiert das im Browser. Mit dem Ahead-of-time-Ansatz (AOT) übernimmt der Build-Prozess den Vorgang, bevor die Anwendung überhaupt ausgeliefert wurde. Im Browser entfällt nun dieser Teil, wodurch die Applikation schneller läuft, die Build-Zeit sich jedoch erhöht.

Über die Angular CLI können Entwickler die erstellte Übersetzungsdatei beim Bau der Applikation mit folgendem Befehl angeben:

ng build --aot --i18n-file src/locale/messages.de.xlf --locale de ↵
--i18n-format xlf

Der direkte Weg über den Angular-Compiler ist eine weitere Option:

"./node_modules/.bin/ngc" --i18nFile=./src/locale/messages.de.xlf ↵
--locale=de --i18nFormat=xlf

Ohne die Nutzung der Angular CLI, zum Beispiel durch eine eigene Webpack-Konfiguration, kann auch die JIT-Methode eine andere Sprache zur Verfügung stellen. Um das zu bewerkstelligen, sind dem Bootstrap der Anwendung drei Provider mitzugeben. Sie werden nicht wie sonst üblich im Root-Modul, sondern als eigene Parameter der Methode bootstrapModule übergeben. Angular kann so vor dem Start die Templates übersetzen.

Der Provider TRANSLATIONS hält den Inhalt der Übersetzungsdatei als String vor. Hier lässt sich kein externer Pfad angeben. Das gezeigte Beispiel funktioniert, da Webpack durch require angewiesen ist, den angegeben Pfad durch die dahinterliegende Datei zu ersetzen:

import { LOCALE_ID, TRANSLATIONS_FORMAT, TRANSLATIONS } from ↵
'@angular/core';
const options: any = {
providers: [
{provide: TRANSLATIONS, useValue: require('./locale ↵
/messages.de.xlf')},
{provide: TRANSLATIONS_FORMAT, useValue: 'xlf'},
{provide: LOCALE_ID, useValue: 'de'}
]
};
platformBrowserDynamic().bootstrapModule(AppModule, options);

Die Performance der Applikation steht an erster Stelle, weshalb der Angular-Compiler viel Arbeit abnimmt, bevor die Anwendung läuft. Mit dem HTML-Attribut i18n markierte Elemente lassen sich über das Tool ng-xi18n oder über die Angular CLI in eine XML-Datei extrahieren und übersetzen. Die Datei lässt sich später als Teil der Applikation integrieren.

Was man hier eventuell neben der Option, Übersetzungen extern zu laden, vermisst, ist die Möglichkeit, mehrere Sprachen dynamisch zur Laufzeit auszuwählen. Aufgrund des Aufbaus ist das im Angular-Kontext jedoch nicht machbar. Entwickler können allerdings pro Sprache eine eigene Applikation bauen und ausliefern. Ein Sprachwechsel hat immer zur Folge, dass die gesamte Anwendung neu zu laden ist. Es kommt also in erster Linie auf das Ziel der eigenen Anwendung an, um zu bestimmen, ob der Ansatz der richtige ist oder nicht.

Daniel Schwab
arbeitet bei Infonova GmbH als Frontend Architekt. Dort beschäftigt er sich mit der Konzeption und Entwicklung von webbasierten Anwendungen sowie dessen Integration im Enterprise Umfeld. Als Co-Autor im aktuellen Buch [3] „Angular: Plattform für moderne Webanwendungen“ gilt sein Augenmerk insbesondere der neuen Version des populären Google Frameworks.
(jul [4])


URL dieses Artikels:
https://www.heise.de/-3703621

Links in diesem Artikel:
[1] https://www.heise.de/hintergrund/Tipps-und-Tricks-fuer-AngularJS-Teil-1-Internationalisierung-2516976.html
[2] https://en.wikipedia.org/wiki/XLIFF
[3] http://angular.at
[4] mailto:jul@heise.de