Перейти к основному содержимому

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

· 10 мин. чтения

1. Обзор

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

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

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

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

  • 102 300,45 (США)
  • 102 300,45 (Польша)
  • 102.300,45 (Германия)

Существуют также разные форматы даты:

  • Понедельник, 1 января 2018 г., 15:20:34 CET (США)
  • 1 января 2018 г., 15:20 CET (Франция).
  • 2018年1月1日星期一 下午03时20分34秒CET (Китай)

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

  • 1200,60 фунтов стерлингов (Соединенное Королевство)
  • € 1.200,60 (Италия)
  • 1 200,60 € (Франция)
  • 1200,60 долларов США (США)

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

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

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

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

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

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

  • де (немецкий)
  • it_CH (итальянский, Швейцария)
  • en_US_UNIX (США, платформа UNIX)

2.1. Поля

Мы уже узнали, что Locale состоит из кода языка, кода страны и варианта. Есть еще два возможных поля для установки: script и extensions .

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

  • Язык может быть кодом ISO 639 alpha-2 или alpha-3 или зарегистрированным вложенным тегом языка.
  • Регион (страна) — это код страны ISO 3166 alpha-2 или код зоны UN numeric-3 .
  • Variant — это чувствительное к регистру значение или набор значений, указывающих вариант Locale .
  • Сценарий должен быть действительным кодом ISO 15924 alpha-4 .
  • Расширения — это карта, состоящая из односимвольных ключей и строковых значений.

Реестр языковых вложенных тегов IANA содержит возможные значения для языка , региона , варианта и сценария .

Не существует списка возможных значений расширения , но значения должны быть правильно сформированными вложенными тегами 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();

Строковое представление вышеуказанной локали — frCA_POSIX #Latn .

Полезно знать, что установка «варианта» может быть немного сложной, поскольку нет официальных ограничений на значения вариантов, хотя метод установки требует, чтобы он был совместим с BCP-47 .

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

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

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

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

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

Конструктор с 3 параметрами:

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

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

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

2.4. Константы

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

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

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

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

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

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

2.6. Доступные локали

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

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

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

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

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

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

Следует помнить, что набор доступных локалей различен для разных реализаций платформы Java и разных областей функциональности .

Полный список поддерживаемых локалей доступен на веб-странице Oracle Java SE Development Kit.

2.7. Язык по умолчанию

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

Locale defaultLocale = Locale.getDefault();

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

Locale.setDefault(Locale.CANADA_FRENCH);

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

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

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

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

Нас интересуют два метода:

  • NumberFormat.getInstance (локаль)
  • NumberFormat.getCurrencyInstance (локаль)

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

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

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

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

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

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, поскольку он содержит совершенно новый API даты/времени . Поэтому мы собираемся просмотреть различные классы форматирования.

4.1. ДатаВремяФорматтер

С момента появления Java 8 основным классом для локализации дат и времени является класс DateTimeFormatter . Он работает с классами, которые реализуют интерфейс 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

Все возможные буквы шаблона с объяснением можно найти в документации по Java DateTimeFormatter . Стоит знать, что итоговое значение зависит от количества символов . В примере есть «MMMM», который печатает полное название месяца, тогда как одна буква «M» дает номер месяца без начального 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.

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

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

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

Документация DecimalFormat показывает все возможные символы шаблона. Все, что нам нужно знать сейчас, это то, что «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^2300@46");

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

Чтобы настроить SimpleDataFormat, мы можем использовать 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 в одной из наших предыдущих статей — руководство по Resource Bundle .

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

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

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

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

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