Недавно я узнал про довольно интересный инструмент, встроенный в РНР. Оказывается, в языке нативно поддерживается универсальный формат шаблонов для сообщений, ICU Message Format. В частности, он используется в Symfony Translation Component и в системе интернационализации фреймворка Yii 2. Однако этот функционал доступен и сам по себе, в чистом РНР. И может использоваться не только для создания многоязычных сайтов, но и в качестве простенького шаблонизатора, например для email-рассылок.

Данный функционал является частью стандартного модуля php-intl, то есть доступен "из коробки" и реализуется с помощью класса MessageFormatter. По сути, это такой printf() на стероидах, с добавлением оператора ветвления и некоторых других возможностей.

Основы

В самом базовом варианте форматтер используется для простой замены плейсхолдеров:

echo msgfmt_format_message("ru", "Привет, {0}!", ['Вася']);
echo msgfmt_format_message("ru", "Привет, {name}!", ['name' => 'Вася']);

Как видно, поддерживаются как нумерованные, так и именованные плейсхолдеры.

Но, разумеется, ради таких простых замен не стоило и затеваться, в этом случае проще использовать обычный sprintf(). ICU message formatter используется из-за развитых возможностей форматирования, и — в частности — с учётом локали:

echo msgfmt_format_message(
    "ru", 
    "Сегодня {0, date, long} или, коротко, {0, date, short}", 
    [new DateTime()]
);
// выведет:
// Сегодня 7 июня 2023 г. или, коротко, 07.06.23

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

{argNameOrNumber, argType, argStyle} 

где argNameOrNumber — это имя или номер плейсхолдера, argType — его тип, и argStyle — дополнительные настройки.

Использование

Установка модуля intl не должна вызывать сложностей, это стандартное sudo apt install php-intl, либо раскомментировать extension=intl в php.ini под Windows.

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

ini_set('intl.use_exceptions', true);

Для получения готовых сообщений можно либо сначала создать формат и затем применить его, либо сделать все в одном вызове. Также можно использовать как объектный, так и процедурный интерфейсы. Я буду использовать в статье функцию msgfmt_format_message(), чтобы код примеров был короче и не вызывал горизонтальный скроллинг. Все три параметра этой функции обязательные. Первой указывается локаль, дальше строка с форматом сообщения и последним — массив с данными.

Типы

К сожалению, внятной документации по этой библиотеке я не нашел. Приходится собирать по крохам и экспериментировать методом тыка, выясняя, как примеры из официальной документации применить к РНР.

К примеру, список из 10 основных типов я взял в статье по Yii::t(), ссылка на которую дана выше. Первые 4 из них требуют обязательного наличия всех трех параметров, а остальные позволяют обойтись двумя.

plural
select
selectordinal
choice (объявлен устаревшим)
number
date
time
spellout
ordinal
duration

Дополнительные настройки

Дополнительные настройки, насколько я понял, делятся на два типа: обычные и начинающиеся с двойного двоеточия, которые называются скелетонами. Обычных настроек всегда немного, например для date и time поддерживаются четыре: short, medium, long, full, а для number — три: integer, currency, percent. Но вот скелетонов может быть сколько угодно, причем они могут быть составными, как в последнем примере (group-off отключает разбивку по тысячам):

echo msgfmt_format_message('ru', '{0, number, percent}', [.50]),"\n";
// 50 %
echo msgfmt_format_message('ru', '{0, number, ::currency/RUR}', [9999.99]),"\n";
// 9 999,99 р.
echo msgfmt_format_message('ru', '{0, number, ::unit/kilogram group-off}', [1000]);
// 1000 кг.

К слову, отдельные числа можно форматировать и с помощью другого класса этого же модуля, NumberFormatter:

echo (new NumberFormatter('@numbers=roman', NumberFormatter::DECIMAL))
    ->format(date('Y'));
// MMXXIII

Интересным типом, относящимся к форматированию чисел, является spellout, который выводит число прописью:

echo msgfmt_format_message('ru', '{0, spellout}', [100500]);
// сто тысяч пятьсот

Ветвление

Но самой, пожалуй, интересной возможностью форматтера является встроенный оператор ветвления (аналогичный конструкции switch), который представлен в нескольких вариантах:

  • plural , для образования множественного числа

  • select, обычно используется для склонения по родам

  • selectordinal для перечислений (поддержка русского языка ограничена)

В шаблонах игнорируются пробельные символы, и благодаря этому их можно форматировать для удобства восприятия сложных конструкций:

$message = 'Всего {files, plural, 
    one{загружен}
    other{загружено}
} {files, number, ::group-off} {files, plural, 
    one{файл}
    few{файла}
    many{файлов}
    other{файла}
}'; 
echo msgfmt_format_message('ru', $message, ['files' => 9991]),"\n";
// Всего загружен 9991 файл
$message = '{host} {host_gender, select, 
        female{будет счастлива} 
        male{будет счастлив}
        neuter{будет счастливо}
        other{будут счастливы}
    } видеть вас на {event},
{event_gender, select, 
        female {которая состоится}
        male{который состоится}
        neuter{которое состоится}
        other{которые состоятся}
    } {date, date} в {date, time, short} {place}
по адресу {address}.';
$data = [
    'host' => 'Мария Ивановна Заподдубная',
    'host_gender' => 'female',
    'event_gender' => 'neuter',
    'event' => 'праздновании дня рождения',
    'date' => new DateTime('2023-05-01 18:00'),
    'place' => 'в кафе "Голубка"',
    'address' => 'улица Садовая, дом 2',
];
echo msgfmt_format_message('ru', $message, $data),"\n";

выведет

Мария Ивановна Заподдубная будет счастлива видеть вас на дне рождения,
которое состоится 1 мая 2023 г. в 18:00 в кафе "Голубка"
по адресу улица Садовая, дом 2.

Соответственно, с другими данными
$data = [
    'host' => 'Полковник Васин',
    'host_gender' => 'male',
    'event_gender' => 'other',
    'event' => 'посиделках',
    'date' => new DateTime('2023-05-01 18:00'),
    'place' => 'у него дома',
    'address' => 'Коммунистический тупик, дом 2',
];

выведет

Полковник Васин будет счастлив видеть вас на посиделках,
которые состоятся 1 мая 2023 г. в 18:00 у него дома
по адресу Коммунистический тупик, дом 2.

Также поддерживаются вложенные конструкции, например в каждую ветку select можно добавить свой plural, но если честно, то я не смог придумать жизненный пример, и поэтому просто скопирую из документации:

Вложенные конструкции
{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.}}}}

Экранирование

Сделано довольно занятно, одинарными кавычками. Любой текст, заключенный в них, интерпретируется, как есть. Чтобы экранировать саму кавычку, её надо удвоить:

echo msgfmt_format_message("ru", "{0} '{0}' O''Neal", [1]);
// 1 {0} O'Neal

Недостатки

  • Часто попадаются невнятные сообщения об ошибках. "Creating message formatter failed" и думай сам, что там не так. Но, справедливости ради, некоторые ошибки наоборот, вполне информативные, например "pattern syntax error (parse error at offset 21".

  • Отсутствие структурированной документации и примеров использования.

  • Ну и необходимость подключать расширение PHP, если оно ещё не подключено.

Заключение

Как видно из приведенных выше примеров, данный механизм позволяет легко решать стандартные задачи, связанные со склонениями и перечислениями в русском (и других языках), а также форматировать текст с учётом локали, причем без подключения дополнительных библиотек. Некоторым минусом является отсутствие примеров конкретно для РНР, но я надеюсь что данная публикация немного восполнит этот пробел.

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

Дополнительные материалы

Комментарии (2)


  1. inilim2
    09.06.2023 05:17
    +1

    Сегодня на работе, нужна была такая вещь, правда то что его нужно устанавливать отдельно, я бы его не называл "из коробки"


    1. Fockker Автор
      09.06.2023 05:17
      +1

      С одной стороны вы правы - если модуль не установлен, то его надо доустанавливать. Но с другой - это же как и с любым другим функционалом РНР. Redis, zip, mbstring, curl, gd, PDO - все эти функции ведь тоже доступны только в виде расширений, и требуют установки перед использованием.

      Тем более что под виндой это совсем просто - убрать комментарий в php.ini, в каких-нибудь CPanel/Xampp - одну галочку в настройках поставить, в докере тоже без проблем, на собственном VPS устанавливается одной командой.

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