<jsptutorial />

Internationalisierung


Grundlagen

Anwendungen und ganz besonders Webanwendungen richten sich heutzutage nur noch selten an ein rein nationales Publikum. Dabei sind nicht nur die Texte zu übersetzen und ggf. spezielle Grafiken zu erzeugen, sondern es sind darüber hinaus zahlreiche Besonderheiten zu beachten. Es gibt unterschiedliche Zeichensätze, und Zahlen-, Währungs- und Datums-Werte werden unterschiedlich formatiert. Wer sich von Java-Standalone-Anwendungen her schon mit Internationalisierung auskennt, kann die ersten beiden Abschnitte getrost überspringen. Da Internationalisierung aber vielfach auch für erfahrene Java-Entwicklerinnen erst mit Webanwendungen zum Thema wird, geben wir zunächst eine kurze Einführung in die grundlegenden Probleme und in die Konstrukte, die Java zur Bewältigung der Probleme anbietet.
Eines vorne weg: Internationalisierung ist eines der größten Probleme bei der Erstellung von Webanwendungen. Browser verhalten sich unterschiedlich, Spezifikationen stehen zueinander im Widerspruch, und dann macht es auch noch einen Unterschied, ob man Formulare per POST oder per GET versendet. Auf den ersten Blick wirkt das Thema harmlos. Java liefert schließlich eine Menge mit, um dieses Problem anzugehen. Leider ist es in der Praxis aber doch eine immer wiederkehrende Quelle zum Teil schwer nachstellbarer Bug-Reports.

Internationalisierung und Lokalisierung


Im Zusammenhang mit Internationalisierung ist auch die Lokalisierung von Bedeutung. Damit ist hier nicht das zunehmend an Bedeutung gewinnende Anbieten ortsbezogener Dienste gemeint, sondern die unterschiedliche Sprachausprägung innerhalb einer Sprachfamilie. Im Deutschen bspw. das deutsche Deutsch, das österreichische Deutsch, das v.a. einige besondere Wörter kennt, und das schweizerische Deutsch. Es reicht also vielfach nicht, eine Anwendung für ein Land zu übersetzen, sondern man muss diese häufig für mehrere Sprachausprägungen verschiedener Länder anpassen.

Locale


In Java ist das zentrale Konstrukt zur Erfassung der Sprache und Darstellungsformen von Zahlen und Datumswerten einer Region die Klasse java.util.Locale. Diese kann entweder nur mit einer Sprache oder mit einer Sprache und dem Land, dessen besondere Sprachausprägung berücksichtigt werden soll, initialisiert werden. Dabei nimmt der Konstruktor jeweils Strings entgegen, die den ISO-Codes für die Sprache und das Land entsprechen. Für Deutschland sind dies "de" für die Sprache und "DE" für das Land.
Für einige Länder und Regionen gibt es bereits Konstanten in der Klasse Locale, letztlich kommt man mit denen aber aufgrund der geringen Anzahl nicht sehr weit. In der API zur Klasse java.util.Locale (hier verlinkt für das JDK 1.5) finden sich allerdings die Links zu Seiten, die alle ISO-Codes auflisten.
Objekte vom Typ Locale können in zahlreichen Klassen zur Internationalisierung als Parameter übergeben werden. Dies trifft bspw. für die verschiedenen Formatierungs-Klassen des Packages java.text oder für die Klasse java.util.RessourceBundle zu. Auf ResourceBundles gehen wir etwas weiter unten im Abschnitt "Properties-Dateien und ResourceBundles" ein.

Encoding


Das schwierigste Problem bei der Internationalisierung sind die Unterschiede zwischen den verschiedenen Zeichensätze1. Die Schwierigkeit rührt daher, dass man einem Byte erstmal nicht ansieht, ob es ein Zeichen oder nur Teil eines Zeichens ist, das sich aus mehreren Bytes zusammensetzt. Und ebenso wenig, zu welchem Encoding das Byte gehört, d.h. welchem Zeichen das Byte entspricht. Die Zeichenfolge "äöüß" entspricht UTF-8 kodiert der Bytefolge "c3 a4 c3 b6 c3 bc c3 9f", als ISO-8859-1 kodiert hingegen der Bytefolge "e4 f6 fc df". Man kann bei der Analyse von Zeichenfolgen Heuristiken anwenden und so versuchen, das Encoding zu erkennen2, aber dies ist immer mit Unsicherheiten verbunden.
Um nicht selber immer wieder über diese Probleme zu stolpern, empfehle ich dringend, nur ein Encoding durchgängig in der gesamten Webanwendung zu benutzen - unabhängig von der jeweilig ausgewählten Sprache der Nutzerin. Und wenn man sich diese Regel zu eigen macht, gibt es eigentlich nur das Encoding UTF-8, das man nutzen kann. UTF-8 ist ein 32-Bit-Encoding3 und reicht für alle noch lebendigen Schriftsprachen aus. UTF-8 nutzt eine variable Länge pro Zeichen. Für die Zeichen des ASCII-Zeichensatzes wird nur jeweils ein Byte benötigt, für andere Zeichen (bspw. die deutschen Umlaute und m.W. auch alle anderen Zeichen europäischer Sprachen) zwei Bytes, für wieder andere drei oder gar vier Byte. Die Erkennung erfolgt anhand der Bitfolgen der Bytes eines Zeichens. Ist bspw. das erste Bit eine "0", so handelt es sich um ein Ein-Byte-Zeichen, eine mit dem ASCII-Zeichensatz kompatible Definition. Wer mehr dazu wissen will, wie sich UTF-8-Zeichen als Bitfolgen zusammensetzen, kann sich die Seite http://www.unicode.org anschauen. Für den Alltag nützlicher ist die übersichtliche Darstellung der verschiedenen Zeichensätze auf http://www.fileformat.info/info/unicode/block/index.htm, wo es zu jedem Unicode-Bereich den Zeichensatz auch als Bilder-Darstellung gibt.4

Zeichenorientierung


Eine Schwierigkeit für ein konsistentes, internationalisierbares Web-Design ist die Tatsache, dass nicht alle Sprachen die gleiche Leserichtung nutzen. Während im Deutschen die westliche Schreibrichtung von links nach rechts genutzt wird, wird im Arabischen und Hebräischen von rechts nach links geschrieben. Die korrekte Zeichenabfolge im klassichen Chinesischen wiederum ist von rechts oben nach links unten, wohingegen seit den 50ern die chinesische Regierung verstärkt eine von links nach rechts-Schrift forciert. Alle von Java und CSS angebotenen Hilfsmittel5 reichen hier sicher nicht alleine aus. Dennoch helfen die hier und generell von Java bereitgestellten Möglichkeiten, die Änderungen klein zu halten und nur dort wirklich neue Views definieren zu müssen, wo das Design aufgrund einer grundlegenden anderen Schreibrichtung geändert werden muss.6 Weitere Infos zum Thema Internationalisierung in Webanwendungen finden sich auch auf SelfHTML und der Arbeit von Hui Liang. Ein Dokument des W3C zu Besonderheiten der verschiedenen Sprachen und deren Behandlung in HTML findet sich hier: http://www.w3.org/TR/css3-text/.

Formatierung von Zahlen, Währungs- und Datumswerten


Neben den unterschiedlichen Zeichensätzen und der Schriftrichtung sind auch Zahlen keineswegs einheitlich, auch wenn sich die arabischen Ziffern durchgesetzt haben. Doch während wir die Tausender mit einem Punkt kennzeichnen und die Nachkommastellen, wie der Ausdruck schon sagt, durch ein Komma abtrennen, wird in den USA genau anders herum verfahren.
Datumswerte sind ebenso in den verschiedensten Formaten anzutreffen (und auf die verschiedenen Kalender gehe ich hier nicht einmal ein). In den USA wird der 1. Mai 2005 "05/01/2005" formatiert, in England und Frankreich "01/05/2005" und bei uns "01.05.2005". Ebenso sind die Langformen, also Formen in denen der Kalendermonat ausgeschrieben wird, unterschiedlich formatiert.
Es gibt in der Standard-API von Java im Package java.text diverse Formatierer, die anhand eines Locale-Objektes die Formatierung in der jeweiligen Sprache korrekt wiedergeben. Hier wird übrigens auch deutlich, dass allein die Sprachangabe zur Erzeugung eines Locale-Objektes meist nicht ausreicht. Denn bei der Datums-Formatierung unterscheidet sich das US-Englische eben von dem britischen Englisch, was freilich auch für einige Wörter und Begriffe gilt.
Wir wollen kurz in einem einfachen Beispiel zeigen, wie man die Klassen java.util.Locale, java.text.NumberFormat und java.text.DateFormat nutzt:
package org.jsptutorial.examples;

import java.text.*;
import java.util.*;

public class FormatterExample {
   
   private void formatNumbers(Locale locale) {
      NumberFormat formatter = NumberFormat.getNumberInstance(locale);
      System.out.println(formatter.format(101001.22d) + ", " + locale);
   }

   private void formatCurrency(Locale locale) {
      NumberFormat formatter = NumberFormat.getCurrencyInstance(locale);
      System.out.println(formatter.format(101001.22d) + ", " + locale);
   }
   
   private void formatDate(Locale locale) {
      Calendar cal = Calendar.getInstance(locale);
      // this is terrible: it actually is the first of may
      cal.set(2005, 04, 01); 
      Date date = cal.getTime();
      // first a formatting-schema then the actual value
      // BTW: DateFormat.MEDIUM yields a textual month 
      // for French and English but not for German
      DateFormat formatter = DateFormat.getDateInstance(DateFormat.LONG, locale);
      System.out.println(formatter.format(date) + ", " + locale);
      formatter = DateFormat.getDateInstance(DateFormat.SHORT, locale);
      System.out.println(formatter.format(date) + ", " + locale);
   }

   public static void main(String[] parm) {
      FormatterExample example = new FormatterExample();
      example.formatNumbers(Locale.GERMAN);
      example.formatNumbers(Locale.FRANCE);
      example.formatNumbers(Locale.US);
      System.out.println();
      example.formatCurrency(Locale.GERMANY);
      example.formatCurrency(Locale.FRANCE);
      example.formatCurrency(Locale.US);
      System.out.println();
      example.formatDate(Locale.GERMAN);
      example.formatDate(Locale.FRANCE);
      example.formatDate(Locale.US);
      example.formatDate(Locale.UK);
   }
}

Wir bekommen folgende Ausgabe:

101.001,22, de
101 001,22, fr_FR
101,001.22, en_US

101.001,22 €, de_DE
101 001,22 €, fr_FR
$101,001.22, en_US

1. Mai 2005, de
01.05.05, de
1 mai 2005, fr_FR
01/05/05, fr_FR
May 1, 2005, en_US
5/1/05, en_US
01 May 2005, en_GB
01/05/05, en_GB

Hätte man bei dem Beispiel bei der Währungsformatierung nicht die Konstante GERMANY sondern die Konstante GERMAN verwendet, hätte man ein Fragezeichen als Währungssymbol bekommen, da Java die Währung in dem Fall nicht sicher wissen kann (es könnte sowohl der Schweizer Franken als auch der in Deutschland und Österreich genutzte Euro gemeint sein).

Auslesen der Eingaben


Eine weitere Schwierigkeit bei der Internationalisierung einer Anwendung ist, dass man sich der Eingabe der Nutzerin nicht sicher sein kann. Nimmt diese nun ein Komma oder einen Punkt für die Nachkommastellen? Formatiert sie das Datum so wie landesweit üblich oder doch so wie in einer anderen Sprache üblich, möglicherweise, weil sie dies von anderen Webseiten oder Desktop-Programmen gewohnt ist? Hier gibt es natürlich nicht die Lösung. Einfach zu versuchen, das, was die Nutzerin meinte, zu erraten, ist immer schlecht.7 Wie will man auf einer deutschen Webseite das Datum 05/01/2005 interpretieren? Als 1.5.2005 oder als 5.1.2005? Das wird nicht funktionieren. Also sollte man es gar nicht erst versuchen. Zwar sind strikte Regeln auf den ersten Blick Nutzerinnen-unfreundlich, aber für die Nutzerin nicht nachvollziehbare Folgen, weil sie sich eben doch was ganz anderes gedacht hat, sind für diese weitaus ärgerlicher. Wenn ein Datum im Format Tag.Monat.Jahr erwartet wird, dann muss man die Anwenderin per Rückmeldung bitten, dies genau so einzugeben.

Zum SeitenanfangZum Seitenanfang

Properties-Dateien und ResourceBundles

Properties-Dateien und die Klasse java.util.ResourceBundle sind das standardmäßig von Java mitgelieferte Instrument zur Internationalisierung von Texten.
Dies funktioniert so, dass die Texte in Properties-Dateien, die in diesem Zusammenhang ResourceBundles genannt werden, vorrätig gehalten werden und über ihre Keys ausgelesen werden. Dazu legt man für jede Sprache und ggf. jede länderspezifische Sprachvariante eine eigene Properties-Datei an. Diese werden nach einer festen Regel durchlaufen. I.F. gehen wir davon aus, dass die Namen der Properties-Dateien alle mit "ApplicationResources" beginnen. Sucht man einen Key für eine länderspezifische Locale (bspw. de_DE, so wird als erstes versucht, den Key aus einer Datei zu finden, die einen entsprechenden Namenszusatz hat. Also in unserem Fall ApplicationResources_de_DE.properties. Gibt es diese Datei nicht, oder wird der Key in dieser Datei nicht gefunden, wird in der Datei ohne Länder-Kürzel gesucht, also ApplicationResources_de.properties. Und gibt es diese nicht, so wird in der Basis-Datei ApplicationResources.properties gesucht. Wird der Key auch hier nicht gefunden, wird eine java.util.MissingResourceException geworfen. Durch dieses automatische Durchlaufen der Hierarchie muss man bspw. in länderspezifischen Dateien nur das überschreiben, was wirklich anders ist. So werden die Dateien übersichtlicher gehalten.
Hier zwei ResourceBundle-Dateien mit lediglich zwei Keys. Zunächst die Datei ApplicationResources.properties, dann die Datei ApplicationResources_de_DE.properties:

# form related keys:
submit.ok=OK
submit.cancel=Cancel

# form related keys:
submit.ok=\u00dcbernehmen
submit.cancel=Abbrechen

In der deutschen Datei fällt auf, dass der Umlaut in der Java-Unicode-Notation wiedergegeben ist. Dies sollte mit allen Zeichen, die nicht im ASCII-Zeichensatz enthalten sind, gemacht werden. Natürlich wäre es eine langwierige Aufgabe, dies von Hand zu machen. Also wird von SUN im JDK das Tool "native2ascii" mitgeliefert, das wir im entsprechenden Abschnitt weiter unten vorstellen.
Man greift auf die Texte in den Properties-Dateien mittels der Klasse java.util.ResourceBundle zu. Dieser gibt man den Namen des Root-ResourceBundles mit und das gewünschte Locale. Die folgende kleine Klasse gibt aus dem oben gezeigten deutschen ResourceBundle den Wert des Keys "submit.ok" aus:
package org.jsptutorial.examples;

import java.util.Locale;
import java.util.ResourceBundle;

public class ResourceBundleExample {

   public static void main(String[] parm) {
      Locale locale = Locale.GERMANY;
      ResourceBundle bundle = 
         ResourceBundle.getBundle("ApplicationResources", locale);
      System.out.println(bundle.getString("submit.ok"));
   }
}

Wichtig ist hier nur, dass das ResourceBundle im Klassenpfad liegt. Man kann die ResourceBundle-Dateien in Verzeichnisse entsprechend den Packages unterbringen oder wie hier im Root des Klassenpfades (bei Webanwendungen wäre das bspw. im Verzeichnis WEB-INF/classes der jeweiligen Webanwendung).

Parametrisierte Texte


Bei manchen Texten, die ausgegeben werden sollen, müssen Ergebnisse von Berechnungen oder Texte aus Formularen etc. eingefügt werden. Diese Werte müssen je nach Sprache an einer anderen Stelle in den Text eingefügt werden. Daher gibt es die Möglichkeit, parametrisierbare Texte in den ResourceBundles unterzubringen.
Nehmen wir an, wir wollen bei der Registrierung einer Nutzerin diese darauf hinweisen, dass ein Nutzername bereits vergeben ist. Dann tragen wir in die Properties-Datei folgenden Key und Wert ein:8
reg.account_in_use=Sie haben "{0}" als Nutzernamen ausgewählt. Dieser ist leider schon vergeben.

Diesen kann man nun als Template nehmen und die benötigten Werte als Parameter eintragen lassen. Dazu erzeugt man ein Array mit den Werten, die man anstelle der Platzhalter eintragen möchte. Der Index des Arrays entspricht dabei der Nummer des Platzhalters. Das Array kann ein Objekt-Array sein, somit können bspw. Double- und String-Werte gleichzeitig substituiert werden.
Das Template und das Array übergibt man nun der Methode format(String, Object[]) der Klasse java.text.MessageFormat. Als Rückgabewert erhält man den mit den entsprechenden Werten aufgefüllten String:
package org.jsptutorial.examples;

import java.text.MessageFormat;
import java.util.*;

public class ParameterizedResourceBundleExample {

   public static void main(String[] parm) {
      Locale locale = Locale.GERMANY;
      ResourceBundle bundle = 
         ResourceBundle.getBundle("ApplicationResources", locale);
      String bundleValue = bundle.getString("reg.account_in_use");
      String[] args = new String[]{"test"};
      String result = MessageFormat.format(bundleValue, args);
      System.out.println("Formatiert: " + result);
   }
}

native2ascii


Weiter oben haben wir in einem deutschen ResourceBundle die Java-typische Unicode-Formatierung von Umlauten gesehen und darauf hingewiesen, dass SUN ein Tool mitliefert, das das Abändern der Umlaute und aller sonstige Nicht-ASCII-Zeichen übernimmt. Dieses Tool ist das kleine Programm native2ascii, das im bin-Verzeichnis der JDK-Installation liegt. Im Folgenden wird davon ausgegangen, dass dieses Verzeichnis in der PATH-Variable des Betriebssystem gespeichert ist. Wenn nicht, müssen Sie bei den folgenden Beispielen entsprechend vor dem Befehl noch den Pfad ergänzen.
Nehmen wir an, wir hätten unser ResourceBundle in einem Editor editiert. Dieser speichert unseren Text in einem bestimmten Encoding ab. Im Deutschen ist dies in aller Regel ISO-8859-1, aber die meisten Editoren ermöglichen es einem, das Encoding einzustellen, in welchem man die Datei abspeichern möchte.
Wenn die Datei in ISO-8859-1 vorliegt, geben wir folgenden Befehl ein:
native2ascii -encoding ISO-8859-1 ApplicationResources_de_DE.properties > transformed/ApplicationResources_de_DE.properties

Anstelle von ISO-8859-1 kann natürlich auch jedes andere durch das JDK unterstützte Encoding verwendet werden. Die Ausgabe wurde hier in eine gleich benannte Datei im Verzeichnis transformed umgeleitet. Idealerweise würde man diesen Schritt in einem ANT-Skript automatisiert durchführen lassen. Zum entsprechenden ANT-Task gibt es hier Informationen: http://ant.apache.org/manual/OptionalTasks/native2ascii.html. So oder so darf die Zieldatei nicht gleich der Ausgangsdatei sein, und somit muss ggf. das Ergebnis wieder zurück kopiert werden. Auch dies kann man bequem mit ANT erledigen. Auf jeden Fall lohnt sich hier die Automatisierung durch ein Skript, ob nun ANT, Bash-Skript oder was auch immer. Schließlich wird man in einer wirklich internationalisierten Anwendung nicht nur deutsche Properties-Dateien umwandeln, und dann kann das schnell viel Arbeit werden.
Will man später den mit native2ascii umgewandelten Text erneut überarbeiten, sollte er natürlich wieder leicht lesbar sein. Man muss also die Umwandlung rückgängig machen. Dazu verwendet man einfach den gleichen Befehl, nun mit dem Parameter "reverse":
native2ascii -reverse -encoding ISO-8859-1 transformed/ApplicationResources_de_DE.properties > ApplicationResources_de_DE.properties

Auch dieses Zurückwandeln wird vom entsprechenden ANT-Task unterstützt.

Zum SeitenanfangZum Seitenanfang

Encoding-Informationen in JSPs, Servlets und im HTML-Code

Intern verwendet Java immer Unicode. Wenn nun der HTML-Code generiert und Text ausgegeben wird, so muss die Servlet-Engine diesen in das gewünschte Ziel-Encoding umformen. Natürlich muss die Servlet-Engine wissen, welches Ziel-Encoding denn verwendet werden soll. In JSPs schreibt man dazu eine der beiden folgenden Zeilen:

<%@ page contentType="text/html; charset=UTF-8" %>

<%@ page pageEncoding="UTF-8" %>

Beide generieren in diesem Fall das gleiche Ergebnis. Die obere der beiden Angaben ist lediglich eine Kurzform, wenn man einen anderen Content-Type wie bspw. "application/xml" verwenden will. Die Angabe "text/html" ist der Default-Wert und somit ist bei HTML-Dateien die untere Zeile ausreichend.
In Servlet verwendet man äquivalent dazu folgenden Code:
package org.jsptutorial.examples.servlets;

import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.http.*;

public class EncodingExampleServlet extends HttpServlet {

   protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      response.setContentType("text/html");
      response.setCharacterEncoding("UTF-8");
      PrintWriter out = response.getWriter();
      out.println("<html><head>");
      out.println("<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\" />");
      out.println("</head><body>");
      out.println("Hallo deutsche Umlautwelt: Köln, Münster, Göttingen...");
      out.println("</body></html>");
      
   }

   protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      this.doGet(request, response);
   }
}

Schauen wir uns für das Servlet sowohl die Anfrage als auch die Antwort kurz an:
screenshot_EncodingExampleServlet_req_resp.png
Wir sehen, dass der Browser sich bezüglich des Encodings nicht eindeutig festlegt. Von dieser Seite ist also keine Hilfe zu erwarten. Die Antwort hingegen ist in UTF-8 kodiert und auch so gekennzeichnet. Damit kommen, wie im Screenshot deutlich wird, die Zeichen in dem gewünschten Encoding beim Browser an. Und auch der HTTP-Header "Content-Type" wurde generiert, der dem Browser mitteilt, dass die Seite in UTF-8 kodiert ist.
Leider, wie so oft, wird diese Information nicht von allen Browser genutzt. Es empfiehlt sich daher zusätzlich, folgendes in den Header-Bereich des HTML-Codes mit aufzunehmen, um auf der sicheren Seite zu sein:
<html>
<head>
<!-- some more header information -->

   <!-- Content is of type HTML and encoded using UTF-8 -->
   <meta http-equiv="content-type" content="text/html; charset=UTF-8" />

<!-- even more header information -->
</head>
<body>
<!-- the body of the page -->
</body>
</html>

Zum SeitenanfangZum Seitenanfang

Servlet-Filter zur Sicherstellung eines korrekten Encodings

Wie oben dargelegt, sollte möglichst ein einheitliches Encoding für alle Webseiten verwendet werden. Wenn man nun das empfohlene Encoding UTF-8 nutzt, wird man aber schnell über ein Problem stolpern: Die übertragenen Formular-Werte sind nicht korrekt formatiert - oder zumindest erscheint es so.
Wenn man bspw. "Köln" in einer UTF-8 kodierten Webseite in ein Formular einträgt und den Wert mit request.getParameter(String) ausliest, so bekommt man das unschöne Ergebnis "Köln". Wie wir inzwischen wissen, kommt das daher, dass in UTF-8 einzelne Zeichen mit mehr als einem Byte dargestellt werden. Das "ö" in diesem Fall mit diesen zwei kryptischen Zeichen (entsprechend den zwei Bytes #c3 und #b6). Aber wieso werden die zwei Bytes nicht automatisch als "ö" erkannt, wo wir doch UTF-8 als Encoding angegeben haben?
Etwas weiter oben haben wir bei dem Beispiel des EncodingExampleServlets festgestellt, dass der Browser keinerlei sinnvollen Angaben über das Encoding mitliefert. Wenn man aber die Encodings so gesetzt hat wie weiter oben beschrieben und bspw. durchgängig UTF-8 verwendet, so wissen wir, dass die Anfrage in UTF-8 encodiert ist. Dies müssen wir nun nur noch der Servlet-Engine mitteilen. Und dazu ruft man die Methode request.setCharacterEncoding(String) auf, wobei request hier ein Objekt vom Typ java.servlet.ServletRequest bezeichnet. Der String, der der Methode übergeben wird, muss ein gültiges Encoding bezeichnen, ansonsten bekommt man eine UnsupportedEncodingException. Welche Encodings unterstützt werden, hängt vom Hersteller der virtuellen Maschine ab. Es gibt aber ein garantiertes Mindestset, den jede VM unterstützen muss. Der kann für die jeweils genutzte JDK-Version der API-Dokumentation der Klasse java.nio.charset.Charset entnommen werden. Sowohl ISO-8859-1 als auch UTF-8 gehören zu diesem Standardset unterstützter Encodings.
Die Methode setCharacterEncoding(String) muss aufgerufen werden, bevor das erste mal Request-Parameter ausgelesen werden, bzw. der BufferedReader über die Methode getReader() geholt wird. Ansonsten hat der Aufruf keine Wirkung. Dies sicherzustellen gelingt am besten über einen Filter (s. dazu das Kapitel Servlet-Filter). Dieser wird idealerweise als erster Filter jedweder Filterkette deklariert, um ganz sicher zu gehen, dass nicht schon ein anderer Filter lesend auf Request-Parameter zugreift.
Im Folgenden ein beispielhafter Code für solch einen Filter:

package org.jsptutorial.util.filters;

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * A simple encoding-filter to ensure correct
 * handling of request encoding.
 *
 * @author Wolfram Rittmeyer, www.jsptutorial.org
 */
public class EncodingFilter implements Filter {

   private static final Log logger = LogFactory.getLog(EncodingFilter.class);
   private String encoding = "UTF-8";

   /**
    * Set the character-encoding of the request to the
    * encoding specified in the deployment descriptor.
    * If none is specified use UTF-8.
    */
   public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
      if (logger.isDebugEnabled()) {
         logger.debug("filtering: " + ((HttpServletRequest)request).getRequestURL());
      }
      request.setCharacterEncoding(encoding);
      filterChain.doFilter(request, response);
   }

   /**
    * Initialize the filter.
    */
   public void init(FilterConfig filterConfig) throws ServletException {
      logger.info("Filter EncodingFilter initializing...");
      String str = filterConfig.getInitParameter("encoding");
      if (str != null) {
         encoding = str;
      }
      logger.info("Using encoding: " + this.encoding);
   }

   public void destroy() {
        logger.info("Filter EncodingFilter destroyed...");
   }
}

Konfiguriert wird dieser Filter durch folgenden Eintrag in der Datei web.xml:
<web-app>
<!-- ... -->

   <!-- first define a named filter -->
   <filter>
      <filter-name>EncodingFilter</filter-name>
      <filter-class>org.jsptutorial.util.filters.EncodingFilter</filter-class>
      <init-param>
         <param-name>encoding</param-name>
         <param-value>UTF-8</param-value>
      </init-param>
   </filter>

   <!-- now map this filter to a URL-pattern -->
   <filter-mapping>
      <filter-name>EncodingFilter</filter-name>
      <url-pattern>/*</url-pattern>
   </filter-mapping>

<!-- ... -->
</web-app>

Mehr zum Thema Servlet-Filter und Filterketten im Kapitel Servlet-Filter.

Zum SeitenanfangZum Seitenanfang

Probleme mit GET-Anfragen

Leider funktioniert der im vorigen Abschnitt vorgestellte Filter nur mit per POST übertragenen Formular-Werten korrekt. Nutzt man GET als Absende-Methode oder hängt man an die URL Parameter-Werte, so stößt man auf Probleme mit der fehlenden Standard-Konformität der Browser. Diese sollten gemäß W3C-Spezifikation GET-Parameter immer im UTF-8-Format an die Adresse anhängen.9 Wieder mal ist auch hier auf die Browser-Hersteller kein Verlass. Anstelle des Standards ist hier leider das Verhalten weit verbreitet, die Parameter in dem Encoding an die Adresse anzuhängen, das gerade vom Browser verwendet wird. Ist also die Seite in ISO-8859-1 kodiert, senden die meisten Browser auch den Anfrage-String als ISO-8859-1-kodierten String und eben nicht im standard-konformen UTF-8-Format. Aufgrund dessen, dass nicht alle Browser sich an diesen Standard halten, kann es sein, dass bei GET-Anfragen die Methode setCharacterEncoding(String) nicht das gewünschte Ergebnis bringt.
Im Tomcat, genauer im Coyote-Connector, kann man daher an zwei Schrauben drehen, um das gewünschte Verhalten des Containers einzustellen. Dies sind zwei optionale Attribute des Connector-Elementes in der Datei server.xml im Verzeichnis conf unterhalb des Tomcat-Root-Verzeichnisses.
Das eine Attribut heißt useBodyEncodingForURI. Dieses legt fest, ob für URL-kodierte Werte der Wert genutzt werden soll, der per setCharacterEncoding(String) gesetzt wird. Was auf den ersten Blick wie die gewünschte Lösung aussieht, birgt allerdings ein Problem mit dem oben genannten Standard. Nutzt ein Browser diesen Standard bei Seiten, die nicht UTF-8-kodiert sind, funktioniert diese Einstellung nicht.
Das andere Attribut heißt URIEncoding. Dieses legt fest, welches Standard-Encoding für URL-kodierte Werte angenommen werden soll. Standardmäßig wird hier ISO-8859-1 angenommen. Dies entspricht dem Standard-Encoding, das gemäß Servlet-Spec genutzt werden soll, wenn setCharacterEncoding(String) nicht aufgerufen wird. Mit dem Standard-Wert ISO-8859-1 läuft man aber erneut Gefahr, dass die Seite bei Browsern, die sich standard-konform verhalten und die Werte mit UTF-8 kodieren, nicht korrekt funktioniert. Die Tomcat-Entwicklerinnen sitzen hier in einer Klemme zueinander inkompatibler Spezifikationen. Hier ist wünschenswert, dass die Servlet-Spezifikation dem W3C-Standard folgt und UTF-8 zum Standard URL-kodierter Werte macht. Leider ist dies auch im vorliegenden "Final Public Draft" der Servlet-Spezifikation 2.5 noch nicht der Fall!
Auch diese Problematik stärkt unser Argument für die durchgängige Verwendung von UTF-8. Setzt man das erste Attribut auf "false"10 und das zweite auf UTF-8, so funktioniert das mit allen Browsern, da diese sich - ob sie sich nun bei anderem Encoding standard-konform verhalten oder nicht - bei UTF-8 korrekt verhalten. Bei UTF-8 und den hier empfohlenen Werten im Connector-Element der Tomcat-Konfigurationsdatei funktioniert der obige Filter dann auch bei GET-Anfragen korrekt. Leider aber wirklich nur dann.

Zum SeitenanfangZum Seitenanfang

Abschließend sei noch darauf hingewiesen, dass es zahlreiche Hilfen zur Internationalisierung in den verschiedenen Frameworks wie Struts oder JSF gibt und die Java Standard Tag Library einem nützliche Tags an die Hand gibt, um auf Werte aus dem ResourceBundle zuzugreifen. Das Kapitel zur Java Standard Tag Library (JSTL) ist derzeit in Arbeit. Bis dahin lohnt ein Blick auf die englische Standard-Dokumentation unter http://java.sun.com/products/jsp/jstl/1.1/docs/tlddocs/index.html. Die für die Internationalisierung interessanten Tags liegen in der "JSTL fmt"-Bibliothek.

Zum SeitenanfangZum Seitenanfang

Anmerkungen:

1) Genauer ist dies häufig auch schon ein Problem zwischen unterschiedlichen Betriebssystemen, die zwar die gleiche Spracheinstellung, aber nicht die gleiche Zeichensatzcodierung nutzen. Bekannte Beispiele sind bspw. die Unterschiede des Zeichensatzes zwischen MS-DOS (Codepage 850) und Windows (Codepage 1252, entspricht Latin 1, bzw. ISO-8859-1) oder zwischen EBCDIC (IBM-Großrechner) und ASCII. (zurück)

2) Mozilla verwendet seine "Mozilla Charset Detectors", um das Encoding zu erraten; der Code wurde auch nach Java portiert. (zurück)

3) Bis 2001 wurden nur 16 Bit genutzt. Inzwischen wurden aber auch einige historische Zeichensätze und Musik-Notationen wie "Antike Griechische Musik-Symbole" mit aufgenommen, und es werden mehr als 65.536 Zeichen durch das Unicode-System unterstützt. (zurück)

4) Da es keinen Zeichensatz gibt, der alle Unicode-Zeichen darstellen kann, ist die Wiedergabe der Zeichen als Bild für viele Zeichensätze unverzichtbar. (zurück)

5) CSS unterstützt die Unterschiede zwischen von-links-nach-rechts- und von-rechts-nach-links-Schrift mit dem Attribut "direction" und den Werten "ltr" (left to right) und "rtl" (right to left). (zurück)

6) Es ist m.E. ohnehin fraglich, ob ein einheitliches Webdesign für alle Länder und Kulturen angemessen ist. (zurück)

7) Das ist eines der Haupt-Ärgernisse sogenannter intelligenter Programme - leider eine Unsitte, die sich auch außerhalb von Redmond zunehmend breit macht. (zurück)

8) Der Verständlichkeit wegen ist das ResourceBundle noch nicht mit dem Tool native2ascii bearbeitet worden. (zurück)

9) Die Spezifikation ist sogar noch rigider und sehr genau und fordert für alle Zeichen, mit Ausnahme von Ziffern und Buchstaben die Formatierung in der Form %xy, wobei xy jeweils durch die zwei Ziffern des Hex-Werts des aktuellen UTF-8-Bytes ersetzt werden müssen. (zurück)

10) Seit Tomcat 5.0 ist der Standard-Wert für das Attribut useBodyEncodingForURI "true", in Tomcat 4.x war der Standard-Wert hingegen "false". (zurück)


www.jsptutorial.org
© 2005, 2006, 2007