Интернационализация и локализация в Java 8

Интернационализация и локализация в Java 8

1. обзор

Internationalization is a process of preparing an application to support various linguistic, regional, cultural or political-specific data. Это важный аспект любого современного многоязычного приложения.

Для дальнейшего чтения, мы должны знать, что существует очень популярное сокращение (вероятно, более популярное, чем фактическое название) для интернационализации -i18n из-за 18 букв между «i» и «n».

Для современных корпоративных программ крайне важно служить людям из разных частей мира или из разных культурных областей. Отдельные культурные или языковые регионы определяют не только языковые описания, но также валюту, числовое представление и даже различный состав даты и времени.

Например, давайте сосредоточимся на цифрах по странам. Они имеют различные десятичные и тысячи разделителей:

  • 102 300,45 (Соединенные Штаты)

  • 102 300,45 (Польша)

  • 102.300,45 (Germany)

Также есть разные форматы даты:

  • Понедельник, 1 января 2018 года 15:20:34 CET (Соединенные Штаты)

  • Лунди 1 января 2018 г. 15 ч. 20 мин. (Франция).

  • 2018 年 1 月 1 日 星期一 下午 03 时 20 分 34 秒 CET (Китай)

Более того, разные страны имеют уникальные символы валюты:

  • £ 1200,60 (Великобритания)

  • € 1.200,60 (Италия)

  • 1 200,60 € (Франция)

  • $1,200.60 (United States)

Важно знать, что даже если страны имеют одинаковую валюту и символ валюты - как Франция и Италия - положение их символа валюты может быть другим.

2. локализация

В Java у нас есть фантастическая функция, называемая классомLocale.

Это позволяет нам быстро различать культурные локали и форматировать наш контент соответствующим образом. Это важно для процесса интернационализации. Как и у i18n, у Localization также есть аббревиатура -*l10n*.

Основная причина использованияLocale заключается в том, что все необходимое форматирование для конкретной локали может быть доступно без перекомпиляции. Приложение может обрабатывать несколько локалей одновременно, поэтому поддержка нового языка проста.

Локали обычно представлены языком, страной и вариантом аббревиатуры, разделенными подчеркиванием:

  • де (немецкий)

  • it_CH (итальянский, Швейцария)

  • en_US_UNIX (США, платформа UNIX)

2.1. поля

Мы уже узнали, чтоLocale consists of language code, country code, and variant. There are two more possible fields to set: script and extensions.

Давайте посмотрим на список полей и посмотрим, каковы правила:

  • Language может быть кодомISO 639 alpha-2 or alpha-3 или подтегом зарегистрированного языка.

  • Region (Страна) - это код страныISO 3166 alpha-2 или код городаUN numeric-3.

  • Variant - это чувствительное к регистру значение или набор значений, определяющих вариантLocale.

  • Script должен быть допустимым кодомISO 15924 alpha-4.

  • Extensions - это карта, состоящая из односимвольных ключей и значенийString.

IANA Language Subtag Registry содержит возможные значения дляlanguage,region,variant иscript.

Нет списка возможных значенийextension, но значения должны быть правильно сформированными вложенными тегамиBCP-47. Ключи и значения всегда преобразуются в нижний регистр.

2.2. Locale.Builderс

Есть несколько способов создания объектовLocale. Один из возможных способов используетLocale.Builder. Locale.Builder имеет пять методов установки, которые мы можем использовать для создания объекта и в то же время для проверки этих значений:

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

String представляет собойLocale какfr_CA_POSIX # Latn_.

Приятно знать, чтоsetting ‘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.

В противном случае он выброситIllformedLocaleException.

В случае, когда нам нужно использовать значение, не прошедшее проверку, мы можем использовать конструкторыLocale, поскольку они не проверяют значения.

2.3. Конструкторы

Locale имеет три конструктора:

  • новый языковой стандарт (язык строк)

  • новый языковой стандарт (язык строки, страна строки)

  • новый языковой стандарт (язык строки, страна строки, вариант строки)

Трехпараметрический конструктор:

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

Допустимыйvariant должен бытьString, состоящим из 5-8 буквенно-цифровых или одиночных цифр, за которыми следуют 3 буквенно-цифровых символа. Мы можем применить UNIX к полюvariant только через конструктор, поскольку он не соответствует этим требованиям.

Однако есть один недостаток использования конструкторов для создания объектовLocale - мы не можем задавать расширения и поля скриптов.

2.4. Константы

Это, вероятно, самый простой и самый ограниченный способ получитьLocales. КлассLocale имеет несколько статических констант, которые представляют наиболее популярную страну или язык:

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

2.5. Языковые теги

Другой способ созданияLocale - это вызов статического фабричного методаforLanguageTag(String languageTag). Для этого метода требуетсяString, который соответствует стандартуIETF BCP 47.

Вот как мы можем создать UKLocale:

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

2.6. Доступные регионы

Хотя мы можем создать несколько комбинаций объектовLocale, мы не сможем их использовать.

Важно помнить, чтоLocalesна платформе зависят от тех, которые были установлены в среде выполнения Java.

Поскольку мы используемLocales для форматирования, различные форматирующие программы могут иметь еще меньший набор доступныхLocales, установленных в среде выполнения.

Давайте посмотрим, как получить массивы доступных языков:

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

После этого мы можем проверить, находится ли нашLocale среди доступныхLocales.

Следует помнить, чтоthe set of available locales is different for various implementations of the Java Platformand various areas of functionality.

Полный список поддерживаемых локалей доступен наthe Oracle’s Java SE Development Kit webpage.

2.7. Локаль по умолчанию

При работе с локализацией нам может потребоваться узнать, какой по умолчаниюLocale в нашем экземпляреJVM. К счастью, есть простой способ сделать это:

Locale defaultLocale = Locale.getDefault();

Кроме того, мы можем указать значение по умолчаниюLocale, вызвав аналогичный метод установки:

Locale.setDefault(Locale.CANADA_FRENCH);

Это особенно актуально, когда мы хотим создать тестыJUnit, которые не зависят от экземпляраJVM.

3. Числа и валюты

Этот раздел относится к средствам форматирования чисел и валют, которые должны соответствовать различным региональным соглашениям.

Для форматирования примитивных числовых типов (int,double), а также их объектных эквивалентов (Integer,Double) мы должны использовать классNumberFormat и его статические фабричные методы.

Для нас интересны два метода:

  • NumberFormat.getInstance(Locale locale)

  • NumberFormat.getCurrencyInstance(Locale locale)

Давайте рассмотрим пример кода:

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

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

Как мы видим, это так же просто, как создатьLocale и использовать его для получения экземпляраNumberFormat и форматирования номера образца. Мы можем заметить, чтоthe output includes locale-specific decimal and thousand separators.

Вот еще один пример:

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

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

Форматирование валюты включает те же шаги, что и форматирование числа. Разница лишь в том, что средство форматирования добавляет символ валюты и круглую десятичную часть к двум цифрам.

4. Дата и время

Теперь мы собираемся узнать о форматировании даты и времени, которое, вероятно, более сложно, чем форматирование чисел.

Прежде всего, мы должны знать, что форматирование даты и времени значительно изменилось в Java 8, поскольку он содержит совершенно новый APIDate/Time. Поэтому мы рассмотрим разные классы форматирования.

4.1. DateTimeFormatterс

Since Java 8 was introduced, the main class for localizing of dates and times is the DateTimeFormatter class. Он работает с классами, реализующими интерфейсTemporalAccessor, например,LocalDateTime,LocalDate, LocalTime илиZonedDateTime. . Чтобы создатьDateTimeFormatter, мы должны предоставить хотя бы шаблон, а затем Locale. Посмотрим пример кода:

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

Мы видим, что после полученияDateTimeFormatter все, что нам нужно сделать, это вызвать методformat().

Для лучшего понимания нам следует ознакомиться с возможными образцами букв.

Рассмотрим, например, буквы:

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

Все возможные шаблоны букв с пояснениями можно найти вhttps://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. В примере есть «ММММ», которое печатает полное название месяца, в то время как одна буква «М» даст номер месяца без начального 0.

Чтобы закончитьDateTimeFormatter, давайте посмотрим, как мы можем отформатировать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);

Чтобы отформатироватьLocalizedDateTime, мы можем использовать методofLocalizedDateTime(FormatStyle dateTimeStyle) и предоставить предопределенныйFormatStyle.

Для более глубокого изучения API Java 8Date/Time у нас есть существующая статьяhere.

4.2. DateFormat иSimpleDateFormatter

Поскольку по-прежнему часто приходится работать над проектами, в которых используютсяDates иCalendars, мы кратко представим возможности форматирования даты и времени с помощью классовDateFormat иSimpleDateFormat.

Проанализируем возможности первого:

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 работает сDates и имеет три полезных метода:

  • getDateTimeInstance

  • getDateInstance

  • getTimeInstance

Все они принимают в качестве параметра предопределенные значенияDateFormat. Каждый метод перегружен, поэтому передачаLocale также возможна. Если мы хотим использовать собственный шаблон, как это сделано вDateTimeFormatter, мы можем использоватьSimpleDateFormat. Давайте посмотрим на короткий фрагмент кода:

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. настройка

Из-за некоторых хороших дизайнерских решений мы не привязаны к шаблону форматирования, зависящему от локали, и можем настроить почти каждую деталь, чтобы полностью удовлетворить результат.

Чтобы настроить форматирование чисел, мы можем использоватьDecimalFormat иDecimalFormatSymbols.

Давайте рассмотрим небольшой пример:

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 показывает все возможные символы шаблона. Все, что нам нужно знать сейчас, это то, что «000000000.000» определяет начальные или конечные нули, ‘,« это разделитель тысяч, а ‘». десятичный.

Также можно добавить символ валюты. Ниже мы видим, что того же результата можно достичь, используя классDateFormatSymbol:

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

Как мы видим, классDecimalFormatSymbols позволяет нам указать любое форматирование чисел, которое мы можем представить.

To customize SimpleDataFormat, we can use DateFormatSymbols.

Давайте посмотрим, насколько просто изменить название дня:

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. Ресурсы

Наконец, важнейшей частью интернационализации вJVM является механизмResource Bundle.

ЦельResourceBundle - предоставить приложению локализованные сообщения / описания, которые могут быть перенесены в отдельные файлы. Мы рассмотрим использование и настройку Resource Bundle в одной из наших предыдущих статей -guide to the Resource Bundle.

7. Заключение

Locales и использующие их средства форматирования - это инструменты, которые помогают нам создавать интернационализированное приложение. Эти инструменты позволяют нам создавать приложение, которое может динамически адаптироваться к языковым или культурным особенностям пользователя без многократных сборок и даже без необходимости беспокоиться о том, поддерживает ли JavaLocale.

В мире, где пользователь может находиться где угодно и говорить на любом языке, способность применять эти изменения означает, что наши приложения могут быть более интуитивными и понятными для большего числа пользователей во всем мире.

При работе с приложениями Spring Boot у нас также есть удобная статья дляSpring Boot Internationalization.

Исходный код этого руководства с полными примерами можно найти вover on GitHub.