Локалізація Java - Форматування повідомлень

1. Вступ

У цьому підручнику ми розглянемо, як ми можемо локалізувати та форматувати повідомлення на основі Locale .

Ми будемо використовувати як MessageFormat Java, так і сторонні бібліотеки 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 .

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

Кожен файл повинен містити пари ключ-значення із повідомленнями відповідною мовою. Наприклад, файл 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");

Значенням змінної повідомлення буде "Аліса надіслала вам повідомлення".

Клас локальної мови Java містить ярлики для часто використовуваних мов та країн.

У випадку польської мови ми можемо написати наступне:

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}

The placeholder's index corresponds to the position of an element from the array of objects that we want to insert.

When present, the type and style may take the following values:

type style
number integer, currency, percent, custom format
date short, medium, long, full, custom format
time short, medium, long, full, custom format
choice custom format

The names of the types and styles largely speak for themselves, but we can consult the official documentation for more details.

Let's take a closer look, though, at custom format.

In the example above, we used the following format expression:

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

In general, the choice style has the form of options separated by the vertical bar (or pipe):

Inside the options, the match value ki and the string vi are separated by # except for the last option. Notice that we may nest other patterns into the string vi as we did it for the last option:

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

The choice type is a numeric-based one, so there is a natural ordering for the match values ki that split a numeric line into intervals:

If we give a value k that belongs to the interval [ki, ki+1) (the left end is included, the right one is excluded), then value vi is selected.

Let's consider in more details the ranges of the chosen style. To this end, we take this pattern:

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

and pass various values for its unique placeholder:

n message
-1, 0, 0.5 You've got no messages.
1, 1.5 You've got a message.
2 You've got two messages.
2.5 You've got 2 messages.
5 You've got 5 messages.

4.3. Making Things Better

So, we're now formatting our messages. But, the message itself remains hardcoded.

From the previous section, we know that we should extract the strings patterns to the resources. To separate our concerns, let's create another bunch of resource files called formats:

In those, we'll create a key called label with language-specific content.

For example, in the English version, we'll put the following string:

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

We should slightly modify the French version because of the zero message case:

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

And we'd need to do similar modifications as well in the Polish and Italian versions.

In fact, the Polish version exhibits yet another problem. According to the grammar of the Polish language (and many others), the verb has to agree in gender with the subject. We could resolve this problem by using the choice type, but let's consider another solution.

4.4. ICU's MessageFormat

Let's use the International Components for Unicode (ICU) library. We have already mentioned it in our Convert a String to Title Case tutorial. It's a mature and widely-used solution that allows us to customize the application for various languages.

Here, we're not going to explore it in full details. We'll just limit ourselves to what our toy application needs. For the most comprehensive and updated information, we should check the ICU's official site.

At the time of writing, the latest version of ICU for Java (ICU4J) is 64.2. As usual, in order to start using it, we should add it as a dependency to our project:

 com.ibm.icu icu4j 64.2 

Suppose that we want to have a properly formed notification in various languages and for different numbers of messages:

N English Polish
0 Alice has sent you no messages.

Bob has sent you no messages.

Alice nie wysłała ci żadnej wiadomości.

Bob nie wysłał ci żadnej wiadomości.

1 Alice has sent you a message.

Bob has sent you a message.

Alice wysłała ci wiadomość.

Bob wysłał ci wiadomość.

> 1 Alice has sent you N messages.

Bob has sent you N messages.

Alice wysłała ci N wiadomości.

Боб wysłał ci N wiadomości.

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

Давайте повторно використаємо файл formatats.properties і додамо туди ключ label-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.