Як перекладати повідомлення, використовуючи ICU MessageFormat

Дата оновлення перекладу 2024-05-29

Як перекладати повідомлення, використовуючи ICU MessageFormat

Повідомлення (тобто, рядки) у додатках практично ніколи не бувають абсолютно статичними. Вони містять змінні або іншу складну логіку на кшталт плюралізації. Для того, щоб обробити це, компонент Translator підтримує синтаксис 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) у ваших повідомленнях:

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, і дозволяє використовувати різні рядки, в залежності від значення змінної. Типове використання
цього гендру:

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
12
13
14
15
16
17
// виводить "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',
]);

// виводить "ACME Company has invited you to their party!"
echo $translator->trans('invitation_title', [
    'organizer_name' => 'ACME Company',
    '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
12
13
14
15
16
17
18
$invitation = '{organizer_gender, select,
    female   {{organizer_name} has invited you to her party!}
    male     {{organizer_name} has invited you to his party!}
    multiple {{organizer_name} have invited you to their party!}
    other    {{organizer_name} has invited you to their party!}
}';

// prints "Ryan has invited you to his party!"
echo $translator->trans(
    $invitation,
    [
        'organizer_name' => 'Ryan',
        'organizer_gender' => 'male',
    ],
    // if you prefer, the required "+intl-icu" suffix is also defined as a constant:
    // Symfony\Component\Translation\MessageCatalogueInterface::INTL_DOMAIN_SUFFIX
    'messages+intl-icu'
);

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

Інша цікава функція - plural. Вона дозволяє вам працювати з плюралізацією у ваших повідомленнях (наприклад, There are 3 apples і There is one apple). Функція виглядає дуже схожою на функцію select:

1
2
3
4
5
6
7
# translations/messages+intl-icu.en.yaml
num_of_apples: >-
    {apples, plural,
        =0    {There are no apples}
        =1    {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 дозволяє вам використовувати числа як порядкову шкалу:

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 Правила множинних чисел у різних мовах.

Дата і час

Функція дата та час дозволяє вам форматувати дати у цільовій локалі, використовуючи IntlDateFormatter:

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, що відповідає константам, визначеним класом 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:

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]);