Internationalisierung und Lokalisierung in Java 8

1. Übersicht

Bei der Internationalisierung wird ein Antrag zur Unterstützung verschiedener sprachlicher, regionaler, kultureller oder politisch spezifischer Daten vorbereitet. Es ist ein wesentlicher Aspekt jeder modernen mehrsprachigen Anwendung.

Für die weitere Lektüre 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 zum Beispiel auf länderspezifische Zahlen. Sie haben verschiedene Dezimal- und Tausendertrennzeichen:

  • 102.300,45 (Vereinigte Staaten)
  • 102 300,45 (Polen)
  • 102.300,45 (Deutschland)

Es gibt auch verschiedene Datumsformate:

  • Montag, 1. Januar 2018, 15:20:34 Uhr MEZ (USA)
  • lundi 1 janvier 2018 15 h 20 CET (Frankreich).
  • 2018 年 1 月 1 日 下午 03 时 20 分 34 秒 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 USD (USA)

Eine wichtige Tatsache ist, 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, die Locale- Klasse.

Dadurch können wir schnell zwischen kulturellen Orten unterscheiden und unsere Inhalte entsprechend formatieren. Es ist wichtig für den Internationalisierungsprozess. Wie i18n hat auch Localization die Abkürzung - l10n .

Der Hauptgrund für die Verwendung von Locale ist, 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 (deutsch)
  • it_CH (Italienisch, Schweiz)
  • en_US_UNIX (Vereinigte Staaten, UNIX-Plattform)

2.1. Felder

Wir haben bereits erfahren, dass das Gebietsschema aus Sprachcode, Ländercode und Variante besteht. Es können zwei weitere Felder festgelegt werden: Skript und Erweiterungen .

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

  • Die Sprache kann ein ISO 639 Alpha-2- oder Alpha-3- Code oder ein Subtag mit registrierter Sprache sein.
  • Region (Land) ist die ISO 3166 Alpha-2- Landesvorwahl oder die numerische UN-3- Vorwahl.
  • Variante ist ein Wert, bei dem zwischen Groß- und Kleinschreibung unterschieden wird, oder eine Reihe von Werten, die eine Variation eines Gebietsschemas angeben .
  • Das Skript muss ein gültiger ISO 15924 Alpha-4- Code sein.
  • Extensions ist eine Map, die aus einzelnen Zeichenschlüsseln und String- Werten besteht.

Die IANA Language Subtag Registry enthält mögliche Werte für Sprache , Region , Variante und Skript .

Es gibt keine Liste möglicher Erweiterungswerte , aber die Werte müssen wohlgeformte BCP-47- Untertags sein. Die Schlüssel und Werte werden immer in Kleinbuchstaben umgewandelt.

2.2. Locale.Builder

Es gibt verschiedene Möglichkeiten, Gebietsschemaobjekte zu erstellen . Ein möglicher Weg ist die Verwendung von Locale.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();

Die String - Darstellung der oben Locale ist fr_CA_POSIX_ # Latn .

Es ist gut zu wissen, dass das Einstellen von "Variante" etwas schwierig sein kann, da es keine offizielle Einschränkung für Variantenwerte gibt, obwohl die Setter-Methode erfordert, dass sie BCP-47- konform ist .

Andernfalls wird eine IllformedLocaleException ausgelöst .

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

2.3. Konstruktoren

Das Gebietsschema 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");

Eine gültige Variante muss eine Zeichenfolge mit 5 bis 8 alphanumerischen Zeichen oder eine einzelne Zahl gefolgt von 3 alphanumerischen Zeichen sein. Wir können "UNIX" nur über den Konstruktor auf das Variantenfeld anwenden, da es diese Anforderungen nicht erfüllt.

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

2.4. Constants

This is probably the simplest and the most limited way of getting Locales. The Locale class has several static constants which represent the most popular country or language:

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

2.5. Language Tags

Another way of creating Locale is calling the static factory method forLanguageTag(String languageTag). This method requires a String that meets the IETF BCP 47 standard.

This is how we can create the UK Locale:

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

2.6. Available Locales

Even though we can create multiple combinations of Locale objects, we may not be able to use them.

An important note to be aware of is that the Locales on a platform are dependent on those that have been installed within the Java Runtime.

As we use Locales for formatting, the different formatters may have an even smaller set of Locales available that are installed in the Runtime.

Let's check how to retrieve arrays of available locales:

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

After that, we can check whether our Locale resides among available Locales.

We should remember that the set of available locales is different for various implementations of the Java Platformand various areas of functionality.

The complete list of supported locales is available on the Oracle's Java SE Development Kit webpage.

2.7. Default Locale

While working with localization, we might need to know what the default Locale on our JVM instance is. Fortunately, there's a simple way to do that:

Locale defaultLocale = Locale.getDefault();

Also, we can specify a default Locale by calling a similar setter method:

Locale.setDefault(Locale.CANADA_FRENCH);

It's especially relevant when we'd like to create JUnit tests that don't depend on a JVM instance.

3. Numbers and Currencies

This section refers to numbers and currencies formatters that should conform to different locale-specific conventions.

To format primitive number types (int, double) as well as their object equivalents (Integer, Double), we should use NumberFormat class and its static factory methods.

Two methods are interesting for us:

  • NumberFormat.getInstance(Locale locale)
  • NumberFormat.getCurrencyInstance(Locale locale)

Let's examine a sample code:

Locale usLocale = Locale.US; double number = 102300.456d; NumberFormat usNumberFormat = NumberFormat.getInstance(usLocale); assertEquals(usNumberFormat.format(number), "102,300.456");

As we can see it's as simple as creating Locale and using it to retrieve NumberFormat instance and formatting a sample number. We can notice that the output includes locale-specific decimal and thousand separators.

Here's another example:

Locale usLocale = Locale.US; BigDecimal number = new BigDecimal(102_300.456d); NumberFormat usNumberFormat = NumberFormat.getCurrencyInstance(usLocale); assertEquals(usNumberFormat.format(number), "$102,300.46");

Formatting a currency involves the same steps as formatting a number. The only difference is that the formatter appends currency symbol and round decimal part to two digits.

4. Date and Time

Now, we're going to learn about dates and times formatting which's probably more complex than formatting numbers.

First of all, we should know that date and time formatting significantly changed in Java 8 as it contains completely new Date/Time API. Therefore, we're going to look through different formatter classes.

4.1. DateTimeFormatter

Since Java 8 was introduced, the main class for localizing of dates and times is the DateTimeFormatter class. It operates on classes that implement TemporalAccessor interface, for example, LocalDateTime, LocalDate, LocalTime or ZonedDateTime. To create a DateTimeFormatter we must provide at least a pattern, and then Locale. Let's see an example code:

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));

We can see that after retrieving DateTimeFormatter all we have to do is to call the format() method.

For a better understanding, we should familiarize with possible pattern letters.

Let's look at letters for example:

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

All possible pattern letters with explanation can be found in the Java documentation of DateTimeFormatter.It's worth to know that final value depends on the number of symbols. There is ‘MMMM' in the example which prints the full month name whereas a single ‘M' letter would give the month number without a leading 0.

To finish on DateTimeFormatter, let's see how we can format LocalizedDateTime:

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);

In order to format LocalizedDateTime, we can use the ofLocalizedDateTime(FormatStyle dateTimeStyle) method and provide a predefined FormatStyle.

For a more in-depth look at Java 8 Date/Time API, we have an existing article here.

4.2. DateFormat and SimpleDateFormatter

As it's still common to work on projects that make use of Dates and Calendars, we'll briefly introduce capabilities of formatting dates and times with DateFormat and SimpleDateFormat classes.

Let's analyze abilities of the first one:

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 works with Dates and has three useful methods:

  • getDateTimeInstance
  • getDateInstance
  • getTimeInstance

All of them take predefined values of DateFormat as a parameter. Each method is overloaded, so passing Locale is possible as well. If we want to use a custom pattern, as it's done in DateTimeFormatter, we can use SimpleDateFormat. Let's see a short code snippet:

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. Customization

Due to some good design decisions, we're not tied to a locale-specific formatting pattern, and we can configure almost every detail to be fully satisfied with an output.

To customize number formatting, we can use DecimalFormat and DecimalFormatSymbols.

Let's consider a short example:

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 shows all possible pattern characters. All we need to know now is that “000000000.000” determines leading or trailing zeros, ‘,' is a thousand separator, and ‘.' is decimal one.

It's also possible to add a currency symbol. We can see below that the same result can be achieved by using DateFormatSymbol class:

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]");

As we can see, DecimalFormatSymbols class enables us to specify any number formatting we can imagine.

To customize SimpleDataFormat, we can use DateFormatSymbols.

Let's see how simple is a change of day names:

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. Resource Bundles

Finally, the crucial part of internationalization in the JVM is the Resource Bundle mechanism.

The purpose of a ResourceBundle is to provide an application with localized messages/descriptions which can be externalized to the separate files. We cover usage and configuration of the Resource Bundle in one of our previous articles – guide to the Resource Bundle.

7. Conclusion

Locales and the formatters that utilize them are tools that help us create an internationalized application. These tools allow us to create an application which can dynamically adapt to the user's linguistic or cultural settings without multiple builds or even needing to worry about whether Java supports the Locale.

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

Wenn Sie mit Spring Boot-Anwendungen arbeiten, finden Sie auch einen praktischen Artikel zur Spring Boot-Internationalisierung.

Der Quellcode dieses Tutorials mit vollständigen Beispielen finden Sie auf GitHub.