Pull to refresh

Форматирование сообщений для Yii::t()

Reading time 16 min
Views 28K
В данной статье рассмотрены особенности форматирования сообщений для системы интернационализации фреймворка Yii 2. В основном, это сведения из документации фреймворка и библиотеки ICU, с дополнительными пояснениями и примерами. Большая часть информации подойдет для любого фреймворка, который использует библиотеку ICU для интернационализации сообщений. В примерах подразумевается перевод сообщений с английского на русский ('en-US' => 'ru-RU'). Настройка системы интернационализации в статье не рассматривается.

Процесс перевода состоит из 2 частей – получение переведенной строки из источника сообщений и форматирование сообщения. В качестве источников сообщений могут использоваться классы yii\i18n\PhpMessageSource, yii\i18n\DbMessageSource, yii\i18n\GettextMessageSource.

Для форматирования используются классы yii\i18n\Formatter и yii\i18n\MessageFormatter, которые используют расширение intl, которое использует библиотеку ICU, описание использования которой находится в ICU User Guide. А еще есть ‎CLDR Specifications и API Reference.

Yii::t() это обертка для вызова I18N::translate()
Скрытый текст
\yii\BaseYii:
    public static function t($category, $message, $params = [], $language = null)
    {
        if (static::$app !== null) {
            return static::$app->getI18n()->translate($category, $message, $params, $language ?: static::$app->language);
        } else {
            $p = [];
            foreach ((array) $params as $name => $value) {
                $p['{' . $name . '}'] = $value;
            }

            return ($p === []) ? $message : strtr($message, $p);
        }
    }

yii\i18n\I18N:
    public function translate($category, $message, $params, $language)
    {
        $messageSource = $this->getMessageSource($category);
        $translation = $messageSource->translate($category, $message, $language);
        if ($translation === false) {
            return $this->format($message, $params, $messageSource->sourceLanguage);
        } else {
            return $this->format($translation, $params, $language);
        }
    }


Параметры в сообщение можно передавать 2 способами, через массив и через запятую. В документации Yii указаны следующие примеры:
$username = 'Alexander';
echo Yii::t('app', 'Hello, {username}!', ['username' => $username]);
// Hello, Alexander!

$price = 100;
$count = 2;
$subtotal = 200;
echo Yii::t('app', 'Price: {0}, Count: {1}, Subtotal: {2}', $price, $count, $subtotal);
// Price: 100, Count: {1}, Subtotal: {2}

Посмотрев на исходный код функций, нетрудно заметить, что второй пример в таком виде работать не будет. Такой вызов можно использовать только для одного параметра, и то только потому, что в функции I18N::format() происходит приведение к типу array:
$price = 100;
echo Yii::t('app', 'Price: {0}', $price);

Скрытый текст
yii\i18n\I18N:
public function format($message, $params, $language)
    {
        $params = (array) $params;
        if ($params === []) {
            return $message;
        }

        if (preg_match('~{\s*[\d\w]+\s*,~u', $message)) {
            $formatter = $this->getMessageFormatter();
            $result = $formatter->format($message, $params, $language);
            if ($result === false) {
                $errorMessage = $formatter->getErrorMessage();
                Yii::warning("Formatting message for language '$language' failed with error: $errorMessage. The message being formatted was: $message.", __METHOD__);

                return $message;
            } else {
                return $result;
            }
        }

        $p = [];
        foreach ($params as $name => $value) {
            $p['{' . $name . '}'] = $value;
        }

        return strtr($message, $p);
    }


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



Правила форматирования


Для параметров могут быть указаны правила форматирования.
{PlaceholderName, ParameterType, ParameterStyle}    // документация Yii
{argNameOrNumber, argType, argStyle}                // документация ICU

Например:
echo Yii::t('app', 'Price: {price, number, currency}', ['price' => 100]);
// Price: $100.00

В документации ICU описывается 10 типов аргументов:
plural
select
selectordinal
choice
number
date
time
spellout
ordinal
duration

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


plural


Документация. Используется для обработки форм множественного числа. Есть 6 предопределенных ключевых слов: zero, one, two, few, many, other. Значение для other обязательно, оно должно всегда присутствовать в паттерне. Для разных языков доступен разный набор ключевых слов. Для русского языка это one, few, many, other; для английского one, other. Подробный список с правилами соответствия можно найти в спецификации CLDR. Тип правил cardinal определяет ключевые слова для plural, тип ordinal для selectordinal.
$fileCount = 21;
echo $fileCount.' '.Yii::t('app', '{fileCount, plural, one{file} other{files}}', ['fileCount' => $fileCount]);

// перевод (messages/ru-RU/app.php)
'{fileCount, plural, one{file} other{files}}' => '{fileCount, plural, one{файл} few{файла} many{файлов} other{файла}}'

// результат
// 1 файл
// 2 файла
// 11 файлов
// 21 файл
// 0 файлов
// 1.5 файла


Для специальных случаев можно указать точное значение (explicit value). После знака '=' не должно быть пробела.
$fileCount = 2;
echo $fileCount.' '.Yii::t('app', '{fileCount, plural, =2{special case files} one{file} other{files}}', [
    'fileCount' => $fileCount
]);

// 2 special case files


Знак '#' во внутренних сообщениях заменяется на числовое значение аргумента. Обратите внимание, значение форматируется с учетом локали — запятая для дробной части, пробел для тысяч. Cистемная локаль, которая устанавливается функцией setlocale(), на библиотеку ICU не влияет.
$fileCount = 21;
echo Yii::t('app', 'Total {fileCount, plural, one{# file} other{# files}}', ['fileCount' => $fileCount]);

// перевод
'Total {fileCount, plural, one{# file} other{# files}}' =>
    'Всего {fileCount, plural, one{# файл} few{# файла} many{# файлов} other{# файла}}'

// Всего 21 файл
// Всего 21,2 файла
// Всего 1 000 000 файлов


Для случаев, когда начало фразы уже подразумевает некоторое количество, есть параметр offset. При обработке его значение вычитается из аргумента. Точные значения сравниваются до вычитания, ключевые слова после. Знак '#' всегда заменяется на результат вычитания.
$likeCount = 2;
echo Yii::t('app', 'You {likeCount, plural,
    offset: 1
    =0{did not like this}
    =1{liked this}
    one{and one other person liked this}
    other{and # others liked this}
}', [
    'likeCount' => $likeCount
]);

// You and one other person liked this

Пока писал статью, нашел полезную ссылку на документацию Meteor MessageFormat, где можно потрогать это все в реальном времени.


select


Документация. Используется для выбора фраз по заданным ключевым словам, обычно для выбора женской / мужской формы слова. Ключевое слово other обязательно, остальные могут быть любыми.
$name = 'Иван';
$gender = 'male';
$city = 'Париж';
echo Yii::t('app', '{name} went to {city}', [
    'name' => $name,
    'went_gender' => $gender,
    'city' => $city,
]);

// перевод
'{name} went to {city}' => '{name} {went_gender, select, male{ездил} female{ездила} other{ездили}} в {city}'

// Иван ездил в Париж
// Мария ездила в Париж
// Иван и Мария ездили в Париж


Сообщения в фигурных скобках могут быть вложенными и ссылаться на другие аргументы. Это работает и для остальных типов. Рекомендуется сложные аргументы выносить наружу и писать полные предложения внутри их сообщений.
Скрытый текст
$host = 'Мария';
$gender_of_host = 'female';
$num_guests = 3;
$message = "
{gender_of_host, select,
    female {
        {num_guests, plural,
            =0 {{host} does not celebrate her birthday.}
            one {{host} invites one guest to her birthday.}
            other {{host} invites # guests to her birthday.}
        }
    }
    male {
        {num_guests, plural,
            =0 {{host} does not celebrate his birthday.}
            one {{host} invites one guest to his birthday.}
            other {{host} invites # guests to his birthday.}
        }
    }
    other {
        {num_guests, plural,
            =0 {{host} do not celebrate their birthday.}
            one {{host} invite one guest to their birthday.}
            other {{host} invite # guests to their birthday.}
        }
    }
}
";
echo Yii::t('app', $message, [
    'host' => $host,
    'gender_of_host' => $gender_of_host,
    'num_guests' => $num_guests,
]);


// перевод
"
{gender_of_host, select,
    female {
        {num_guests, plural,
            =0 {{host} does not celebrate her birthday.}
            one {{host} invites one guest to her birthday.}
            other {{host} invites # guests to her birthday.}
        }
    }
    male {
        {num_guests, plural,
            =0 {{host} does not celebrate his birthday.}
            one {{host} invites one guest to his birthday.}
            other {{host} invites # guests to his birthday.}
        }
    }
    other {
        {num_guests, plural,
            =0 {{host} do not celebrate their birthday.}
            one {{host} invite one guest to their birthday.}
            other {{host} invite # guests to their birthday.}
        }
    }
}
" => "
{gender_of_host, select,
    female {
        {num_guests, plural,
            =0 {{host} не празднует ее день рождения.}
            one {{host} приглашает # гостя на ее день рождения.}
            other {{host} приглашает # гостей на ее день рождения.}
        }
    }
    male {
        {num_guests, plural,
            =0 {{host} не празднует его день рождения.}
            one {{host} приглашает # гостя на его день рождения.}
            other {{host} приглашает # гостей на его день рождения.}
        }
    }
    other {
        {num_guests, plural,
            =0 {{host} не празднуют их день рождения.}
            one {{host} приглашают # гостя на их день рождения.}
            other {{host} приглашают # гостей на их день рождения.}
        }
    }
}
"

// Иван приглашает 3 гостей на его день рождения.
// Мария приглашает 3 гостей на ее день рождения.
// Близнецы Иван и Петр приглашают 3 гостей на их день рождения.



selectordinal


Этот тип аналогичен plural, за исключением того, что у него другой набор разрешенных ключевых слов. Используется для вывода порядковых числительных и связанных с ними выражений. Для русского языка поддерживается только слово other, так что 2 буквы вывести не получится.
$n = 3;
echo Yii::t('app', 'You are {0, selectordinal, one{#st} two{#nd} few{#rd} other{#th}} visitor', [$n]);
// You are 3rd visitor

// перевод
'You are {0, selectordinal, one{#st} two{#nd} few{#rd} other{#th}} visitor' =>
    'Вы {0, selectordinal, other{#-й}} посетитель',

// Вы 3-й посетитель

Почему-то работают только нумерованные параметры, для именованных возникает предупреждение: «Call to ICU MessageFormat::format() has failed».


choice


Документация. Используется для конвертирования числовых диапазонов в соответствующие им строки. Изначально этот тип был предназначен для форм множественного числа. Однако, правила многих языков слишком сложны для его возможностей. Он считается устаревшим, и вместо него рекомендуется использовать plural и select.
Формат сообщения разбивает числовой диапазон [-∞, +∞] на несколько диапазонов. Каждому диапазону сопоставляется строка. Выбирается первый диапазон, где значение аргумента не больше верхней границы, и возвращается соответствующая ему строка. Число начального диапазона игнорируется, что соответствует -∞. Знак '<' (less_than) исключает границу, знак '#' (less_than_or_equal) включает в диапазон. Вместо числа можно указывать знак '∞' (U+221E), вместо '#' знак '≤' (U+2264). Перед знаком '|' не должно быть пробела, иначе он включается в текст сообщения предыдущего диапазона.
$fileCount = 1;
echo $fileCount.': '.Yii::t('app', '{fileCount, choice, 0 # no files| 1 # one file| 1 < many files}', [
    'fileCount' => $fileCount,
]);
// 0 считается -inf
// [-inf] | [1] | (1, +inf]
// [-inf, 1) | [1] | (1, +inf]

// 0: no files
// 1: one file
// 2: many files


$day = 1;
echo $day.' - '.Yii::t('app', '{day, choice, 0 # unknown|1 # Sun|2 # Mon|3 # Tue|4 # Wed|5 # Thu|6 # Fri|7 # Sat|8 # unknown}', ['day' => $day]);
// [-inf, 1) | [1, 2) | [2, 3) | [3, 4) | [4, 5) | [5, 6) | [6, 7) | [7, 8) | [8, +inf]
// 1 - Sun
// 2 - Mon


number


Документация. Используется для вывода числовых значений. Можно задать стиль integer, currency, percent, либо указать паттерн вручную. Значение форматируется с учетом параметров локали — десятичный разделитель, разделитель тысяч, знак валюты и др. По умолчанию для чисел выводится до 3 знаков дробной части.
$value = 123456.789012;
echo Yii::t('app', 'Value: {value, number}', ['value' => $value]);
// Value: 123,456.789

echo Yii::t('app', 'Value: {value, number, integer}', ['value' => $value]);
// Value: 123,457

$value = 1.23;
echo Yii::t('app', 'Value: {value, number, percent}', ['value' => $value]);
// Value: 123%


Через строку сообщения влиять на эти параметры нельзя. Если нужно выводить в другом формате, лучше использовать специализированную функцию yii\i18n\Formatter::asDecimal().
$value = 123456.789;
Yii::$app->formatter->decimalSeparator = '.';
Yii::$app->formatter->thousandSeparator = ' ';
$formattedValue = Yii::$app->formatter->asDecimal($value);
echo Yii::t('app', 'Balance: {value, number} - {formattedValue}', ['value' => $value, 'formattedValue' => $formattedValue]);
// Balance: 123,456.789 - 123 456.789


Формат паттернов лучше показать на примерах. При отбрасывании цифр производится округление по правилам математики.
Скрытый текст
// 0 - цифра, выводится всегда
$value = 1234567;
echo $value.': '.Yii::t('app', 'Result - {value, number, 000000.0000}', ['value' => $value]);
echo '<br>';
// 123: Result - 000123.0000
// 1234567: Result - 1234567.0000


// # - цифра, выводится, если не 0
$value = 123.456789;
echo $value.': '.Yii::t('app', 'Result - {value, number, ######.####}', ['value' => $value]);
echo '<br>';
// 123: Result - 123
// 123.456789: Result - 123.4568


// 1-9 - округление
$value = 123.333;
echo $value.': '.Yii::t('app', 'Result - {value, number, 0.2}', ['value' => $value]);
echo '<br>';
// 123.111: Result - 123.2
// 123.333: Result - 123.4


// @ - значащая цифра
$value = 1;
echo $value.': '.Yii::t('app', 'Result - {value, number, @@@}', ['value' => $value]);
echo '<br>';
// 123.456: Result - 123
// 1.23456: Result - 1.23
// 123456: Result - 123000
// 1: Result - 1.00


// от 2 до 4 значащих цифр
$value = 12.3456;
echo $value.': '.Yii::t('app', 'Result - {value, number, @@##}', ['value' => $value]);
echo '<br>';
// 12: Result - 12
// 12.3: Result - 12.3
// 12.3456: Result - 12.35


// . - десятичный разделитель (разделитель дробной части)
// , - группирующий разделитель (разделитель тысяч)
// обозначают позицию разделителей, заменяются обозначениями из параметров локали
$value = 123456.789;
echo $value.': '.Yii::t('app', 'Result - {value, number, #,###.##}', ['value' => $value]);
echo '<br>';
// 123456.789: Result - 123,456.79

echo $value.': '.Yii::t('app', 'Result - {value, number, #,####.##}', ['value' => $value]);
echo '<br>';
// 123456.789: Result - 12,3456.79

// отсутствие группирующего разделителя отключает разделение
// отсутствие десятичного разделителя отключает вывод дробной части
echo $value.': '.Yii::t('app', 'Result - {value, number, #}', ['value' => $value]);
echo '<br>';
// 123456.789: Result - 123457

// группирующий разделитель может встречаться 2 раза (если больше, остальные игнорируются)
$value = 987654321;
echo $value.': '.Yii::t('app', 'Result - {value, number, #,##,###}', ['value' => $value]);
echo '<br>';
// 987654321: Result - 98,76,54,321


// ; - для отрицательных значений можно указать паттерн с другим префиксом и суффиксом
// на остальные параметры вывода второй паттерн не влияет
$value = -12.34;
echo $value.': '.Yii::t('app', 'Result - {value, number, #.##;minus # value}', ['value' => $value]);
echo '<br>';
// -12.34: Result - minus 12.34 value


// E - разделяет мантиссу и экспоненту в экспоненциальной записи
$value = 123000000;
echo $value.': '.Yii::t('app', 'Result - {value, number, #.##E+00}', ['value' => $value]);
echo '<br>';
// 123000000: Result - 1.23E+08


// * - дополнение символом (padding); значение дополняется символом, следующим после *
$value = 1234;
echo $value.': '.Yii::t('app', 'Result - {value, number, *_######}', ['value' => $value]);
echo '<br>';
//  123: Result - ___123
// 1234: Result - __1234

$value = 1234;
echo $value.': '.Yii::t('app', 'Result - {value, number, ######*_}', ['value' => $value]);
echo '<br>';
//  123: Result - 123___
// 1234: Result - 1234__


// % - символ процента; значение при выводе умножается на 100
// ‰ - символ промилле (U+2030); значение при выводе умножается на 1000
$value = 0.123;
echo $value.': '.Yii::t('app', 'Result - {value, number, #.#‰}', ['value' => $value]);
echo '<br>';
// 0.123: Result - 12.3%
// 0.123: Result - 123‰


// ¤ - знак валюты (U+00A4)
// одинарный - знак валюты, двойной - международное обозначение валюты, тройной - текстовое название валюты
// (несмотря на описание в документации, тройной символ работает так же, как одинарный)
$value = 12.34;
echo $value.': '.Yii::t('app', 'Result - {value, number, #.##¤}', ['value' => $value]);
echo '<br>';
// 12.34: Result - 12.34$

echo $value.': '.Yii::t('app', 'Result - {value, number, #.##¤¤}', ['value' => $value]);
echo '<br>';
// 12.34: Result - 12.34USD



Результат вызова (number, currency) также зависит от локали, в частности, из нее берется обозначение валюты. Так что, если у вас пока нет перевода для строки с использованием currency, цена будет выводиться в долларах, а когда перевод появится, то в рублях. Поэтому лучше использовать специализированную функцию yii\i18n\Formatter::asCurrency().
$price = 123456;
echo \Yii::t('app', 'Price: {price, number, currency}', ['price' => $price]);
// Price: $123,456.00

// перевод
'Price: {price, number, currency}' => 'Цена: {price, number, currency}'

// Цена: 123 456,00 руб.


$price = Yii::$app->formatter->asCurrency(123456, 'GBP');
echo \Yii::t('app', 'Price: {price}', ['price' => $price]);
// Price: 123 456,00 £


Кстати, локаль можно указывать вот в таком виде: ru-RU@currency=GBP. Подробнее в ICU User Guide.
$price = 123456;
echo Yii::t('app', 'Price: {0, number, currency}', $price, 'ru-RU@currency=GBP');

// перевод (messages/ru-RU@currency=GBP/app.php)
'Price: {0, number, currency}' => 'Цена: {0, number, currency}'

// Цена: 123 456,00 £


date


time


Документация. Используются для вывода даты и времени. Можно задать стиль short, medium, long, full, либо указать паттерн вручную. При использовании паттерна не имеет значения, что указывать — date или time. Формат довольно простой, описание всех символов можно найти в документации или в стандарте Unicode. Разное количество повторений символа имеет разное значение. При форматировании учитывается, с какого дня начинается неделя в текущей локали — воскресенье или понедельник.
$d = strtotime('2015-04-18 11:30:16');

echo Yii::t('app', 'Date: {d, date, short} | {d, date, medium} | {d, date, long} | {d, date, full}', ['d' => $d]);
echo '<br>';
// Date: 4/18/15 | Apr 18, 2015 | April 18, 2015 | Saturday, April 18, 2015
// Дата: 18.04.15 | 18 апр. 2015 г. | 18 апреля 2015 г. | суббота, 18 апреля 2015 г.

echo Yii::t('app', 'Time: {d, time, short} | {d, time, medium} | {d, time, long} | {d, time, full}', ['d' => $d]);
echo '<br>';
// Time: 11:30 AM | 11:30:16 AM | 11:30:16 AM GMT | 11:30:16 AM GMT
// Время: 11:30 | 11:30:16 | 11:30:16 GMT | 11:30:16 GMT


// y, M, d, H, m, s - год, месяц, день, час, минута, секунда
echo Yii::t('app', 'Date: {d, date, yyyy-MM-dd HH:mm:ss}', ['d' => $d]);
echo '<br>';
// Date: 2015-04-18 11:30:16

// E, e, S, a - день недели, номер дня недели, миллисекунды, AM/PM
echo Yii::t('app', "Date: {d, date, d MMMM yyyy; EEEE; e 'day of week'; HH-mm-ss.SSS'ms'; a}", ['d' => $d + 0.100]);
echo '<br>';
// Date: 18 April 2015; Saturday; 7 day of week; 11-30-16.100ms; AM
// Дата: 18 апреля 2015; суббота; 6 день недели; 11-30-16.100мс; до полудня


spellout


Запись числа словами. Можно использовать, например, в каких-нибудь бланках отчетности, где надо писать сумму прописью.
echo Yii::t('app', '{n, number} is spelled as {n, spellout}', ['n' => 42]);
// 42 is spelled as forty-two

// перевод
'{n, number} is spelled as {n, spellout}' => '{n, number} пишется как {n, spellout}'

// 42 пишется как сорок два


Для русского языка по умолчанию выводит в мужском роде. Чтобы вывести в женском (двадцать одна копейка), нужно добавить название набора правил. Перед знаком '%' не должно быть пробела, иначе не работает.
echo Yii::t('app', '{value, spellout}', ['value' => 21]);
// twenty-one

// перевод
'{value, spellout}' => '{value, spellout,%spellout-cardinal-feminine}'

// двадцать одна

Этот способ я нашел на stackoverflow. Там приводится ссылка на документацию системы Saxon, это единственное место, где есть более-менее внятное описание.

Какие правила понимает форматтер для определенной локали, можно узнать так:
$formatter = new \NumberFormatter('ru-RU', \NumberFormatter::SPELLOUT);
echo $formatter->getTextAttribute(\NumberFormatter::PUBLIC_RULESETS);
// %spellout-numbering-year;%spellout-numbering;%spellout-cardinal-masculine;%spellout-cardinal-neuter;%spellout-cardinal-feminine;

Для полноты картины было бы неплохо, если функция yii\i18n\Formatter::asSpellout() имела такую же сигнатуру, как и asInteger(). Сейчас она принимает 1 параметр. Тогда можно было бы писать так:
echo Yii::$app->formatter->asSpellout(21, [], [\NumberFormatter::DEFAULT_RULESET => "%spellout-cardinal-feminine"]);

Типы ordinal и duration тоже поддерживают третий параметр. Названия наборов правил по умолчанию можно найти в исходниках проекта ICU (функция RuleBasedNumberFormat::initDefaultRuleSet()): spellout — '%spellout-numbering', ordinal — '%digits-ordinal', duration — '%duration'.


ordinal


Вывод порядкового номера. Аналог selectordinal без возможности указать варианты. На русском языке выводит только точку после числа.
echo Yii::t('app', 'You are the {n, ordinal} visitor here!', ['n' => 42]);
// You are the 42nd visitor here!
// Вы 42. посетитель здесь!


duration


Параметр рассматривается как число секунд и выводится как продолжительность чего-либо в формате 'h:m:s'. Для русского языка не работает, значение выводится как есть в формате integer.
echo Yii::t('app', 'You are here for {n, duration} already!', ['n' => 123]);
// You are here for 47 sec. already!
// You are here for 2:03 already!
// Вы находитесь здесь уже 123!



Особенности поведения


Для расширения intl можно задать, выдавать или нет предупреждения об ошибках. Уровень ошибки задается в php.ini, параметр intl.error_level. Если его убрать, предупреждения не возникают; сообщения не форматируются и выводятся как есть.

Если расширение intl не установлено, его работа эмулируется, однако возможности эмуляции ограничены. Тип select поддерживается полностью, для типа plural поддерживаются только формы one и other (как для английского языка), для типа number только аргументы типа integer. Остальные типы не поддерживаются.

Если передан неправильный формат, при использовании расширения возникает предупреждение «MessageFormatter::__construct(): msgfmt_create: message formatter creation failed». В режиме эмуляции работает без ошибок, сообщение выводится как есть.

Символы синтаксиса, встречающиеся в тексте, должны быть заключены в апострофы. Двойной апостроф заменяется на одинарный. Одинарный апостроф выводится как есть, но может привести к ошибке разбора сообщения.
$count = 3;
echo Yii::t('app', "Example of string with ''syntax characters'': '{' '}' '{test}' {count, plural, other{''count'' value is # '#{}'}}", ['count' => $count]);
// Example of string with 'syntax characters': { } {test} 'count' value is 3 #{}

echo Yii::t('app', "Example of string with ''syntax characters'': ''{' '}' '{test}' {count, plural, other{''count'' value is # '#{}'}}", ['count' => $count]);
// MessageFormatter::__construct(): msgfmt_create: message formatter creation failed


Если в Yii::t() не передаются переменные, либо передаются просто в фигурных скобках без правил форматирования, то обращения к intl не будет. Сообщение будет выведено как есть, с двойными апострофами, дробные значения будут выведены в системной локали. В функции I18N::format() есть регулярное выражение, которое проверяет наличие правил.
$count = 3.2;
echo Yii::t('app', "Example of string with ''syntax characters'': {count}", ['count' => $count]);

// перевод
"Example of string with ''syntax characters'': {count}" => "Пример строки с ''syntax characters'': {count}"

// Пример строки с ''syntax characters'': 3.2


А вот это похоже на баг в расширении intl. В некоторых случаях одну и ту же переменную нельзя использовать 2 раза в одном сообщении, появляется предупреждение «MessageFormatter::format(): Inconsistent types declared for an argument». Например, нельзя вывести, если в одном из вариантов не указан тип, либо как select и plural, но можно вывести как number и plural. В режиме эмуляции все работает правильно.
echo Yii::t('app', '{value} {value, plural, other{test}}', ['value' => 1]);
// MessageFormatter::format(): Inconsistent types declared for an argument

echo Yii::t('app', '{value, select, other{test}} {value, plural, other{test}}', ['value' => 1]);
// MessageFormatter::format(): Inconsistent types declared for an argument

echo Yii::t('app', '{value, number} {value, plural, other{test}}', ['value' => 1]);
// 1 test

И даже больше. Разные переменные с похожими названиями дают такую же ошибку. Но если имена отличаются сильнее, ошибка исчезает.
echo Yii::t('app', '{valueA} {valueB, plural, other{test}}', [
    'valueA' => 1,
    'valueB' => 2,
]);
// MessageFormatter::format(): Inconsistent types declared for an argument

echo Yii::t('app', '{valueA} {valueB1, plural, other{test}}', [
    'valueA' => 1,
    'valueB1' => 2,
]);
// 1 test

Это предупреждение можно найти в исходниках проекта intl в файле msgformat_helpers.cpp. Чуть выше есть коммент «We found a different type for the same arg!», что явно не соотносится с последним примером.

Небольшое дополнение
Чтобы правильно обрабатывать русские формы слов, когда расширение intl не установлено, или для каких-то специфичных случаев, можно в каком-нибудь хелпере сделать следующую функцию:
public static function numberForWord($number)
{
    $number = $number % 100;
    return ($number < 20 ? $number : $number % 10);
}


// использование
$yearNumber = 22;
echo Yii::t('app', '{yearNumber}{yearNumberForWord, plural, =1{st} =2{nd} =3{rd} other{th}} year', [
    'yearNumber' => $yearNumber,                                        // 1, 2, 11, 22
    'yearNumberForWord' => PluralHelper::numberForWord($yearNumber),    // 1, 2, 11, 2
]);

// перевод
'{yearNumber}{yearNumberForWord, plural, =1{st} =2{nd} =3{rd} other{th}} year' =>
    '{yearNumber}{yearNumberForWord, plural, =1{ый} =2{ой} =3{ий} =6{ой} =7{ой} =8{ой} other{ый}} год',

// 22nd year
// 22ой год


Статья получилась довольно большая, но я решил, что лучше пусть вся основная информация по этой теме будет собрана в одном месте.
Tags:
Hubs:
+11
Comments 9
Comments Comments 9

Articles