Internaționalizare și localizare în Java 8

1. Prezentare generală

Internaționalizarea este un proces de pregătire a unei cereri pentru a sprijini diferite date lingvistice, regionale, culturale sau politice specifice. Este un aspect esențial al oricărei aplicații moderne în mai multe limbi.

Pentru lecturi suplimentare , ar trebui să știm că există o abreviere foarte populară (probabil mai populară decât numele real) pentru internaționalizare - i18n datorită celor 18 litere dintre „i” și „n”.

Este esențial ca programele de întreprindere actuale să deservească oameni din diferite părți ale lumii sau din mai multe zone culturale. Regiunile culturale sau lingvistice distincte nu determină doar descrierile specifice limbii, ci și moneda, reprezentarea numărului și chiar compoziția divergentă a datei și orei.

De exemplu, să ne concentrăm pe numere specifice fiecărei țări. Au diverse zecimale și mii de separatori:

  • 102.300,45 (Statele Unite)
  • 102 300,45 (Polonia)
  • 102.300,45 (Germania)

Există și diferite formate de dată:

  • Luni, 1 ianuarie 2018 15:20:34 CET (Statele Unite)
  • luni 1 ianuarie 2018 15 h 20 CET (Franța).
  • 2018 年 1 月 1 日 星期一 下午 03 时 20 分 34 秒 CET (China)

Mai mult, diferite țări au simboluri monetare unice:

  • 1.200,60 GBP (Regatul Unit)
  • 1.200,60 € (Italia)
  • 1 200,60 € (Franța)
  • 1.200,60 USD (Statele Unite)

Un fapt important de știut este că, chiar dacă țările au aceeași monedă și simbolul monedei - cum ar fi Franța și Italia - poziția simbolului monedei lor ar putea fi diferită.

2. Localizare

În Java, avem la dispoziție o caracteristică fantastică numită clasa Locale .

Ne permite să diferențiem rapid între locațiile culturale și să ne formatăm conținutul în mod corespunzător. Este esențial pentru procesul de internaționalizare. La fel ca i18n, Localization are și abrevierea - l10n .

Motivul principal pentru utilizarea Locale este că toate formatările necesare specifice locației pot fi accesate fără recompilare. O aplicație poate gestiona mai multe localizări în același timp, astfel încât acceptarea unei noi limbi este simplă.

Localurile sunt de obicei reprezentate de limbă, țară și abrevierea variantelor separate printr-un subliniat:

  • de (germană)
  • it_CH (italiană, Elveția)
  • en_US_UNIX (Statele Unite, platforma UNIX)

2.1. Câmpuri

Am aflat deja că opțiunea locală constă din cod de limbă, cod de țară și variantă. Există încă două câmpuri posibile de setat: script și extensii .

Să aruncăm o privire printr-o listă de câmpuri și să vedem care sunt regulile:

  • Limba poate fi un cod alfa-2 sau alfa-3 ISO 639 sau o etichetă sub- limbă înregistrată.
  • Regiunea (țara) este codul de țară ISO 3166 alfa-2 sau codul de zonă ONU numeric-3 .
  • Varianta este o valoare sensibilă la majuscule sau la un set de valori care specifică o variație a unei valori locale .
  • Scriptul trebuie să fie un cod alfa-4 ISO 15924 valid .
  • Extensii este o hartă care constă din taste cu un singur caracter și valori String .

Registrul subetichete IANA Language conține valori posibile pentru limbă , regiune , variantă și script .

Nu există o listă de posibile valori de extensie , dar valorile trebuie să fie subetichete BCP-47 bine formate . Tastele și valorile sunt întotdeauna convertite în minuscule.

2.2. Locale.Builder

Există mai multe moduri de a crea obiecte Locale . Un mod posibil utilizează Locale.Builder . Locale.Builder are cinci metode de setare pe care le putem folosi pentru a construi obiectul și în același timp pentru a valida aceste valori:

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

Șir Reprezentarea de mai sus Locale este fr_CA_POSIX_ # Latn .

Este bine de știut că setarea „variantei” poate fi puțin dificilă, deoarece nu există restricții oficiale asupra valorilor variantelor, deși metoda setter necesită conformitatea BCP-47 .

În caz contrar, va arunca IllformedLocaleException .

În cazul în care trebuie să folosim o valoare care nu trece de validare, putem folosi constructori Locale deoarece aceștia nu validează valori.

2.3. Constructori

Locale are trei constructori:

  • Locale noi (limbaj șir)
  • Locale noi (limba șirului, țara șirului)
  • Locale noi (Limba șirului, țara șirului, varianta șirului)

Un constructor cu 3 parametri:

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

O variantă validă trebuie să fie un șir de 5 până la 8 alfanumerice sau un singur numeric urmat de 3 alfanumerice. Putem aplica „UNIX” câmpului de variantă numai prin intermediul constructorului, deoarece nu îndeplinește aceste cerințe.

Cu toate acestea, există un dezavantaj al utilizării constructorilor pentru a crea obiecte Locale - nu putem seta extensii și câmpuri script.

2.4. Constante

Acesta este probabil cel mai simplu și cel mai limitat mod de a obține localuri . Locale clasa are mai multe constante statice , care reprezintă cea mai populară țară sau limbă:

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

2.5. Etichete de limbă

Un alt mod de a crea Locale este apelarea metodei din fabrică statică pentru LanguageTag (String languageTag) . Această metodă necesită un șir care îndeplinește standardul IETF BCP 47 .

Acesta este modul în care putem crea Locale Regatul Unit :

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

2.6. Locale disponibile

Chiar dacă putem crea mai multe combinații de obiecte Locale , este posibil să nu le putem folosi.

O notă importantă de care trebuie să știți este că setările locale de pe o platformă depind de cele care au fost instalate în Java Runtime.

Pe măsură ce folosim Locale pentru formatare, diferiții formatatori pot avea un set și mai mic de Locale disponibile care sunt instalate în Runtime.

Să verificăm cum să extragem matrici de localizări disponibile:

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

După aceea, putem verifica dacă localizarea noastră se află între localurile disponibile .

Ar trebui să ne amintim că setul de localizări disponibile este diferit pentru diferite implementări ale platformei Java și pentru diferite domenii de funcționalitate .

Lista completă a localizărilor acceptate este disponibilă pe pagina web Oracle Java SE Development Kit.

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.

Într-o lume în care un utilizator poate fi oriunde și poate vorbi orice limbă, capacitatea de a aplica aceste modificări înseamnă că aplicațiile noastre pot fi mai intuitive și mai ușor de înțeles de către mai mulți utilizatori la nivel global.

Când lucrăm cu aplicații Spring Boot, avem și un articol convenabil pentru internaționalizarea Spring Boot.

Codul sursă al acestui tutorial, cu exemple complete, poate fi găsit pe GitHub.