Internationalisierung und Lokalisierung in Java 8

Internationalisierung und Lokalisierung in Java 8

1. Überblick

Internationalization is a process of preparing an application to support various linguistic, regional, cultural or political-specific data. Dies ist ein wesentlicher Aspekt jeder modernen mehrsprachigen Anwendung.

Für die weitere Lektüre von, sollten wir wissen, dass es eine sehr beliebte Abkürzung (wahrscheinlich populärer als der tatsächliche Name) für die Internationalisierung gibt -i18n aufgrund der 18 Buchstaben zwischen 'i' und 'n'.

Für heutige Unternehmensprogramme ist es entscheidend, Menschen aus verschiedenen Teilen der Welt oder aus verschiedenen Kulturbereichen zu dienen. Unterschiedliche Kultur- oder Sprachregionen bestimmen nicht nur sprachspezifische Beschreibungen, sondern auch Währung, Zahlendarstellung und sogar unterschiedliche Datums- und Zeitzusammensetzungen.

Konzentrieren wir uns beispielsweise auf länderspezifische Zahlen. Sie haben verschiedene Dezimal- und Tausendertrennzeichen:

  • 102.300,45 (Vereinigte Staaten)

  • 102 300,45 (Polen)

  • 102.300,45 (Germany)

Es gibt auch verschiedene Datumsformate:

  • Montag, 1. Januar 2018, 15:20:34 Uhr MEZ (Vereinigte Staaten)

  • Donnerstag, 1. Januar 2018, 15 Uhr 20 MEZ (Frankreich).

  • Von 2018 bis 1 von 1 bis 3 von 20 bis 34 Uhr MEZ (China)

Darüber hinaus haben verschiedene Länder eindeutige Währungssymbole:

  • £ 1.200.60 (Vereinigtes Königreich)

  • € 1.200,60 (Italien)

  • 1 200,60 € (Frankreich)

  • $1,200.60 (United States)

Es ist wichtig zu wissen, dass selbst wenn Länder dieselbe Währung und dasselbe Währungssymbol haben - wie Frankreich und Italien - die Position ihres Währungssymbols unterschiedlich sein kann.

2. Lokalisierung

In Java steht uns eine fantastische Funktion zur Verfügung, dieLocale-Klasse.

Dadurch können wir schnell zwischen kulturellen Schauplätzen unterscheiden und unsere Inhalte entsprechend formatieren. Dies ist für den Internationalisierungsprozess von wesentlicher Bedeutung. Wie i18n hat auch die Lokalisierung die Abkürzung -*l10n*.

Der Hauptgrund für die Verwendung vonLocale besteht darin, dass auf alle erforderlichen länderspezifischen Formatierungen ohne Neukompilierung zugegriffen werden kann. Eine Anwendung kann mehrere Gebietsschemas gleichzeitig verarbeiten, sodass die Unterstützung neuer Sprachen unkompliziert ist.

Gebietsschemas werden normalerweise durch Sprache, Land und Variantenabkürzung dargestellt, die durch einen Unterstrich getrennt sind:

  • de

  • it_CH (Italienisch, Schweiz)

  • de_DE_UNIX (Vereinigte Staaten, UNIX-Plattform)

2.1. Felder

Wir haben bereits gelernt, dassLocale consists of language code, country code, and variant. There are two more possible fields to set: script and extensions.

Schauen wir uns eine Liste der Felder an und sehen wir uns die Regeln an:

  • Language kann einISO 639 alpha-2 or alpha-3 Code oder ein Untertag in registrierter Sprache sein.

  • Region (Land) ist die Landesvorwahl vonISO 3166 alpha-2oder die Vorwahl vonUN numeric-3.

  • Variant ist ein Wert, bei dem zwischen Groß- und Kleinschreibung unterschieden wird, oder eine Menge von Werten, die eine Variation vonLocale angeben.

  • Script muss ein gültigerISO 15924 alpha-4-Code sein.

  • Extensions ist eine Karte, die aus einzelnen Zeichenschlüsseln undString Werten besteht.

IANA Language Subtag Registry enthält mögliche Werte fürlanguage,region,variant undscript.

Es gibt keine Liste möglicherextension-Werte, aber die Werte müssen wohlgeformteBCP-47-Subtags sein. Die Schlüssel und Werte werden immer in Kleinbuchstaben umgewandelt.

2.2. Locale.Builder

Es gibt verschiedene Möglichkeiten,Locale-Objekte zu erstellen. Ein möglicher Weg verwendetLocale.Builder. Locale.Builder verfügt über fünf Setter-Methoden, mit denen wir das Objekt erstellen und gleichzeitig diese Werte validieren können:

Locale locale = new Locale.Builder()
  .setLanguage("fr")
  .setRegion("CA")
  .setVariant("POSIX")
  .setScript("Latn")
  .build();

DieString-Darstellung der obigenLocale istfr_CA_POSIX # Latn_.

Es ist gut zu wissen, dasssetting ‘variant' may be a little bit tricky as there’s no official restriction on variant values, although the setter method requires it to be BCP-47 compliant.

Andernfalls wirdIllformedLocaleException ausgelöst.

In dem Fall, dass wir einen Wert verwenden müssen, der die Validierung nicht besteht, können wirLocale-Konstruktoren verwenden, da sie keine Werte validieren.

2.3. Konstruktoren

Locale hat drei Konstruktoren:

  • neues Gebietsschema (String-Sprache)

  • neues Gebietsschema (String-Sprache, String-Land)

  • neues Gebietsschema (String-Sprache, String-Land, String-Variante)

Ein 3-Parameter-Konstruktor:

Locale locale = new Locale("pl", "PL", "UNIX");

Ein gültigesvariant muss einString von 5 bis 8 alphanumerischen Zeichen oder eine einzelne Zahl sein, gefolgt von 3 alphanumerischen Zeichen. Wir können "UNIX" nur über den Konstruktor auf das Feldvariantanwenden, da es diese Anforderungen nicht erfüllt.

Es gibt jedoch einen Nachteil bei der Verwendung von Konstruktoren zum Erstellen vonLocale-Objekten: Wir können keine Erweiterungen und Skriptfelder festlegen.

2.4. Konstanten

Dies ist wahrscheinlich die einfachste und am wenigsten eingeschränkte Methode, umLocales zu erhalten. Die KlasseLocalehat mehrere statische Konstanten, die das beliebteste Land oder die beliebteste Sprache darstellen:

Locale japan = Locale.JAPAN;
Locale japanese = Locale.JAPANESE;

2.5. Sprach-Tags

Eine andere Möglichkeit,Locale zu erstellen, besteht darin, die statische Factory-MethodeforLanguageTag(String languageTag) aufzurufen. Diese Methode erfordert einString, das demIETF BCP 47-Standard entspricht.

So können wir die UKLocale erstellen:

Locale uk = Locale.forLanguageTag("en-UK");

2.6. Verfügbare Gebietsschemas

Obwohl wir mehrere Kombinationen vonLocale-Objekten erstellen können, können wir sie möglicherweise nicht verwenden.

Ein wichtiger Hinweis ist, dass dieLocales auf einer Plattform von denen abhängen, die in der Java Runtime installiert wurden.

Da wirLocales für die Formatierung verwenden, steht den verschiedenen Formatierern möglicherweise noch wenigerLocales zur Verfügung, die in der Laufzeit installiert sind.

Lassen Sie uns überprüfen, wie Arrays verfügbarer Gebietsschemas abgerufen werden:

Locale[] numberFormatLocales = NumberFormat.getAvailableLocales();
Locale[] dateFormatLocales = DateFormat.getAvailableLocales();
Locale[] locales = Locale.getAvailableLocales();

Danach können wir überprüfen, ob sich unsereLocaleunter den verfügbarenLocales.befinden

Wir sollten uns daran erinnern, dassthe set of available locales is different for various implementations of the Java Platformand various areas of functionality.

Die vollständige Liste der unterstützten Gebietsschemas ist aufthe Oracle’s Java SE Development Kit webpage. verfügbar

2.7. Standardgebietsschema

Während der Arbeit mit der Lokalisierung müssen wir möglicherweise wissen, wie hoch die StandardeinstellungLocalein unserer InstanzJVMist. Glücklicherweise gibt es einen einfachen Weg, dies zu tun:

Locale defaultLocale = Locale.getDefault();

Wir können auch einen Standardwert vonLocaleangeben, indem wir eine ähnliche Setter-Methode aufrufen:

Locale.setDefault(Locale.CANADA_FRENCH);

Dies ist besonders relevant, wenn wirJUnit-Tests erstellen möchten, die nicht von derJVM-Instanz abhängen.

3. Zahlen und Währungen

Dieser Abschnitt bezieht sich auf Zahlen- und Währungsformatierer, die unterschiedlichen länderspezifischen Konventionen entsprechen sollten.

Um primitive Zahlentypen (int,double) sowie deren Objektäquivalente (Integer,Double) zu formatieren, sollten wir die KlasseNumberFormat und ihre statische Klasse verwenden Fabrikmethoden.

Zwei Methoden sind für uns interessant:

  • NumberFormat.getInstance(Locale locale)

  • NumberFormat.getCurrencyInstance(Locale locale)

Lassen Sie uns einen Beispielcode untersuchen:

Locale usLocale = Locale.US;
double number = 102300.456d;
NumberFormat usNumberFormat = NumberFormat.getInstance(usLocale);

assertEquals(usNumberFormat.format(number), "102,300.456");

Wie wir sehen können, ist es so einfach,Locale zu erstellen und damit die Instanz vonNumberFormatabzurufen und eine Beispielnummer zu formatieren. Wir können feststellen, dassthe output includes locale-specific decimal and thousand separators.

Hier ist ein weiteres Beispiel:

Locale usLocale = Locale.US;
BigDecimal number = new BigDecimal(102_300.456d);

NumberFormat usNumberFormat = NumberFormat.getCurrencyInstance(usLocale);
assertEquals(usNumberFormat.format(number), "$102,300.46");

Das Formatieren einer Währung umfasst dieselben Schritte wie das Formatieren einer Zahl. Der einzige Unterschied besteht darin, dass der Formatierer das Währungssymbol und den runden Dezimalteil an zwei Ziffern anfügt.

4. Datum und Uhrzeit

Jetzt lernen wir die Formatierung von Datum und Uhrzeit kennen, die wahrscheinlich komplexer ist als die Formatierung von Zahlen.

Zunächst sollten wir wissen, dass sich die Formatierung von Datum und Uhrzeit in Java 8 erheblich geändert hat, da es eine völlig neueDate/Time-API enthält. Daher werden wir verschiedene Formatierungsklassen durchsehen.

4.1. DateTimeFormatter

Since Java 8 was introduced, the main class for localizing of dates and times is the DateTimeFormatter class. Es arbeitet mit Klassen, die die Schnittstelle vonTemporalAccessorimplementieren, z. B.LocalDateTime,LocalDate, LocalTime oderZonedDateTime. . Um einDateTimeFormatter zu erstellen, müssen wir mindestens ein Muster bereitstellen, und dann Locale. Lass uns einen Beispielcode sehen:

Locale.setDefault(Locale.US);
LocalDateTime localDateTime = LocalDateTime.of(2018, 1, 1, 10, 15, 50, 500);
String pattern = "dd-MMMM-yyyy HH:mm:ss.SSS";

DateTimeFormatter defaultTimeFormatter = DateTimeFormatter.ofPattern(pattern);
DateTimeFormatter deTimeFormatter = DateTimeFormatter.ofPattern(pattern, Locale.GERMANY);

assertEquals(
  "01-January-2018 10:15:50.000",
  defaultTimeFormatter.format(localDateTime));
assertEquals(
  "01-Januar-2018 10:15:50.000",
  deTimeFormatter.format(localDateTime));

Wir können sehen, dass wir nach dem Abrufen vonDateTimeFormatter nur die Methodeformat() aufrufen müssen.

Zum besseren Verständnis sollten wir uns mit möglichen Musterbuchstaben vertraut machen.

Schauen wir uns zum Beispiel Buchstaben an:

Symbol  Meaning                     Presentation      Examples
  ------  -------                     ------------      -------
   y       year-of-era                 year              2004; 04
   M/L     month-of-year               number/text       7; 07; Jul; July; J
   d       day-of-month                number            10

   H       hour-of-day (0-23)          number            0
   m       minute-of-hour              number            30
   s       second-of-minute            number            55
   S       fraction-of-second          fraction          978

Alle möglichen Musterbuchstaben mit Erklärung finden Sie inhttps://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html.It’s worth to know that final value depends on the number of symbols. Im Beispiel gibt 'MMMM' den vollständigen Monatsnamen aus, wohingegen ein einzelner 'M'-Buchstabe die Monatsnummer ohne führende 0 angibt.

Um mitDateTimeFormatter fertig zu werden, sehen wir uns an, wie wirLocalizedDateTime formatieren können:

LocalDateTime localDateTime = LocalDateTime.of(2018, 1, 1, 10, 15, 50, 500);
ZoneId losAngelesTimeZone = TimeZone.getTimeZone("America/Los_Angeles").toZoneId();

DateTimeFormatter localizedTimeFormatter = DateTimeFormatter
  .ofLocalizedDateTime(FormatStyle.FULL);
String formattedLocalizedTime = localizedTimeFormatter.format(
  ZonedDateTime.of(localDateTime, losAngelesTimeZone));

assertEquals("Monday, January 1, 2018 10:15:50 AM PST", formattedLocalizedTime);

UmLocalizedDateTime zu formatieren, können wir die MethodeofLocalizedDateTime(FormatStyle dateTimeStyle) verwenden und ein vordefiniertesFormatStyle. bereitstellen

Für einen tieferen Einblick in die API von Java 8Date/Timehaben wir einen vorhandenen Artikelhere.

4.2. DateFormat undSimpleDateFormatter

Da es immer noch üblich ist, an Projekten zu arbeiten, dieDates undCalendars verwenden, werden wir kurz die Funktionen zum Formatieren von Datums- und Uhrzeitangaben mit den KlassenDateFormat undSimpleDateFormat vorstellen.

Analysieren wir die Fähigkeiten des ersten:

GregorianCalendar gregorianCalendar = new GregorianCalendar(2018, 1, 1, 10, 15, 20);
Date date = gregorianCalendar.getTime();

DateFormat ffInstance = DateFormat.getDateTimeInstance(
  DateFormat.FULL, DateFormat.FULL, Locale.ITALY);
DateFormat smInstance = DateFormat.getDateTimeInstance(
  DateFormat.SHORT, DateFormat.MEDIUM, Locale.ITALY);

assertEquals("giovedì 1 febbraio 2018 10.15.20 CET", ffInstance.format(date));
assertEquals("01/02/18 10.15.20", smInstance.format(date));

DateFormat arbeitet mitDates und hat drei nützliche Methoden:

  • getDateTimeInstance

  • getDateInstance

  • getTimeInstance

Alle verwenden vordefinierte Werte vonDateFormat als Parameter. Jede Methode ist überladen, sodass auchLocale übergeben werden können. Wenn wir ein benutzerdefiniertes Muster verwenden möchten, wie es inDateTimeFormatter erfolgt, können wirSimpleDateFormat verwenden. Sehen wir uns einen kurzen Code-Ausschnitt an:

GregorianCalendar gregorianCalendar = new GregorianCalendar(
  2018, 1, 1, 10, 15, 20);
Date date = gregorianCalendar.getTime();
Locale.setDefault(new Locale("pl", "PL"));

SimpleDateFormat fullMonthDateFormat = new SimpleDateFormat(
  "dd-MMMM-yyyy HH:mm:ss:SSS");
SimpleDateFormat shortMonthsimpleDateFormat = new SimpleDateFormat(
  "dd-MM-yyyy HH:mm:ss:SSS");

assertEquals(
  "01-lutego-2018 10:15:20:000", fullMonthDateFormat.format(date));
assertEquals(
  "01-02-2018 10:15:20:000" , shortMonthsimpleDateFormat.format(date));

5. Anpassung

Aufgrund einiger guter Entwurfsentscheidungen sind wir nicht an ein länderspezifisches Formatierungsmuster gebunden, und wir können fast jedes Detail so konfigurieren, dass es mit einer Ausgabe vollständig zufrieden ist.

Zum Anpassen der Zahlenformatierung können wirDecimalFormat undDecimalFormatSymbols verwenden.

Betrachten wir ein kurzes Beispiel:

Locale.setDefault(Locale.FRANCE);
BigDecimal number = new BigDecimal(102_300.456d);

DecimalFormat zeroDecimalFormat = new DecimalFormat("000000000.0000");
DecimalFormat dollarDecimalFormat = new DecimalFormat("$###,###.##");

assertEquals(zeroDecimalFormat.format(number), "000102300,4560");
assertEquals(dollarDecimalFormat.format(number), "$102 300,46");

The DecimalFormat documentation zeigt alle möglichen Musterzeichen. Jetzt müssen wir nur noch wissen, dass "000000000.000" führende oder nachfolgende Nullen bestimmt, "," ein Tausendertrennzeichen und "." ist eine Dezimalzahl.

Es ist auch möglich, ein Währungssymbol hinzuzufügen. Wir können unten sehen, dass das gleiche Ergebnis mit der KlasseDateFormatSymbolerzielt werden kann:

Locale.setDefault(Locale.FRANCE);
BigDecimal number = new BigDecimal(102_300.456d);

DecimalFormatSymbols decimalFormatSymbols = DecimalFormatSymbols.getInstance();
decimalFormatSymbols.setGroupingSeparator('^');
decimalFormatSymbols.setDecimalSeparator('@');
DecimalFormat separatorsDecimalFormat = new DecimalFormat("$###,###.##");
separatorsDecimalFormat.setGroupingSize(4);
separatorsDecimalFormat.setCurrency(Currency.getInstance(Locale.JAPAN));
separatorsDecimalFormat.setDecimalFormatSymbols(decimalFormatSymbols);

assertEquals(separatorsDecimalFormat.format(number), "$10^[email protected]");

Wie wir sehen können, können wir mit der KlasseDecimalFormatSymbolsjede erdenkliche Zahlenformatierung angeben.

To customize SimpleDataFormat, we can use DateFormatSymbols.

Mal sehen, wie einfach eine Änderung der Tagesnamen ist:

Date date = new GregorianCalendar(2018, 1, 1, 10, 15, 20).getTime();
Locale.setDefault(new Locale("pl", "PL"));

DateFormatSymbols dateFormatSymbols = new DateFormatSymbols();
dateFormatSymbols.setWeekdays(new String[]{"A", "B", "C", "D", "E", "F", "G", "H"});
SimpleDateFormat newDaysDateFormat = new SimpleDateFormat(
  "EEEE-MMMM-yyyy HH:mm:ss:SSS", dateFormatSymbols);

assertEquals("F-lutego-2018 10:15:20:000", newDaysDateFormat.format(date));

6. Ressourcenpakete

Schließlich ist der entscheidende Teil der Internationalisierung inJVM derResource Bundle-Mechanismus.

Der Zweck vonResourceBundle besteht darin, einer Anwendung lokalisierte Nachrichten / Beschreibungen bereitzustellen, die an die separaten Dateien ausgelagert werden können. Wir behandeln die Verwendung und Konfiguration des Ressourcenpakets in einem unserer vorherigen Artikel -guide to the Resource Bundle.

7. Fazit

Locales und die Formatierer, die sie verwenden, sind Tools, mit denen wir eine internationalisierte Anwendung erstellen können. Mit diesen Tools können wir eine Anwendung erstellen, die sich dynamisch an die sprachlichen oder kulturellen Einstellungen des Benutzers anpassen kann, ohne dass mehrere Builds erforderlich sind oder sich Gedanken darüber machen müssen, ob Java dieLocaleunterstützt.

In einer Welt, in der ein Benutzer überall sein und jede Sprache sprechen kann, bedeutet die Fähigkeit, diese Änderungen anzuwenden, dass unsere Anwendungen für mehr Benutzer weltweit intuitiver und verständlicher sind.

Wenn Sie mit Spring Boot-Anwendungen arbeiten, haben wir auch einen praktischen Artikel fürSpring Boot Internationalization.

Der Quellcode dieses Tutorials mit vollständigen Beispielen befindet sich inover on GitHub.