Isolierung Locale-spezifischer Daten Locale-spezifische Daten müssen an die Sprach- und Landeskonventionen des jeweiligen Endbenutzers angepasst werden. Der in einer Benutzerschnittstelle anzuzeigende Text ist das wohl offensichtlichste Beispiel derart Locale-spezifischer Daten. Thema dieses Unterkapitels ist die Erzeugung und das Laden von ResourceBundle-Objekten bzw. der Zugriff auf derartige Objekte, mit deren Hilfe Locale-spezifische Daten verwaltet werden können. Die Klasse ResourceBundle Aus konzeptioneller Sicht ist jedes ResourceBundle-Objekt eine Menge von Sub-klassen, die zueinander in Beziehung stehen und die denselben Basisnamen aufweisen. Die folgende Aufzählung illustriert die Verwendung dieser Klassen. Basisname der Klasse, die Beschriftungen von Buttons verwaltet, ist ButtonLabel. Die auf den Basisnamen folgenden Zeichen repräsentieren den Sprach- und den Landes-Code bzw. eine Variante einer Localen. ButtonLabel_de_DE entspricht also der Localen, die durch den Sprach-Code für Deutsch (de) und durch den Landes-Code für Deutschland (DE) spezifiziert wird. Um ein geeignetes ResourceBundle-Objekt auszuwählen, wird die Methode ResourceBundle.getBundle aufgerufen. Im folgenden Beispiel wird das ResourceBundle-Objekt von ButtonLabel für die Locale ausgewählt, die der Sprache Deutsch, dem Land Deutschland und der Plattform UNIX entspricht. Locale currentLocale = new Locale("de", "DE", "UNIX"); ResourceBundle introLabels = ResourceBundle.getBundle("ButtonLabel", currentLocale); Wenn eine ResourceBundle-Klasse für eine ausgewählte Locale nicht existiert, versucht die Methode getBundle, die bestmögliche Entsprechung zu laden. Wenn also bspw. keine Klasse für die Kombination ButtonLabel_de_DE_UNIX existiert, sucht die Methode getBundle in folgender Reihenfolge nach Alternativen: ButtonLabel_de_DE_UNIX ButtonLabel_de_DE ButtonLabel_de ButtonLabel Findet die getBundle-Methode in dieser Reihenfolge keine Entsprechung, so wird eine weitere Suche in derselben Art und Weise in der Standard-Localen durchgeführt. Schlägt auch dies fehl, so wird eine Fehlermeldung ausgelöst (MissingResourceException). Es ist empfehlenswert, stets eine Basisklasse ohne weitere Suffixe anzugeben, wie im Beispiel ButtonLabel. In diesem Fall ist eine Fehlersituation leicht zu vermeiden. Die abstrakte Klasse ResourceBundle beinhaltet zwei Subklassen: ListResourceBundle und PropertyResourceBundle. Die auszuwählende Subklasse hängt zum einen vom Datentyp und zum anderen von der Art, in der sie lokalisiert werden soll, ab. Ein PropertyResourceBundle-Objekt greift auf ein oder mehrere Properties Files zu. Übersetzbare Texte sollten daher immer in Properties Files gespeichert werden. Da diese ausschließlich aus Text bestehen und nicht Teil des Java-Quellcodes sind, können sie in beliebigen Texteditoren von Übersetzern erzeugt werden. Hierzu ist keinerlei Programmiererfahrung notwendig. Properties Files können ausschließlich aus Werten für String-Objekte bestehen. Müssen hingegen andere Datentypen gespeichert werden, so sind ListResourceBundle-Objekte zu verwenden. Der Zugriff von PropertyResourceBundle-Objekten auf Properties Files erfolgt analog zum Zugriff durch ResourceBundle-Objekte. Die Klasse ListResourceBundle verwaltet Ressourcen in Form einer Liste. Jedes ListResourceBundle-Objekt greift auf eine .class-Datei zu. In Erweiterung zu ResourceBundle-Objekten kann in einem ListResourceBundle-Objekt jedes Locale-spezifische Objekt gespeichert werden. Um eine zusätzliche Locale zu unterstützen, muss eine neue Quelldatei erzeugt und in eine .class-Datei kompiliert werden. Da aber Übersetzer üblicherweise keine Programmierer sind, sollten Textobjekte, die eine Übersetzung erfordern, nicht in einem ListResourceBundle-Objekt verwaltet werden. Die Klasse ResourceBundle ist extrem flexibel. Wenn bspw. zunächst ein Locale-spezifischer Text in ein ListResourceBundle-Objekt geladen wurde, anschließend aber entschieden wurde, ein PropertyResourceBundle-Objekt zu verwenden, so ist die Auswirkung auf den Code minimal. Der folgende Aufruf von getBundle lädt bspw. ein ResourceBundle-Objekt für eine geeignete Locale, in der das ButtonLabel-Objekt von einer .class-Datei oder von einem Properties File verwaltet wird: ResourceBundle introLabels = ResourceBundle.getBundle("ButtonLabel", currentLocale); ResourceBundle-Objekte enthalten eine Liste von (Schlüssel-Wert)-Paaren. Ein Schlüssel wird immer als Zeichenkette angegeben, dessen Wert anschließend aus dem ResourceBundle-Objekt geladen wird. Der Wert ist das Locale-spezifische Objekt. Im folgenden Beispiel werden die Schlüssel OkSchluessel und AbbrechenSchluessel verwendet: class ButtonLabel_de extends ListResourceBundle { // Deutsche Version public Object[][] ladeInhalte() { return inhalte; } static final Object[][] inhalte= { {"OkSchluessel", "OK"}, {"AbbrechenSchluessel", "Cancel"}, } } Um die Zeichenkette OK aus dem ResourceBundle-Objekt zu laden, muss der geeignete Schlüssel angegeben werden, wenn getString aufgerufen wird: String okLabel = ButtonLabel.getString("OkSchluessel"); Es sollte darauf hingewiesen werden, dass das vorangehende Beispiel stark vereinfacht ist, da die String-Werte hart im Quellcode verdrahtet sind. Dies ist allerdings keine gute Programmierpraxis, da bspw. Übersetzer mit Properties Files arbeiten, die getrennt vom Quellcode sind. Ein Properties File beinhaltet stets Paare aus Schlüssel und Wert. Ein Beispiel hierfür ist im Folgenden beschrieben: OkSchluessel = OK AbbrechenSchluessel= Abbrechen Vorbereitung von ResourceBundle-Objekten Wird eine Anwendung entwickelt, die über eine Benutzeroberfläche verfügt, so sind meist eine Vielzahl von Locale-spezifischer Objekte enthalten. Zu Beginn sollte daher der Programm-Code auf Objekte überprüft werden, die mit der Locale variieren. Diese Objekte können bspw. Instanzen folgender Klassen sein: String, Component, Graphics, Image, Color oder AudioClip. Diese Liste beinhaltet keine Objekte, die Zahlen, Datumsangaben oder Zeiten repräsentieren. Der Grund hierfür liegt darin, dass das Format dieser Daten zwar mit der Locale variiert, nicht aber die Objekte selbst. Anstelle einer Isolierung dieser Objekte in einem ResourceBundle-Objekt findet eine Formatierung durch spezielle Locale-sensitive Klassen statt. Dies wird im Folgenden detailliert erklärt. Im Allgemeinen werden die Objekte, die in einem ResourceBundle-Objekt gespeichert sind, vorab definiert; sie sind in einer fertig gestellten Anwendung (auch zur Laufzeit) nicht veränderbar. Es versteht sich aber von selbst, dass ein String-Objekt, das von einem Endbenutzer in einem Textfeld eingegeben werden soll, nicht in einem ResourceBundle-Objekt isoliert werden sollte, da eine derartige Zeichenkette von Tag zu Tag variieren kann. Die Zeichenkette ist daher eher für die jeweilige Programmausführung als für die Locale, in der das Programm läuft, spezifisch. Üblicherweise sind die meisten Objekte, die in einem ResourceBundle-Objekt isoliert werden, String-Objekte. Nicht alle String-Objekte sind allerdings Locale-spezifisch. Wenn ein String bspw. niemals einem Endbenutzer präsentiert wird, muss er auch nicht internationalisiert werden. Es wird erkennbar, dass die Entscheidung, welche String-Objekte zu internationalisieren sind, nicht immer eindeutig ist. Ein Beispiel hierfür sind Log-Dateien, die ein Endbenutzer normalerweise nicht einsieht. Das Problem liegt hierbei in den vielfältigen Auslegungsmöglichkeiten des Wortes normalerweise. ResourceBundle-Objekte können organisiert werden, indem jedes mit einer anderen Kategorie von Objekten geladen wird. So können bspw. alle Beschriftungen von Buttons in ein ResourceBundle-Objekt ButtonLabelsBundle geladen werden. Aus der Kategorisierung ergeben sich die folgenden Vorteile: Der Code ist einfacher lesbar und damit auch einfach zu warten. Das Einladen eines Objekts aus einem ResourceBundle-Objekt ist schneller, wenn das ResourceBundle-Objekt lediglich eine kleine Anzahl von Objekten enthält. Übersetzer können einfacher mit kleineren Properties Files arbeiten. Die meisten Locale-spezifischen Daten bestehen aus String-Objekten. Wenn diese Objekte übersetzt werden müssen, so können sie in einem ResourceBundle-Objekt gespeichert werden, das auf Properties Files zurückgreift. Übersetzer können dann zusätzliche Sprachen anbieten, indem neue Properties Files generiert werden. ResourceBundle-Objekte und Properties Files Anhand dieses Abschnitts wird ein Java-Programm entwickelt, das die erklärten Konzepte verdeutlicht. Hierzu sind die folgenden Schritte auszuführen: Erzeugung eines Standard-Properties-Files Es sollte stets ein Standard-Properties-File generiert werden. Der Name der Datei muss hierzu mit dem Basisnamen des ResourceBundle-Objekts beginnen und mit dem Suffix .properties enden. In diesem Beispiel ist der Basisname ButtonsBundle. Das Properties File muss daher ButtonsBundle.properties heißen. Inhalt der Datei sind die folgenden Zeilen: # Dies ist das Standard-ButtonsBundle.properties File b1 = OK b2 = Abbrechen b3 = Ende Es ist zu beachten, dass Kommentarzeilen immer mit dem Doppelkreuz ( #) beginnen. Die anderen Zeilen beinhalten die Paare aus Schlüssel und dazugehörigem Wert. Es ist allerdings darauf zu achten, den Schlüssel nach der Definition nicht weiter zu verändern, da dieser meist vom Quellcode referenziert wird. Die Werte hingegen werden von Übersetzern verändert, wenn neue Properties Files für weitere Sprachen geschaffen werden. Erzeugung weiterer Properties Files Um eine zusätzliche Locale zu unterstützen, müssen neue Properties Files angelegt werden, die die übersetzten Werte beinhalten. Änderungen am Source-Code sind dazu nicht notwendig, da das Programm auf die Schlüssel, nicht aber auf die Werte zugreift. Soll bspw. die englische Sprache unterstützt werden, so würden die Werte aus der Datei ButtonsBundle.properties übersetzt und in der Datei ButtonsBundle_en_US.properties gespeichert werden. Hierzu ist zu bemerken, dass der Name der Datei aus dem Basisnamen und dem Suffix .properties besteht, zusätzlich aber aus dem Sprach- und dem Landes-Code (hierzu siehe Tab. 6-1 und Tab. 6-2). Der Inhalt der Datei ButtonsBundle_en_US.properties sieht dann folgendermaßen aus: # Dies ist die Datei ButtonsBundle_en_US.properties b1 = OK b2 = Cancel b3 = End Spezifikation der Localen Das Beispielprogramm erzeugt folgendes Locale-Objekt: Locale[] unterstuetzteLocale = { new Locale("de","DE"), new Locale("en","US") } Locale derzeitigeLocale = new Locale("de","DE"); Die Aufrufe der Locale-Konstruktoren spezifizieren jeweils Sprach- und Landes-Code. Diese Codes stimmen mit den Properties Files überein, die in den ersten beiden Schritten erzeugt wurden. Erzeugung des ResourceBundle-Objekts In diesem Schritt wird erkennbar, wie die Locale, die Properties Files und das ResourceBundle-Objekt zueinander in Beziehung stehen. Um ein ResourceBundle-Objekt zu erzeugen, muss die getBundle-Methode aufgerufen werden, die den Basisnamen und die Locale angibt: ResourceBundle labels = ResourceBundle.getBundle("ButtonsBundle",derzeitigeLocal e); Die Methode getBundle überprüft zuerst, ob eine .class-Datei existiert, die dem Basisnamen entspricht. Ist dies nicht der Fall, so wird nach Properties Files gesucht. In diesem Beispiel findet getBundle daher die entsprechenden Properties Files. Nach erfolgreicher Lokalisierung gibt getBundle ein PropertyResourceBundle-Objekt zurück, das mit den Paaren aus Schlüssel und Wert aus dem Properties File geladen wurde. Wenn kein Properties File für eine Locale existiert, so sucht die Methode getBundle nach einem Properties File, das dem gewünschten am nächsten kommt. Dieses Vorgehen wurde bereits beschrieben. Einladen des lokalisierten Textes Um die übersetzten Werte aus dem ResourceBundle-Objekt abzufragen, muss die getString-Methode verwendet werden: String wert = buttons.getString(schluessel); Der String, der als Ergebnis von getString zurückgeliefert wird, entspricht dem als Argument angegebenen Schlüssel. Der String ist dann auch in der geeigneten Sprache, gesetzt den Fall, dass ein Properties File für die angegebene Locale existiert. Da sich die Schlüssel nicht verändern, muss auch der Aufruf von getString niemals verändert werden. Iteration über alle Schlüssel Wenn die Werte aller Schlüssel eines ResourceBundle-Objekts geladen werden sollen, muss die Methode getKeys verwendet werden. Diese Methode liefert eine Aufzählung (Enumeration-Objekt) aller Schlüssel des ResourceBundle-Objekts als Ergebnis zurück. Anschließend kann über das Enumeration-Objekt iteriert werden und jeder Wert mit der getString-Methode geladen werden. Das folgende Programmsegment illustriert dieses Vorgehen: ResourceBundle buttons = ResourceBundle.getBundle("ButtonsBundle",derzeitigeLocal e); Enumeration buendleSchluessel = buttons.getKeys(); while (buendleSchluessel.hasMoreElements()) { String schluessel = (String)buendleSchluessel.nextElement(); String wert = buttons.getString(schluessel); System.out.println("Schluessel = " + schluessel + ", " + "Wert = " + wert); } Ausführung des Programms Setzt man das Programm zusammen und führt es aus, so ergibt sich die folgende Ausgabe. Die erste Gruppe der Zeilen zeigt die Werte, die getString für verschiedene Locale-Objekte zurückliefert. Die zweite Zeilengruppe zeigt die letzten drei Zeilen an, wenn mit der getKeys-Methode über die Schlüssel iteriert wird. Locale = de_DE, Schluessel = b2, Wert = Abbrechen Locale = en_US, Schluessel = b2, Wert = Cancel Schluessel = b3, Wert = Ende Schluessel = b2, Wert = Abbrechen Schluessel = b1, Wert = OK Verwendung von ListResourceBundle-Objekten In diesem Abschnitt wird anhand eines Beispiels schrittweise die Verwendung des ListResourceBundle-Objekts dargestellt. Erzeugung der ListResourceBundle-Subklassen Ein ListResourceBundle-Objekt greift grundsätzlich auf eine .class-Datei zu. Im ersten Schritt muss daher eine derartige Datei für jede unterstützte Locale generiert werden. Im Beispiel ist der Basisname des ListResourceBundle-Objekts AdressenBundle. Wie im vorangegangenen Beispiel werden zwei Locale-Objekte unterstützt, für die die Dateien AdressenBundle_de_DE.class und AdressenBundle_en_US.class erzeugt werden müssen. Die Klasse AdressenBundle ist im folgenden Code definiert. Wie bei Properties Files besteht auch der Klassenname aus dem Basisnamen und den Sprach- und Landes-Codes. Innerhalb der Klasse wird eine zweidimensionale Liste mit den Paaren aus Schlüssel und Wert initialisiert. Als Schlüssel werden in diesem Beispiel Name, Adresse und Alter verwendet. Die Schlüssel müssen als String-Objekte erzeugt werden und in jeder Klasse der Menge AdressenBundle identisch sein. Die Werte können einen beliebigen Objekttyp haben. import java.util.*; public class AdressenBundle_de_DE extends ListResourceBundle { public Object[][] ladeInhalte() { return inhalte; } private Object[][] inhalte= { {"Name", new String ("Fischer")},{"Adresse", new String("KOM - TU Darmstadt")},{"Alter", new Integer(29)}, } } Spezifikation der Localen Die Spezifikation der Localen erfolgt analog zum vorangegangenen Beispiel: Locale[] unterstuetzteLocales = { new Locale("en","US"), new Locale("de","DE") }; Jede Locale entspricht einer der AdressenBundle-Klassen. Die deutsche Locale, die mit de und DE angegeben wird, entspricht daher der Datei AdressenBundle_de_DE.class. Erzeugung des ResourceBundle-Objekts Auch die Generierung des ListResourceBundle-Objekts erfolgt analog zum vorangegangenen Beispiel: ResourceBundle stats = ResourceBundle.getBundle("AdressenBundle",derzeitigeLoca le); Laden der internationalisierten Objekte Nachdem ein ListResourceBundle-Objekt für die Locale erzeugt wurde, können Objekte mittels der Schlüssel eingeladen werden. Mittels der folgenden Programmzeile kann das Alter geladen werden, indem getObject mit dem Schlüsselparameter Alter aufgerufen wird. Es ist darauf zu achten, dass getObject ein Objekt zurückgibt, weshalb eine Typumwandlung (Casting) auf Integer erfolgen muss: Integer alter = (Integer)adressen.getObject("Alter");