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 .