Teil 3: Kein Ressourcenkonflikt

Backend meets Frontend Reloaded: Drei Wege zur Implementierung von ResourceBundles

Sven Ruppert

© Shutterstock / HelenField & bkf (modifiziert)

Im letzten Teil der Serie haben wir uns damit beschäftigt, wie mittels I18NProvider innerhalb von Vaadin Flow der Zugriff auf ein ResouceBundle erfolgen kann. Im dritten Teil sehen wir uns nun an, wie man dies auf mehrere Sprachen ausweitet und im Zuge dessen eine mögliche Implementierung eigener ResourcenBudles durchführt.

ResourceBundle – seit JDK 1.1

Wenn man sich im JDK ein wenig umsieht, findet man die abstrakte Klasse ResourceBundle. Diese stellt die Basisimplementierung für den Umgang mit Schlüssel/Wert-Paaren bereit, die zum Beispiel schon in Swing-Anwendungen für die Mehrsprachlichkeit verwendet worden ist. Wie in den vorangegangenen Teilen dieser Tutorialreihe erwähnt, benötigen wir ein ResourceBundle, in dem die verschiedenen Übersetzungen für die unterstützten Sprachen abgelegt werden können. Die grundlegende Implementierung des ResourceBundle ist die abstrakte Klasse mit dem Namen ResourceBundle. Wir werden uns nun drei Wege ansehen, die in diesem Kontext gegangen werden können.

PropertyResourceBundle

Die am häufigsten verwendete Version ist die bereits dem JDK beiligende Implementierung, die Properties-Dateien verwendet. Hier können wir mit dem Einsatz sofort loslegen, nachdem wir die benötigten Properties-Dateien erstellt haben. Lediglich das zu verwendende Namens-Schema für die Dateien selbst ist in diesem Fall zu beachten, die ausführliche Beschreibung ist in der JavaDoc zu finden. Die Kurzversion stellt sich wie folgt dar:

Der Basisname der Datei muss bei allen Dateien sein, in unserem Fall vaadinapp. Für die deutsche Sprache bekommt dann die Datei zusätzlich ein _de angehängt. Um die englische Sprache zu unterstützen wird ein _en verwendet. Die daraus resultierenden Dateinamen sind dann in diesem Fall:

  • vaadinapp.properties
  • vaadinapp_de.properties
  • vaadinapp_en.properties

Wenn man anschließend noch den Unterschied zwischen Deutschland und der Schweiz sprachlich abbilden möchte wird das de-Bundle nochmals spezialisert:

  • vaadinapp_de_DE.properties
  • vaadinapp_de_CH.properties

Der entscheidende Punkt ist jedoch, dass die Properties-Dateien im Klassenpfad verfügbar sein müssen. Nicht immer ist es die Lösung, die angestrebt wird.

  public static final String RESOURCE_BUNDLE_NAME = "vaadinapp";

  private static final ResourceBundle RESOURCE_BUNDLE_EN = getBundle(RESOURCE_BUNDLE_NAME , ENGLISH);
  private static final ResourceBundle RESOURCE_BUNDLE_DE = getBundle(RESOURCE_BUNDLE_NAME , GERMAN);

ListResourceBundle

Die abstrakte Implementierung des ResouceBundles, über die wir als nächstes sprechen, befindet sich in der Klasse ListResourceBundle. Auch in diesem Fall bekommen wir eine abstrakte Implementierung geliefert. Jeweils eine Implementierung oder Instanz der Implementierung ist für ein dediziertes Gebietsschema bereitzustellen. Zu implementieren ist immer die Methode protected Object [] [] getContents ().

Wie gerade erwähnt muss für jedes Gebietsschema, das wir zur Laufzeit unterstützen möchten, eine entsprechende Implementierung oder Instanz zur Verfügung stehen. In der Praxis sieht es unter Umständen wie folgt aus:

public class VaadinAppResource extends ListResourceBundle {
  @Override
  protected Object[][] getContents() {
    return new Object[][]{
        {"s1" , "value 01"} ,
        {"s2" , "value 02"}
    };
  }
}

Diese Implementierung, die wir oben gesehen haben, verwenden wir als Standardressource. Um jetzt eine englische und eine deutsche Variante zu erhalten, müssen wir zwei weitere Klassen implementieren.

public class VaadinAppResource_de extends ListResourceBundle {

  @Override
  protected Object[][] getContents() {
    return new Object[][]{
        {"s1" , "value 01 - de"} ,
        {"s2" , "value 02 - de"}
    };
  }
}

public class VaadinAppResource_en extends ListResourceBundle {

  @Override
  protected Object[][] getContents() {
    return new Object[][]{
        {"s1" , "value 01 - en"} ,
        {"s2" , "value 02 - en"}
    };
  }
}

Auf diese Weise erhält jedes dedizierte Gebietsschema eine eigene Implementierung. Der jeweilige Name der Klasse muss den gleichen Regeln entsprechen, die wir für Properties-Dateien angewendet haben.

Aber wie benutzt man das?

Die Verwendung dieses ResourceBundles basiert auf dem vollständig qualifizierten Namen der Hauptklasse, in unserem Fall der Klasse VaadinAppResource. Mit diesem Klassennamen und dem angeforderten Gebietsschema wird einem die statische Methode getBundle der Klasse ResourceBundle die Instanz des benutzerdefinierten ResourceBundles für das angeforderte Gebietsschema geben.

ResourceBundle.getBundle(VaadinAppResource.class.getName(), Locale.ENGLISH )

Wenn das angeforderte Gebietsschema nicht vorhanden ist, wird eine MissingResourceException erzeugt. Eine Sache, die möglicherweise nicht regelmäßig verwendet wird, ist die Verwendung des Standardgebietsschemas. Um das Standard-ResouceBundle zu erhalten, muss das Gebietsschema ROOT verwendet werden. Natürlich sollte die Implementierung auch getestet werden.

public class VaadinAppResourceTest {

  @Test
  @DisplayName("Locale English")
  void test001() {
    final ResourceBundle a = ResourceBundle.getBundle(VaadinAppResource.class.getName(), Locale.ENGLISH );
    Assertions.assertEquals("value 01 - en", a.getString("s1"));
  }

  @Test
  @DisplayName("Locale German")
  void test002() {
    final ResourceBundle a = ResourceBundle.getBundle(VaadinAppResource.class.getName(), Locale.GERMAN );
    Assertions.assertEquals("value 01 - de", a.getString("s1"));
  }

  @Test
  @DisplayName("Locale Default")
  void test003() {
    final ResourceBundle a = ResourceBundle.getBundle(VaadinAppResource.class.getName() , Locale.ROOT);
    Assertions.assertEquals("value 01" , a.getString("s1"));
  }
}

Custom ResourceBundle

Die beiden vorherigen Möglichkeiten werden vom JDK selbst bereitgestellt. Der dritte Weg, den wir gehen könnten, basiert auf der Implementierung der abstrakten Klasse ResouceBundle. Übrigens gibt es kein Interface, wie zum Beispiel ResouceBundle, das für eigene Implementierungen verwendet werden kann. Damit ist man in gewisser Weise an die vorhandene partielle Implementierung gebunden. Selbstverständlich kann man auch über eine vollständig eigene Realisierung nachdenken. Die abstrakte Klasse ResouceBundle stellt zwei abstrakte Methoden bereit:

  • protected Object handleGetObject(String key)
  • public Enumeration getKeys()

Beginnen wir mit der Methode handleGetObject. Wie man hier sehen kann, ist das ResourceBundle ist nicht auf die Verwendung von Strings beschränkt. Dieser Mechanismus kann für jeden beliebigen Ausgabetyp verwendet werden. In diesem Beispiel allerdings verwenden wir es nur für Übersetzungen, die Methode ist der Übersetzungsvorgang selbst.

Um es möglichst einfach zu halten, basiert die Implementierung in diesem Beispiel auf der Klasse ConcurrentHashMap, die für uns den Schlüssel-/Wertspeicher darstellt. An dieser Stelle kan man kreativ werden und alles verwenden und einsetzen, was erforderlich ist. Hier können beispielsweise externe Dienste in Anspruch genommen werden oder eine Verbindung zu einem Persistenzspeicher hergestellt werden oder, oder, oder…

Zurück zu unserem einfachen Schlüssel-/Wert-Speicher: Die Implementierung dieser Methode ist lediglich ein Delegator für die Methode get von der Map selbst:

public class VaadinResourceBundle extends ResourceBundle {

  private  Map<String, String> values = new ConcurrentHashMap<>();

  @Override
  protected Object handleGetObject(String key) {
    return values.get(key);
  }
}

Die nächste zu implementierende Methode ist die Auflistung aller verfügbaren Schlüssel in dieser Instanz des ResourceBundles. In unserem Fall also lediglich der Schlüsselsatz der verwendeten Map.

public class VaadinResourceBundle extends ResourceBundle {

  private  Map<String, String> values = new ConcurrentHashMap<>();

  @Override
  protected Object handleGetObject(String key) {
    return values.get(key);
  }

  @Override
  public Enumeration getKeys() {
    return Collections.enumeration(values.keySet());
  }
}

Sicher, diese Implementierungen hängen von dem im Einsatz befindlichen Speicher ab. Allerdings ist die abzubildende Logik recht geradlinig. Interessanter ist vielmehr, wie zur Laufzeit die Instanz dieses ResourceBundles basierend auf einem angeforderten Gebietsschema erhalten werden kann. Zu beachten ist, dass der grundlegende Mechanismus auf der Anforderung einer Instanz für ein definiertes Gebietsschema basiert. Eine Instanz kann demnach nicht für mehr als ein Gebietsschema angegeben werden.

Wir sind un an einem Punkt angelangt, an dem wir technische Entscheidungen treffen müssen, die zu dem jeweiligen Projekt passen. Für dieses Beispiel habe ich mich für eine statische Factory-Methode entschieden. Natürlich kann man auch über die Verwendung von einem ServiceLocator, einer Dependency Injnection oder etwas anderem nachdenken. Hier sind der Phantasie keine Grenzen gesetzt.

Die statische Factory-Methode gibt mir für das angeforderte Gebietsschema die erstellte Instanz, die verwendet werden sollt. Dafür können unterschiedliche Strategien zum Einsatz kommen, wie mit angeforderten Gebietsschemas umzugehen ist, die nicht unterstützt werden, oder spezialisierten Gebietsschemata für verschiedene Sprachvariationen.

Ein anderes großes Thema sind Caching- und Lazy-Loading-Strategien, wenn das Erstellen und/oder halten von vollständigen ResouceBundles teuer ist. In diesem Beispiel werden wir allerdings nicht auf derartige Dinge eingehen, das werden wir zu einem späteren Zeitpunkt im Detail besprechen.

public class VaadinResourceBundle extends ResourceBundle {

  private  Map<String, String> values = new ConcurrentHashMap<>();

  // the technical decision on how to create a variant for a Locale
  public static ResourceBundle forLocale(Locale locale) {

    final VaadinResourceBundle resourceBundle = new VaadinResourceBundle();
    if (locale.equals(Locale.GERMAN)) {
      resourceBundle.values.put("btn.click-me" , "drück mich");
    }

    if (locale.equals(Locale.ENGLISH)) {
      resourceBundle.values.put("btn.click-me" , "click me");
    }
    return resourceBundle;
  }

  @Override
  protected Object handleGetObject(String key) {
    return values.get(key);
  }

  @Override
  public Enumeration getKeys() {
    return Collections.enumeration(values.keySet());
  }
}

Die Verwendung erfolgt dann innerhalb der I18NProvider-Implementierung, so wie wir es im letzten Teil gesehen haben.

  private static final ResourceBundle RESOURCE_BUNDLE_EN = VaadinResourceBundle.forLocale(ENGLISH);
  private static final ResourceBundle RESOURCE_BUNDLE_DE = VaadinResourceBundle.forLocale(GERMAN);

Fazit

Wir haben nun eine erste Implementierung eines eigenen ResourceBundles. Technisch sind wir in unserem Beispiel noch sehr einfach geblieben, für den skalierbaren Einsatz gibt es noch weitere Themen die von Interesse sein können. Diese werden wir zu einem späteren Zeitpunkt in dieser Serie aufgreifen. Im nächsten Teil werden wir uns ansehen, wie man dies nun verwenden kann, um Vaadin Flow zu erweitern.

Das Beispiel zum vorliegenden Serienteil ist wie immer auf GitHub zu finden. Wer Fragen und Anmerkungen hat, meldet sich am besten per Twitter an @SvenRuppert oder direkt per Mail an sven.ruppert@gmail.com

Happy Coding!

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: