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

Локализация Java — форматирование сообщений

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

1. Введение

В этом руководстве мы рассмотрим, как мы можем локализовать и форматировать сообщения на основе Locale .

Мы будем использовать как Java MessageFormat , так и стороннюю библиотеку ICU.

2. Пример использования локализации

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

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

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

Простым примером такого сообщения может быть следующее:

Alice has sent you a message.

Это нормально для англоязычных пользователей, но неанглоговорящие могут быть не очень довольны. Например, франкоговорящие пользователи предпочли бы видеть это сообщение:

Alice vous a envoyé un message.

В то время как поляки были бы довольны, увидев это:

Alice wysłała ci wiadomość.

Что, если мы хотим иметь правильно оформленное уведомление даже в том случае, когда Алиса отправляет не одно сообщение, а несколько сообщений?

У нас может возникнуть соблазн решить проблему, объединив различные части в одну строку, например:

String message = "Alice has sent " + quantity + " messages";

Ситуация может легко выйти из-под контроля, когда нам нужны уведомления в случае, когда не только Алиса, но и Боб могут отправлять сообщения:

Bob has sent two messages.
Bob a envoyé deux messages.
Bob wysłał dwie wiadomości.

Обратите внимание, как глагол меняется в случае польского ( wysłała vs wysłał ) языка. Это иллюстрирует тот факт, что банальная конкатенация строк редко бывает приемлемой для локализации сообщений .

Как мы видим, мы получаем два типа вопросов: один связан с переводами, а другой связан с форматами . Давайте рассмотрим их в следующих разделах.

3. Локализация сообщений

Мы можем определить локализацию, или l10n , приложения как процесс адаптации приложения к удобству пользователя . Иногда также используется термин интернализация, или i18n .

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

./6f6572251f47b16770e526c24791b4b2.png

Каждый файл должен содержать пары ключ-значение с сообщениями на соответствующем языке. Например, файл messages_en.properties должен содержать следующую пару:

label=Alice has sent you a message.

messages_pl.properties должен содержать следующую пару:

label=Alice wysłała ci wiadomość.

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

ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.UK);
String message = bundle.getString("label");

Значением переменной message будет «Алиса отправила вам сообщение».

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

В случае с польским языком мы могли бы написать следующее:

ResourceBundle bundle
= ResourceBundle.getBundle("messages", Locale.forLanguageTag("pl-PL"));
String message = bundle.getString("label");

Отметим, что если мы не укажем локаль, система будет использовать локаль по умолчанию. Подробнее об этом можно узнать в нашей статье « Интернационализация и локализация в Java 8 ». Затем среди доступных переводов система выберет тот, который наиболее похож на активную в данный момент локаль.

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

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

4. Формат сообщения

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

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

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

4.1. Формат сообщения Java ``

Для форматирования строк Java определяет многочисленные методы форматирования в java.lang.String . Но мы можем получить еще большую поддержку через java.text.format.MessageFormat .

Чтобы проиллюстрировать это, давайте создадим шаблон и скормим его экземпляру MessageFormat :

String pattern = "On {0, date}, {1} sent you "
+ "{2, choice, 0#no messages|1#a message|2#two messages|2<{2, number, integer} messages}.";
MessageFormat formatter = new MessageFormat(pattern, Locale.UK);

В строке шаблона есть слоты для трех заполнителей.

Если мы предоставим каждое значение:

String message = formatter.format(new Object[] {date, "Alice", 2});

Затем MessageFormat заполнит шаблон и отобразит наше сообщение:

On 27-Apr-2019, Alice sent you two messages.

4.2. Синтаксис формата сообщения

Из приведенного выше примера мы видим, что шаблон сообщения:

pattern = "On {...}, {..} sent you {...}.";

содержит заполнители, которые представляют собой фигурные скобки {…} с обязательным индексом аргумента и двумя необязательными аргументами, типом и стилем :

{index}
{index, type}
{index, type, style}

Индекс заполнителя соответствует позиции элемента из массива объектов, который мы хотим вставить.

При наличии тип и стиль могут принимать следующие значения:

   | тип    | стиль   | 
| `количество` | `целое число, валюта, процент, пользовательский формат` |
| `свидание` | `короткий, средний, длинный, полный, произвольный формат` |
| `время` | `короткий, средний, длинный, полный, произвольный формат` |
| `выбор` | `пользовательский формат` |

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

Однако давайте подробнее рассмотрим пользовательский формат .

В приведенном выше примере мы использовали следующее выражение формата:

{2, choice, 0#no messages|1#a message|2#two messages|2<{2, number, integer} messages}

Как правило, стиль выбора имеет вид вариантов, разделенных вертикальной чертой (или вертикальной чертой):

./6b7713f9e28a91f65acc88b0c43b2ce4.png

Внутри опций значение совпадения k i и строка v i разделены символом #, за исключением последней опции. Обратите внимание, что мы можем вкладывать другие шаблоны в строку v i , как мы сделали это для последнего варианта:

{2, choice, ...|2<{2, number, integer} messages}

Тип выбора является числовым , поэтому существует естественный порядок значений соответствия k i , которые разбивают числовую строку на интервалы:

./02581b5cfe69f62984ee6d9326173640.png

Если задать значение k , принадлежащее интервалу [k i , k i+1 ) (левый конец включен, правый исключен), то выбирается значение v i .

Рассмотрим подробнее диапазоны выбранного стиля. Для этого возьмем вот такую выкройку:

pattern = "You''ve got "
+ "{0, choice, 0#no messages|1#a message|2#two messages|2<{0, number, integer} messages}.";

и передавать различные значения для его уникального заполнителя:

   | н    | сообщение   | 
| `-1, 0, 0,5` | `У вас нет сообщений.` |
| `1, 1,5` | `Вам пришло сообщение.` |
| `2` | `У вас есть два сообщения.` |
| `2,5` | `У вас есть 2 сообщения.` |
| `5` | `У вас 5 сообщений.` |

4.3. Делаем вещи лучше

Итак, теперь мы форматируем наши сообщения. Но само сообщение остается жестко запрограммированным.

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

./1ddc4b1735d82d59414a5acfaab7b6c6.png

В них мы создадим ключ label с содержимым для конкретного языка.

Например, в английской версии мы поместим следующую строку:

label=On {0, date, full} {1} has sent you 
+ {2, choice, 0#nothing|1#a message|2#two messages|2<{2,number,integer} messages}.

Мы должны немного изменить французскую версию из-за нулевого регистра сообщений:

label={0, date, short}, {1}{2, choice, 0# ne|0<} vous a envoyé 
+ {2, choice, 0#aucun message|1#un message|2#deux messages|2<{2,number,integer} messages}.

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

На самом деле, польская версия демонстрирует еще одну проблему. Согласно грамматике польского языка (и многих других), глагол должен согласовываться по роду с подлежащим. Мы могли бы решить эту проблему, используя тип выбора, но давайте рассмотрим другое решение.

4.4. Формат сообщения ICU ``

Давайте воспользуемся библиотекой International Components for Unicode (ICU). Мы уже упоминали об этом в нашем учебнике по преобразованию строки в регистр заголовков . Это зрелое и широко используемое решение, которое позволяет нам настраивать приложение для различных языков.

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

На момент написания последняя версия ICU для Java ( ICU4J ) — 64.2. Как обычно, чтобы начать его использовать, мы должны добавить его в качестве зависимости в наш проект:

<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
<version>64.2</version>
</dependency>

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

   | Н    | Английский    | польский   | 
| `0` | `Алиса не отправляла вам сообщений. Боб не посылал вам сообщений.` | `Алиса nie wysłała ci żadnej wiadomości. Bob nie wysłał ci żadnej wiadomości.` |
| `1` | `Алиса отправила вам сообщение. Боб отправил вам сообщение.` | `Алиса wysłała ci wiadomość. Bob wysłał ci wiadomość.` |
| `> 1` | `Алиса отправила вам N сообщений. Боб отправил вам N сообщений.` | `Алиса wysłała ci N wiadomości. Bob wysłał ci N wiadomości.` |

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

Воспользуемся повторно файлом formats.properties и добавим туда метку ключа-icu следующего содержания:

label-icu={0} has sent you
+ {2, plural, =0 {no messages} =1 {a message}
+ other {{2, number, integer} messages}}.

Он содержит три заполнителя, которые мы загружаем, передавая туда трехэлементный массив:

Object[] data = new Object[] { "Alice", "female", 0 }

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

label-icu={0} {2, plural, =0 {nie} other {}}
+ {1, select, male {wysłał} female {wysłała} other {wysłało}}
+ ci {2, plural, =0 {żadnych wiadomości} =1 {wiadomość}
+ other {{2, number, integer} wiadomości}}.

мы используем его, чтобы различать wysłał/wysłała/wysłało .

5. Вывод

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

Как всегда, фрагменты кода для этого туториала находятся в нашем репозитории GitHub .