Как переводить сообщения, используя ICU MessageFormat

Дата обновления перевода 2021-09-30

Как переводить сообщения, используя ICU MessageFormat

Сообщения (т.е. строки) в приложениях практически никогда не бывают абсолютно статическими. Они содержат переменные или другую сложную логику вроде плюрализации. Для того, чтобы обработать это, компонент Переводчик поддерживает синтаксис ICU MessageFormat.

Tip

Вы можете протестировать примеры ICU MessageFormatter в этом онлайн-редакторе.

Использование формата сообщений ICU

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

??????? ??? ????? ??? ????? ??????? ????????? ICU
messages.en.yaml messages+intl-icu.en.yaml
messages.fr_FR.xlf messages+intl-icu.fr_FR.xlf
admin.en.yaml admin+intl-icu.en.yaml

Все сообщения в этом файле теперь будут обработаны MessageFormatter во время перевода.

Заполнители сообщений

Базовое использование MessageFormat позволяет вам использовать заполнители (которые называются аргументами в ICU MessageFormat) в ваших сообщениях:

  • YAML
  • XML
  • PHP
1
2
# translations/messages+intl-icu.en.yaml
say_hello: 'Hello {name}!'

Caution

В предыдущем формате перевода, заполнители часто были обернуты в % (например, %name%). Этот символ % больше не валиден с синтаксисом ICU MessageFormat, поэтому вам нужно переименовать ваши параметры, если вы обновляетесь с предыдущего формата.

Все, заключенное в фигурные скобки ({...}) обрабатывается форматироващиком и заменяется заполнителем:

1
2
3
4
5
// выводит "Hello Fabien!"
echo $translator->trans('say_hello', ['name' => 'Fabien']);

// выводит "Hello Symfony!"
echo $translator->trans('say_hello', ['name' => 'Symfony']);

Выбор разных сообщений, в зависимости от состояния

Синтаксис фигурных скобок позволяет "изменять" вывод переменной. Одной из таких функций является функция select. Она действует как PHP-функция switch statement, и позволяет использовать разные строки, в зависимости от значения переменной. Типичное использование этого гендера:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
# translations/messages+intl-icu.en.yaml

# требуется ключ 'other', который выбирается, если не подходит больше ни один случай
invitation_title: >-
    {organizer_gender, select,
        female {{organizer_name} has invited you for her party!}
        male   {{organizer_name} has invited you for his party!}
        other  {{organizer_name} have invited you for their party!}
    }

Это может выглядеть очень сложно. Базовый синтаксис для всех функций - {variable_name, function_name, function_statement} (где, как вы увидите позже, function_statement является необязательным для некоторых функций). В данном случае, имя функции - select, а ее утверждение содержит "случаи" этого выбора. Эта функция применяется поверх переменной organizer_gender:

1
2
3
4
5
6
7
8
9
10
11
// выводит "Ryan has invited you for his party!"
echo $translator->trans('invitation_title', [
    'organizer_name' => 'Ryan',
    'organizer_gender' => 'male',
]);

// выводит "John & Jane have invited you for their party!"
echo $translator->trans('invitation_title', [
    'organizer_name' => 'John & Jane',
    'organizer_gender' => 'not_applicable',
]);

Синтаксис {...} варьируется между режимами "literal" (буквально) и "code" (код). Это позволяет вам использовать буквальный текст в выбранных утверждениях:

  1. Первый блок {organizer_gender, select, ...} запускает режим "code", что означает, чтоorganizer_gender обрабатывается, как переменная.
  2. Внутренний болк {... has invited you for her party!} возвращает вас в режим "literal", что означает, что текст не обрабатывается.
  3. Внутри этого блока, {organizer_name} снова запускает режим "code", позволяя organizer_name быть обработанным, как переменная.

Tip

Хотя может казаться более логичным размещать только her, his или their в переменном утверждении, лучше использовать "сложные аргументы" на краю структуры сообщения. Строки таким образом более читаемы для переводчиков, как вы можете увидеть в случае other, другие части предложения могут быть подвержены влиянию переменных.

Tip

Возможно переводить сообщения ICU MessageFormat прямо в коде, не определяя их ни в каком файле:

1
2
3
4
5
6
7
8
9
10
11
$invitation = '{organizer_gender, select,
    female {{organizer_name} has invited you for her party!}
    male   {{organizer_name} has invited you for his party!}
    other  {{organizer_name} have invited you for their party!}
}';

// выводит "Ryan has invited you for his party!"
echo $translator->trans($invitation, [
    'organizer_name' => 'Ryan',
    'organizer_gender' => 'male',
]);

Плюрализация

Другая интересная функция - plural. Она позволяет вам работать с плюрализацией в ваших сообщениях (например, There are 3 apples и There is one apple). Функция выглядит очень похоже на функцию select:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
# translations/messages+intl-icu.en.yaml
num_of_apples: >-
    {apples, plural,
        =0    {There are no apples}
        one   {There is one apple...}
        other {There are # apples!}
    }

Правила плюрализации на самом деле достаточно сложные и отличваются в каждом языке. Например, русский использует разные множественные окончания для чисел, заканчивающихся на 1; чисел, заканчивающихся на 2, 3 или 4; числе, заканчивающихся на 5, 6, 7, 8 или 9; и кроме этого есть еще и исключения!

Для того, чтобы правильно перевести это, возможные случаи в функции plural, также будут отличаться для каждого языка. К примеру, русский язык будет иметь one, few, many и other, в то время как английский - только one и other. Полный список возможных случаев можно найти в документе Unicode Правила множественных чисел в разных языках. Добавив префикс =, вы можете соответствовать точным значениям (как 0 в примере выше).

Использование этой строки такое же, как с переменными и выбором:

1
2
3
4
5
// выводит "There is one apple..."
echo $translator->trans('num_of_apples', ['apples' => 1]);

// выводит "There are 23 apples!"
echo $translator->trans('num_of_apples', ['apples' => 23]);

Note

Вы также можете установить переменную offset, чтобы определить, должна ли плюрализация быть относительной (например, в предложениях вроде You and # other people / You and # other person).

Tip

При комбинировании функций select и plural, постарайтесь, чтобы функция select все равно была крайней:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{gender_of_host, select,
    female {{num_guests, plural, offset:1
        =0    {{host} does not give a party.}
        =1    {{host} invites {guest} to her party.}
        =2    {{host} invites {guest} and one other person to her party.}
        other {{host} invites {guest} and # other people to her party.}
    }}
    male {{num_guests, plural, offset:1
        =0    {{host} does not give a party.}
        =1    {{host} invites {guest} to his party.}
        =2    {{host} invites {guest} and one other person to his party.}
        other {{host} invites {guest} and # other people to his party.}
    }}
    other {{num_guests, plural, offset:1
        =0    {{host} does not give a party.}
        =1    {{host} invites {guest} to their party.}
        =2    {{host} invites {guest} and one other person to their party.}
        other {{host} invites {guest} and # other people to their party.}
    }}
}

Плюрализация в наследуемом синтаксисе Symfony может быть использована с пользовательскими диапазонами (например, иметь разные сообщения для 0-12, 12-40 и 40+). Формат сообщений ICU не имеет такой функции. Вместо этого, такая логика должна быть помещена в PHP-код:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Вместо
$message = $translator->trans('balance_message', $balance);
// с сообщением вроде:
// ]-Inf,0]Oops! I'm down|]0,1000]I still have money|]1000,Inf]I have lots of money

// использовать три разных сообщения для каждого диапазона:
if ($balance < 0) {
    $message = $translator->trans('no_money_message');
} elseif ($balance < 1000) {
    $message = $translator->trans('some_money_message');
} else {
    $message = $translator->trans('lots_of_money_message');
}

Дополнительные функции заполнителя

Кроме этого, ICU MessageFormat имеет несколько других интересных функций.

Ordinal

Схоже с plural, selectordinal позволяет вам использовать числа как порядковую шкалу:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
# translations/messages+intl-icu.en.yaml
finish_place: >-
    You finished {place, selectordinal,
        one   {#st}
        two   {#nd}
        few   {#rd}
        other {#th}
    }!

# при форматировании числа только как порядкового (как выше), вы также можете
# использовать функцию `ordinal`:
finish_place: You finished {place, ordinal}!
1
2
3
4
5
6
7
8
// выводит "You finished 1st!"
echo $translator->trans('finish_place', ['place' => 1]);

// выводит "You finished 9th!"
echo $translator->trans('finish_place', ['place' => 9]);

// выводит "You finished 23rd!"
echo $translator->trans('finish_place', ['place' => 23]);

Возможные случаи этого также показаны в документе Unicode Правила множественных чисел в разных языках.

Date and Time

Функция дата и время позволяет вам форматировать даты в целевой локали, используя IntlDateFormatter:

  • YAML
  • XML
  • PHP
1
2
# translations/messages+intl-icu.en.yaml
published_at: 'Published at {publication_date, date} - {publication_date, time, short}'

"Утверждение функции" для функций time и date может быть short, medium, long или full, что соответствует константам, определенным классом the IntlDateFormatter:

1
2
// выводит "Published at Jan 25, 2019 - 11:30 AM"
echo $translator->trans('published_at', ['publication_date' => new \DateTime('2019-01-25 11:30:00')]);

Numbers

Форматировщик number позволяет вам форматировать числа, используя NumberFormatter Intl:

  • YAML
  • XML
  • PHP
1
2
3
# translations/messages+intl-icu.en.yaml
progress: '{progress, number, percent} of the work is done'
value_of_object: 'This artifact is worth {value, number, currency}'
1
2
3
4
5
6
7
8
9
// выводит "82% of the work is done"
echo $translator->trans('progress', ['progress' => 0.82]);
// выводит "100% of the work is done"
echo $translator->trans('progress', ['progress' => 1]);

// выводит "This artifact is worth $9,988,776.65"
// если бы мы перевели это, к примеру, на французский, значение отображалось бы как
// "9 988 776,65 €"
echo $translator->trans('value_of_object', ['value' => 9988776.65]);