Internationalisierung

Alle textuellen Ausgaben einer Anwendung müssen prinzipiell mehrsprachig erfolgen können. Hierfür wird Internationalisierung über Resource-Keys mit korrespondierenden Übersetzungen in Messages-Properties-Dateien eingesetzt.

Deklaration von Resource-Keys

Ein Resource-Key wird im Code in einer I18NConstants-Klasse (pro Paket) definiert. Ein Template für eine solche I18NConstants-Dateil sieht folgendermaßen aus:

/*
 * @(#) I18NConstants.java
 *
 * Copyright (c) 2016 Business Operation Systems AG. All Rights Reserved.
 */
package your.package;

import com.top_logic.basic.util.ResKey;
import com.top_logic.layout.I18NConstantsBase;
import com.top_logic.layout.ResPrefix;

/**
 * Internationalization constants for this package.
 *
 * @see ResPrefix
 * @see ResKey
 *
 * @version $Revision: $ $Author: $
 */
@SuppressWarnings("javadoc")
public class I18NConstants extends I18NConstantsBase {

        // Define your keys here...
        public static ResKey MY_KEY;

        static {
                initConstants(I18NConstants.class);
        }
}

Die Keys werden als Pseudo-Konstanten vom Typ ResKey (ohne den final-Modifier) deklariert. Die Initialisierung erfolgt über die Magie initConstants(...) zum Ladezeitpunkt der Klasse.

Deklaration von Übersetzungen

Übersetzungen für Keys werden in Resource-Bundles, die Übersetzungen für jede unterstützte Sprache enthalten, hinterlegt. Jedes Modul deklariert die von ihm hinzugefügten Bundles über eine Konfigurations-Sektion:

<config service-class="com.top_logic.basic.util.ResourcesModule">
        <instance error-on-missing-key="true">
                <static-bundles>
                        <bundles>
                                <bundle name="MyMessages" />
                        </bundles>
                </static-bundles>
        </instance>
</config>

Im Verzeichnis /webapp/WEB-INF/conf/resources/ des entsprechenden Module werden dann für jede Sprache (z.B. de und en) Übersetzungsdateien hinterlegt:

my.module/webapp/WEB-INF/conf/resources:
   MyMessages_de.properties
   MyMessages_en.properties

Für den oben deklarierten Key werden dann in

my.module/webapp/WEB-INF/conf/resources/MyMessages_de.properties:

class.your.package.I18NConstants.MY_KEY = Eine deutsche Übersetzung.

eine deutsche und in

my.module/webapp/WEB-INF/conf/resources/MyMessages_en.properties:

class.your.package.I18NConstants.MY_KEY = An Englisch translation.

eine englische Übersetzung hinterlegt.

Aus einer Key-Konstanten MY_KEY mit vollqualifiziertem Namen your.package.I18NConstants.MY_KEY wird durch Voranstellen von class. der Schlüssel class.your.package.I18NConstants.MY_KEY zu dem eine entsprechende Übersetzung hinterlegt werden kann. Der vollqualifizierte Name einer Konstanten kann in Eclipse durch Öffnen des Kontext-Menüs über der Deklaration und wählen des Menüpunkts "Copy qualified name" besonders einfach kopiert und in die entsprechende Internationalisierungsdatei übertragen werden.

Zugriff auf Übersetzungen im Code

Die Klasse Resources stellt den Zugriff auf Übersetzungen her. Im Rendering sind die passenden Resources für den aktuellen Nutzer über DisplayContext.getResources() erreichbar. An Stellen, an denen kein DisplayContext verfügbar ist, kann über Resources.getInstance() auf die passenden Ressourcen zugegriffen werden.

Das Nachschlagen einer Übersetzung geschieht über die API getString(ResKey):

void render(DisplayContext context, TagWriter out) {
  ResKey key = I18NConstants.MY_KEY;
  String text = context.getResources.getString(key);
  out.writeText(text);
}

Obiges Beispiel druckt "Eine deutsche Übersetzung." für einen deutschen Nutzer und "An Englisch translation." für einen englischen.

Übersetzungen mit dynamischen Argumenten

Häufig kann es vorkommen, dass in den darzustellenden Text dynamische Inhalten (Zahlen, Datumsangaben, o.ä.) eingebettet werden müssen.

Die hinterlegte Übersetzund darf daher Platzhalter nach der Spezifikation von java.text.MessageFormat enthalten. Die Dynamischen Inhalten können dann zusammen mit einem Basis-Resource-Key zu einem Message-Key kombiniert und an jede API weitergereicht werden, die mit ResKey umgehen kann:

ResKey titleKey = ResKey.message(I18NConstants.MY_MESSAGE__DATE, new Date());

new MyDisplayControl(titleKey).write(...);

Wenn die Übersetzung direkt an der Stelle durchgeführt wird, an der die Argumente verfügbar sind, können diese auch direkt an die Resources weitergereicht werden:

void render(DisplayContext context, TagWriter out) {
  out.writeText(context.getResources.getMessage(I18NConstants.MY_MESSAGE__DATE, new Date()));
}

Ist gleichbedeutend mit:

void render(DisplayContext context, TagWriter out) {
  out.writeText(context.getResources.getString(ResKey.message(I18NConstants.MY_MESSAGE__DATE, new Date())));
}

Format von Übersetzungen mit Argumenten

In der Übersetzung wird der dynamische Wert (hier das aktuelle Datum) an Stelle des Plazhalters {0} in die Übersetzung eingebettet:

my.module/webapp/WEB-INF/conf/resources/MyMessages_de.properties:

class.your.package.I18NConstants.MY_MESSAGE__DATE = Version vom {0}.

Details, wie Formattierungen in diese Platzhalter eingebettet werden können finden sich in der API-Dokumentation zu ​MessageFormat. Hierüber lassen sich auch komplexe Formattierungen erreichen, die bei Zahlenausgabe eine korrekte Singular/Plural-Form von sich auf die Zahl beziehenden Wörtern erreichen.

Konvention für Resource-Keys mit Argumenten

Konstantennamen für Übersetzungsschlüssel, welche dynamische Anteile enthalten, müssen die Argumente nach einem doppelten Unterstrich aufzählen (für jedes Argument ein einzelnes Wort, mehrere Argumente jeweils durch Unterstrich getrennt).

Keys mit drei Argumenten könnten beispielsweise folgendermaßen heißen:

   public static ResKey MY_KEY__ARG1_ARG2_ARG3;
   public static ResKey ERROR_MESSAGE__CODE_DATE_SEVERITY;

Übersetzungen mit eingebautem Fallback

Bei einer Übersetzung kann es sinnvoll sein, einen generischen Text zu hinterlegen, der in manchen Anwendungsfällen spezialisiert werden kann. Beispielsweise könnte für eine Anlage-Komponente generisch ein Titel "Neues Element anlegen" hinterlegt sein, so dass beim Einbinden der Komponente eine nette Default-Ansicht erreicht wird. Für konkrete Anwendungsfälle soll sich diese Übersetzung aber zu "Neues Projekt anlegen" oder "Meilenstein hinzufügen" spezialisieren lassen.

Hierfür lassen sich Resource-Keys zu Fallback-Chains zusammenfügen:

ResKey defaultKey = I18NConstants.CREATE_ELEMENT_TITLE;
ResKey specializedKey = component.getResPrefix().key("title");

ResKey titleKey = ResKey.fallback(specializedKey, defaultKey);

new MyDisplayControl(titleKey).write(...);

Theoretisch lassen sich so mehrstufige Fallback-Keys konstruieren, die dann an jede API weitergereicht werden können, die mit ResKey umgehen kann.

Generierter Text anstatt von Übersetzungen

Ist eine API auf Internationalisierung vorbereitet z.B.

class MyDisplay {
  void setLabel(ResKey labelKey) {...}
}

kann MyDisplay einfach mit Resource-Keys verwendet werden:

display.setLabel(I18NConstants.WINDOW_TITLE);

Allerdings kann es unter Umständen auch notwendig sein, eine solche API mit einem fertigen Text (der beispielsweise aus einer Nutzereingabe stammt) zu bedienen. Auch dies ist möglich, indem der literale Text in einen ResKey verpackt wird. Ein solcher spezieller Key wird bei der Auflösung wieder zu dem Text, mit dem er generiert wurde:

String userInput = ...;

display.setLabel(ResKey.text(userInput));

Resource Prefixe von Komponenten

Komponenten die eine Toolbar haben, müssen einen Resource Prefix in ihrer XML-Datei definieren. (Siehe: LayoutComponent.Config.getResPrefix()) Dieser soll dem in #18227 definiertem Schema entsprechen:

  • mit dem Prefix layouts. beginnen
  • nach dem konstanten Prefix aus einem Punkt-getrennten Pfad zu der Layout-XML-Datei bestehen, in der der Key definiert ist.
  • als letzten Teil den lokalen Namen der Komponente haben, an der der Key definiert ist:
    <component class="..."
       name="MyForm"
       page="/jsp/....jsp"
       resPrefix="layouts.demo.form.SomeFormDemo.MyForm."
    />