Данное неуклюжее слово в заголовке означает, что настало время познакомиться с реализацией системы перевода текстовых ресурсов проекта на различные языки мира. Как мы помним, в Ko2 за это отвечал класс I18n, занимавшийся поиском ключевых фраз (key-based strings, даже не знаю, как по-русски это объяснить ) в файлах директории i18n. В новой версии все немного сложнее.
Класс I18n
Начнем с того, что сам по себе класс I18n по-прежнему существует, и занимается примерно тем же. Однако между ним и пользователем поставлена специальная функция __()
, объявленная в файле SYSPATH/base.php:
function __($string, array $values = NULL, $lang = 'en-us') { if ($lang !== I18n::$lang) { // The message and target languages are different // Get the translation for this message $string = I18n::get($string); } return empty($values) ? $string : strtr($string, $values); } |
Данная функция переводит строку $string
на текущий язык, используя файлы в папке i18n.
Текущий используемый язык хранится в свойстве
I18n::$lang
, по умолчанию он установлен в ‘en-us‘. Так как свойство объявлено как public, никто не мешает по ходу работы приложения его менять.
Есть поддержка плейсхолдеров (placeholders), т.е. вызов __('hello, :username', array(':username' => $user->name))
сначала переведет фразу ‘hello, :username‘, а затем заменит :username
на значение свойства name переменной $user
.
Как Вы уже могли заметить, для перевода дана целая фраза, а не более привычные по Ko2 разделенные точками ключевые слова (типа ‘user.login.greetings‘). Дело в том, что Shadowhand предлагает нам несколько новую схему перевода:
- Переводу подлежит целой предложение или фраза, которая уже должна быть читабельна. Обосновывается это тем, что в случае отсутствия нужного перевода возвращается исходная строка. Конечно, основанная на ключах фраза ‘user.login.greetings‘ несколько неинформативна.
-
Если используемый в функции
__()
язык (параметр$lang
) не отличается от установленного вI18n::$lang
, то фраза переводиться не будет (!), только подстановка значений из массива$values
. Тут выясняется, что переменная$lang
в принципе не участвует в переводе строк. МетодI18n::get()
всегда берет язык из свойстваI18n::$lang
, так что по сути переменная$lang
указывает на дефолтную кодировку проекта. Например, приI18n::$lang == 'en-us'
и вызове__('test', NULL, 'ru-ru')
, строка ‘test‘ будет переведена на английский (как бы это странно не выглядело), а приI18n::$lang == 'ru-ru'
перевода не будет, т.е. будет возвращена фраза ‘test‘. -
Файлы перевода должны располагаться в папке i18n с разбиением по коду языка, например для американского английского (en-us) файл будет i18n/en/us.php, а для русского (ru-ru) — i18n/ru/ru.php. Формат в принципе аналогичен всем остальным конфигурационным файлам в Ko3:
return array ( 'Hello, :username' => 'Здравствуйте, :username', 'Register' => 'Зарегистрироваться', );
В модулях, папках APPPATH и SYSPATH могут быть i18n-файлы с одинаковыми названиями, они будут объединены.
Таким образом, предполагается, что программист разрабатывает проект с использованием целых фраз на своем родном языке (или на английском, ставшим де-факто основным языком в программировании), а уже затем осуществляет перевод их на прочие локали. Если необходимо переводить целые блоки текста, рекомендуется использовать отдельные шаблоны (views) для этого.
Лично мне не очень удобна такая схема, я слишком привык к ключам. Единственное, что мешает нам использовать ключи — отсутствие перевода фраз в случае использования «родного» языка. Так что вносим мааааленькие коррективы в файл base.php (естественно, сохраняем его в папке APPPATH, ибо нельзя трогать системное):
function __($string, array $values = NULL, $lang = 'en-us') { // Get the translation for this message $string = I18n::get($string); return empty($values) ? $string : strtr($string, $values); } |
Все просто — мы исключили проверку языка. Теперь в любом случае будет производиться поиск строки (в нашем случае строка будет состоять из ключей, поэтому-то мы и хотим переводить в обязательном порядке) и подстановка в нее значений из $values
. Чтобы при запуске проекта использовалась наша новая версия функции, необходимо в index.php найти строчку require SYSPATH.'base'.EXT;
и заменить ее на require APPPATH.'base'.EXT;
Дополнительно замечу, что при работе с
__()
иI18n::get()
языковые файлы кешируются после первого прочтения, т.е. постоянного обращения к файловой системе уже не происходит.
Перевод ошибок валидации (сообщения)
Одной из главных возможностей интернационализации в Ko2 являлась подстановка имени i18n-файла в метод errors()
объекта Validation, чтобы ошибки автоматически переводились. Специально для подобных вещей (т.е. использующих иерархию ресурсов) в Ko3 были сделаны сообщения (messages).
Все ресурсы для сообщений хранятся в папке messages с разбиением на файлы (по темам). Например, фреймворк содержит файл SYSPATH/messages/validate.php с переводом типичных правил валидации:
return array( 'not_empty' => ':field must not be empty', 'matches' => ':field must be the same as :param1', 'regex' => ':field does not match the required format', 'exact_length' => ':field must be exactly :param1 characters long', 'min_length' => ':field must be at least :param1 characters long', 'max_length' => ':field must be less than :param1 characters long', 'in_array' => ':field must be one of the available options', 'digit' => ':field must be a digit', ); |
В качестве ключей — правила объекта Validate, значения — это строки с описанием ошибки. Это не столько перевод в чистом виде, сколько промежуточный результат, подготовка данных для дальнейшей локализации через функцию __()
. Основная идея заключается в том, что большая часть ошибок валидации содержит описание стандартных ошибок (таких, как несоответствие по длине строки, составу символов и т.д.), и вместо написания перевода для каждой возможной ошибки можно унифицировать строку. Как мы видим, перевод содержит имя проверяемого поля (параметр :field
), некоторые ошибки имеют дополнительные параметры (:param1
, :param2
и т.д.), которые подставляются автоматически, если правило их использовало (к примеру, максимальная длина строки).
Алгоритм поиска сообщения таков. Сперва в файле
$file
ищем ключ$field.$error
(например, ‘username.min_length‘). Если не найден, ищем$field.default ('username.default')
. Если и этот не найден, то смотрим на$error
(‘min_length‘). Т.е. поиск ведется от частных случаев (перевод зависит от имени поля и правила) к общим (тексты ошибок зависят только от правила). И не забываем, что в message-файле точек нет, там все разбивается на подмассивы (как это было в Ko2).
Давайте посмотрим, как работает механизм сообщений. Предположим, у нас возникла ошибка валидации поля ‘username‘ по правилу min_length(5)
. Ошибки возвращает метод errors($file = NULL, $translate = TRUE)
, в зависимости от преданных параметров результат может отличаться. Параметр $file отвечает за имя файла в папке messages. Параметр $translate
по умолчанию (TRUE) предписывает переводить на дефолтный язык (т.е. на указанный в I18n::$lang
), можно указать конкретную локаль, либо FALSE, если перевод не нужен. Таким образом, для вывода сообщений об ошибках могут быть следующие варианты:
// создаем объект Validate с одним полем $val = Validate::factory(array('username' => 'test')); // добавляем правило min_length(5) $val->rule('username', 'min_length', array(5)); // вернет FALSE var_dump($val->check()); // выведет array( "username"] => array( "min_length", array(5) ) var_dump($val->errors()); // выведет array( "username"] => "поле username должно быть длиной не менее 5" ) var_dump($val->errors('validate')); // выведет array( "username"] => ":field must be at least :param1 characters long" ) var_dump($val->errors('validate', FALSE)); |
В первом случае мы вызывали метод errors()
без параметров, в результате получили исходные данные для самостоятельного формирования ошибки (массив вида $field => array($rule, $param1, param2...)
).
Во втором — указали (параметр $file
) имя файла с сообщениями (в данном случае Kohana будет использовать файл messages/validate.php). В результате получили перевод в виде массива $field => $error_text
, где в сообщении уже было подставлено имя поля и параметры (в данном случае это минимальное количество символов). Обратите внимание, что у меня результат на русском, т.к. параметр I18n::$lang == 'ru-ru'
, а в файле i18n/ru/ru.php есть такая вот строчка:
':field must be at least :param1 characters long' => 'поле :field должно быть длиной не менее :param1', |
Если ее не будет, то ошибка не будет окончательно переведена, и сообщение будет на английском языке, т.е. username must be at least 5 characters long
.
Третий случай довольно любопытен. Мы указали имя message-файла, но дополнительно указали не переводить полученную строку. В результате получаем массив вида $field
=> $message_text
. Мало того, что строка не переведена (в общем-то мы на это рассчитывали, когда указывали $translate = FALSE
), но имя поля и параметры правила не были подстановлены. Мне кажется, это недоработка (в настоящий момент жду ответа на соответствующую задачу).
Вместо прощания
Ну и напоследок, еще одна возможность. Наверняка захочется вместо имени поля подставлять более говорящий текст, например ‘имя пользователя‘ вместо ‘username‘. Специально для этого в объекте Validate есть метод label($field, $label)
, который заменяет в тексте сообщения имя поля на эту метку (т.е. ключ :field
будет заменен не на ‘username‘, а на примененное в методе label()
значение). Например, так: $val->label('username', __('username'))
. Только учтите, что все вызовы label()
должны быть произведены ДО вызова check()
.
А есть ли встроенная поддержка множественных чисел для корректного отображения подобных записей: 1 день, 2 дня, 10 дней?
Предлагаю по аналогии с I18n перейти на М12ть.
@Random
«Англоязычники» в таком не нуждаются, так что придется все делать самостоятельно.
Ну как же не нуждаются? А 1 comment, 23 comments?
Тут инфлектора достаточно, т.к. единственное число соответствует только значению 1. В русском языке все намного сложнее.
Спасибо за статью. Лично считаю такой подход весьма неудобным, с точками было кратко и ясно. Также, к сожалению, работать с группами и подмассивами невозможно.
@zeek
Да, к сожалению, группировать фразы теперь можно только в сообщениях (message)… Хотя, конечно, никто не мешает расширить стандартный I18n для поддержки подмассивов.
А не подскажите оптимальный для Ко3 вариант реализации роутинга для мультиязычных сайтов? По типу domain/lang/controller ? Для Ко2 было несколько реализаций, а вот как лучше реализовать в Ко3 пока не совсем ясно.
На данный момент можно выбрать одно из решений, описанных тут и тут. Первое плохо тем, что оно требует корректировки системных классов Request и URL, второе использует полное название языка вместе с регионом (‘en-us’, ‘ru-ru’ и т.д.), что не очень удобно. Это связано с тем, что Ko3 пока что не поддерживает языков без кода региона.