• September 24, 2014

Lokalisierung dynamischer Inhalte

Die Lokalisierung von Textinhalten einer Anwendung ist ein häufiges Problem bei Anwendungen mit einer mehrsprachigen Nutzergruppe. Normalerweise werden alle statischen Inhalte, also im Grunde genommen alle „eingebauten" Anwendungstexte von einer Eigenschaftendatei mit Schlüsselwertpaaren bereitgestellt und von dort nach Bedarf abgerufen. 

Benutzergenerierte dynamische Inhalte stellen uns vor etwas komplexere Herausforderungen. Lokalisierte Werte werden normalerweise in einer Datenbank als Schlüsselwertpaare gespeichert. Wie diese Werte abgerufen werden, hängt von mehreren Faktoren ab, beispielsweise der Struktur und der Datenmenge.

Die folgende Lösung wird für den Nutzungsfall entwickelt, in dem ein benutzerdefinierter Inhalt durch eine hierarchische Objektstruktur definiert wird, die potentiell große Mengen unterschiedlicher Einheiten und unterschiedlicher Daten enthält. Weitere Datenstrukturen werden voraussichtlich zukünftig stärker sowohl durch neue Entitätstypen und neue lokalisierbare Textfelder in bestehenden Entitäten genutzt.

Ursprüngliche Inhalte und Übersetzungen

In einigen System wird nicht davon ausgegangen, dass sich die Anzahl der unterstützten Sprachen ändert. Zusätzlich könnten die gesamten Inhalte in alle verfügbaren Sprachen übersetzt werden. In solchen Situationen ist es sinnvoll, auch die ursprünglichen Inhalte mit der gleichen Datenstruktur wie die Übersetzungen zu speichern. Diese Art von Lösung wird häufig von einer API (Programmierschnittstelle) zwischen Inhalts- und Benutzerschnittstellenfeldern gehandhabt, und es entsteht ggf. unnötiger Overhead beim Abrufen von Inhalten.

Eine andere Herangehensweisen an dieses Problem wäre das Speichern der Standardsprachversionsinhalte in die originalen Datenstrukturfelder. Die Benutzerschnittstelle kann dann die notwendigen Daten direkt über die bereitgestellten Datenobjekte anzeigen, ohne eine spezifische API anzusprechen oder sogar ohne Kenntnisse darüber, welche Sprache aktuell verwendet wird.

Zur Übersetzung der Inhalte in beliebige weitere Sprachen bräuchten wir dann zwei Dinge. Erstens ein Übersetzungsobjekt, dessen Aufgabe es ist, von dem Datenspeicher abgerufene Inhalte in verschiedene Sprachen zu übersetzen, bevor die Daten an die Benutzerschnittstelle übermittelt werden. Darüber hinaus brauchen wir ein Wörterbuch - eine Sammlung von Übersetzungen aus der Standardsprache in eine andere Sprache.

Idealerweise sollte diese Lösung nicht anwendungsspezifisch und leicht erweiterbar sein. Dadurch entstehen einige Anforderungen an übersetzte Datenobjekte, da ein Übersetzer in der Lage sein muss, den Inhalte der Datenobjekte zu übersetzen, ohne genau zu wissen, welche Art von Inhalten er gerade übersetzt. Diese Lösung sollte auch die Erstellung, Wartung und Erweiterung der Übersetzung ohne Notwendigkeit großer Mengen an anwendungsspezifischem Code unterstützen.

Definieren von Entitäten für die Lokalisierungslösung

Die folgende Lösung für das oben beschriebene Problem wurde in Java umgesetzt. Ähnliche Muster können in jeder objektorientierten Sprache im Rahmen der Möglichkeiten der genannten Sprachen umgesetzt werden.

Wir beginnen damit, die Schnittstelle als lokalisierbar zu definieren. Alle Entitäten, die lokalisierbare Daten enthalten, müssen eine lokalisierbare Schnittstelle implementieren, damit das Übersetzerobjekt die Inhalte in der Entität übersetzen kann.

public interface Localizable {
    String getLocalizerKey();
    List<String> getLocalizedFields();
    String getLocalizedFieldValue(String field);
    setLocalizedFieldValue(String field, String value);
    void localize(Localizer localizer);
}

String getLocalizerKey() sorgt für einen eindeutigen String, der zur Identifizierung des Objekts verwendet wird.

String getLocalizedFields() sorgt für eine Liste von Namensfeldern, deren Inhalt lokalisiert werden kann.

String getLocalizedFieldValue(String field) ruft den Wert eines durch seinen Namen identifizierten Feldes ab.

void setLocalizedFieldValue(String field, String value) wird verwendet, den Wert eines lokalisierten Feldes einzustellen.

void localize(Localizer localizer) wird von einem Lokalisierobjekt für die tatsächliche Übersetzung verwendet.

Im Folgenden finden Sie ein Beispiel für ein lokalisierbares Objekt:

public class Page implements Localizable {
    private static final String NAME = "NAME";
    private static final String DESCRIPTION = "DESCRIPTION";
    private static final List<String> LOCALIZED_FIELDS = Arrays.asList(new String[] {NAME, DESCRIPTION});
....
    public String getLocalizerKey() {
        return LocalizerKeys.LOCALIZABLE_PREFIX_PAGE.getKey() + getId() + "_";
}
    public List<String> getLocalizedFields() {
        return PageUI.LOCALIZED_FIELDS;
    }
    public String getLocalizedFieldValue(String field) {
        if(NAME.equals(field)) {
            return getName();
        } else if(DESCRIPTION.equals(field)) {
            return getDescription();
        } else {
            return "";
        }
    }
    public void setLocalizedFieldValue(String field, String value) {
        if(NAME.equals(field)) {
            setName(value);
        } else if(DESCRIPTION.equals(field)) {
            setDescription(value);
        }
    }
....
}

Beachten Sie, dass die Definition der lokalisierbaren Felder vor dem Beginn der Objektdefinition erfolgt. Beachten Sie auch, dass die tatsächlichen Felder und die Getter und Setter in dem Beispiel aus Platzgründen weggelassen wurden. Wir präsentieren weiter unten auch die Umsetzung des Lokalisierungsverfahrens. 

Als nächstes definieren wir einen lokalisierten Wert, also im Grunde ein einfaches Schlüsselwertpaar, das mit bestimmten Übersetzungsobjekten verbunden ist. Ein Übersetzungsobjekt ist ein Wörterbuch, das alle Übersetzungen für eine Sprache und eine Objektstruktur enthält. Die Definition eines Übersetzungsobjekts wird hier nicht vorgestellt. Sie können jedoch davon ausgehen, dass es eine Identifizierungs-ID, eine Sammlung von LocalizedValue-Objekten und Verweise auf die übergeordnete Strukturebene enthält, die übersetzt werden soll.

public class LocalizedValue {
    private long translationId;
    private String localizerKey;
    private String localizedValue;
}

Und schließlich folgt das tatsächliche Übersetzerobjekt, der Localizer.

public class Localizer {
    private final Map<String, String> valueMap;
    public void localize(Localizable localizable) {
        for(String field : localizable.getLocalizedFields()) {
            String localizedValue = valueMap.get(localizable.getLocalizerKey() + field);
            if(localizedValue != null) {
                localizable.setLocalizedFieldValue(field, localizedValue);
            }
        }
    }
}

Bitte beachten Sie, dass Localizer über eine Liste von Schlüsselwertpaaren verfügt. Wenn eine Entität Localizer zur Lokalisierung seines Inhalts anfordert, fordert Localizer erst eine Liste aller Felder in dieser Entität an, die lokalisierbar sind. Dann durchsucht er die Schlüsselwertpaare nach dem Schlüssel, der durch die Kombination aus Objektschlüssel und Feldnamen entsteht. Und schließlich setzt er den Wert des genannten Feldes in der lokalisierten Entität auf den eines neuen lokalisierten Werts. All dies kann in einem einfachen Loop erfolgen, das die lokalisierbare Schnittstelle die Lokalisierung alle notwendigen Felder ohne Kenntnis des tatsächlichen Typs der lokalisierten Objekte erlaubt.

Und wie funktioniert der eigentliche Lokalisierungsvorgang?

Wie fügt sich das alles schließlich zu einer echten Umsetzung zusammen? Stellen wir uns vor, dass wir bereit sind, ein Seitenobjekt anzuzeigen, das seinerseits einige lokalisierbarer Unterentitäten enthält, und wir möchten den Inhalt übersetzen, bevor wir ihm dem Benutzer anzeigen. Zunächst einmal rufen wir das eigentliche Objekt aus dem Datenspeicher ab. Wenn wir feststellen, dass der Inhalt lokalisiert werden muss, erstellen wir ein Lokalisierungsobjekt und pflegen die Übersetzungen ein. Wir sehen uns später noch genauer an, wie diese effizient lokalisiert werden können.

Der nächste Schritt ist nun die eigentliche Lokalisierung. Dafür rufen wir das Lokalisierungsverfahren im Seitenobjekt auf und geben den befüllten Localizer als Parameter an.

public void localize(Localizer localizer) {
    localizer.localize(this);
    for(PageContent content : pageContents) {
        localizer.localize(content);
    }
}

Bitte beachten Sie, dass die Seitenentität weiß, dass es eine Sammlung von PageContent-Entitäten hält, also weist sie bei der Lokalisierung den Localizer an, auch die Übersetzung für diese untergeordneten Objekte durchzuführen. Auf diese Weise wird die gesamte Objekthierarchie lokalisiert, und wiederum muss der Localizer nicht wissen, was er eigentlich übersetzt.

Immer dann, wenn Inhalte in einer anderen als der Standardsprache angezeigt werden, wird jedes lokalisierbare Objekt durch ein Localizer-Objekt geführt, bevor es an die Benutzerschnittstelle weitergeleitet wird. Es versteht sich von selbst, dass diese Daten jetzt schreibgeschützt sind. Zur Bearbeitung müssen wir eine eigene Bearbeitungsschnittstelle implementieren.

Wie Sie sehen können, kann man diese Lösung besonders einfach expandieren, während die lokalisierbare Datenstruktur sich erweitert. Das Hinzufügen von Feldern zu bestehenden Objekten und ihre Lokalisierung ist ebenso leicht wie das Hinzufügen dieser neuen Felder zu der Liste der lokalisierbaren Felder und die Erweiterung der Implementierung von getLocalizedValue und setLocalizedValue, sodass diese die neuen Felder einschließen. Was völlig neue lokalisierbare Objekte angeht, reicht es aus, wenn sie die lokalisierbare Schnittstelle implementieren.

Leistung und Bearbeitung

Ein paar offene Fragen verbleiben jedoch. Die erste betrifft die Population des Localizer. Wie können wir die benötigten lokalisierten Werte abrufen, ohne zu viel Overhead zu verursachen?

Die Antwort hängt von der Größe und Struktur der lokalisierten Daten ab. Wenn der Wert der Felder nicht zu extensiv ist, dann reicht es vielleicht aus, alle Schlüsselwertpaare für eine Übersetzung abzurufen und einfach diese zu verwenden. Für größere Datenmengen könnten wir anwendungsspezifische Extensionen von LocalizedValue durch Hinzufügen von Tags und Referenzen vornehmen, auf die beim Abruf kleinerer Wertesubsets Bezug genommen werden kann.

In dem oben beschriebenen Beispiel mit Seiten, in denen Multiple PageContent-Objekte vorhanden sind, könnten wir das Feld „pageId" hinzufügen, das sich auf ein Paging für alleLocalizedValue-Instanzen bezieht, die entweder zu der genannten Seite oder dem Page Content-Objekt darin gehören. Dadurch wird es möglich, durch PageId alle benötigten LocalizedValue-Instanzen abzurufen, die zu der genannten Seite oder den Unterseiten gehören, ohne LocalizedValues abzurufen, die zu anderen Seiten gehören. Die gleiche Lösung kann nach Bedarf erweitert werden, um die aktuelle Datenstruktur und die Art und Weise darzustellen, wie Daten abgerufen werden.

Was die Erstellung, die Wartung und die Bearbeitung von Übersetzungen und LocalizedValues angeht, werde ich hier keine Komplettlösung präsentieren. Stattdessen stelle ich einige Hinweise sowie die tatsächliche Implementierung als kleine Übung für alle Interessierten bereit.

  1. Der einfachste Fall einer Benutzerschnittstelle zur Erstellung einer neuen Übersetzung und Beibehaltung aller LocalizedValues darin wäre einfach ein Textbereich mit Schlüsselwertpaaren, für die Schlüssel automatisch bereitgestellt werden.
  2. Bei anspruchsvolleren Lösungen kann und sollte die Benutzerschnittstelle zur Beibehaltung einzelnerLocalizedValues mit dem selben Code erfolgen, unabhängig davon, welche Objektwerte oder Werte aktuell bearbeitet werden.
  3. Das Erstellen der Benutzerschnittstellen zur Bearbeitung aller LocalizedValues bei einer Einzelübersetzung kann mit minimalem anwendungsspezifischen Code durch Verwendung herkömmlicher Localization-Dateien und Feldnamen für LocalizedValue-Überschriften erfolgen. Anwendungsspezifischer Code ist nur zur bequemen Organisation editierbarer LocalizedValues erforderlich, damit diese die aktuelle Datenstruktur wiederspiegeln und zur Bereitstellung von Überschriften für auf Objektklassen basierende Organisation.
  4. Wenn unsere LocalizedValue-Klasse Referenzen zu unterschiedlichen Objekten in unserer Objekthierarchie enthält (siehe das oben stehende pageID-Beispiel), sollte die Erstellung neuer LocalizedValue-Entitäten darauf ausgerichtet sein, Werte zu korrigieren. Eine Möglichkeit dafür ist das Hinzufügen von void setEntityRelations(LocalizerEntityRelationSetter relationSetter) zur lokalisierbaren Schnittstelle. Die Implementierung des LocalizerEntityRelationSetter verfügt über Implementierungen für jeden Entitätentyp zur Einstellung der richtigen Beziehungen. Auch hier kann ein anwendungsspezifischen Code auf einzelne Implementierungsklassen beschränkt werden.

Die oben beschriebene Lösung ist sicher nicht für alle Localization-Bedarfe umsetzbar. Es wurde jedoch in mindestens einem großen Projekt nachgewiesen, das sie mit der Anwendungs- und Datenstruktur leicht erweiterbar ist. Das Trennen von Übersetzungen und deren Verwaltung von der tatsächlichen Content-Erstellung reflektiert auch häufige Situationen im echten Leben, in denen die Übersetzungen häufig sehr viel später als der ursprüngliche Inhalt erstellt werden. Schließlich kreieren wir Anwendungen für den Einsatz in der echten Welt, also ist die Imitation der Arbeitsabläufe im echten Leben normalerweise sehr fruchtbar.

Weitere Posts lesen