Контент


Ko3: мультиязычность

Данное неуклюжее слово в заголовке означает, что настало время познакомиться с реализацией системы перевода текстовых ресурсов проекта на различные языки мира. Как мы помним, в 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 предлагает нам несколько новую схему перевода:

  1. Переводу подлежит целой предложение или фраза, которая уже должна быть читабельна. Обосновывается это тем, что в случае отсутствия нужного перевода возвращается исходная строка. Конечно, основанная на ключах фраза ‘user.login.greetings‘ несколько неинформативна. ;)
  2. Если используемый в функции __() язык (параметр $lang) не отличается от установленного в I18n::$lang, то фраза переводиться не будет (!), только подстановка значений из массива $values. Тут выясняется, что переменная $lang в принципе не участвует в переводе строк. Метод I18n::get() всегда берет язык из свойства I18n::$lang, так что по сути переменная $lang указывает на дефолтную кодировку проекта. Например, при I18n::$lang == 'en-us' и вызове __('test', NULL, 'ru-ru'), строка ‘test‘ будет переведена на английский (как бы это странно не выглядело), а при I18n::$lang == 'ru-ru' перевода не будет, т.е. будет возвращена фраза ‘test‘.
  3. Файлы перевода должны располагаться в папке 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().

Google Bookmarks Digg Reddit del.icio.us Ma.gnolia Technorati Slashdot Yahoo My Web News2.ru БобрДобр.ru RUmarkz Ваау! Memori.ru rucity.com МоёМесто.ru Mister Wong

Опубликовано в Kohana3.

Теги: , , .


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

Будьте в курсе обсуждения, подпишитесь на RSS ленту комментариев к этой записи.

  1. Random пишет:

    А есть ли встроенная поддержка множественных чисел для корректного отображения подобных записей: 1 день, 2 дня, 10 дней?

  2. altspam пишет:

    :idea: Предлагаю по аналогии с I18n перейти на М12ть.

  3. BIakaVeron пишет:

    @Random
    «Англоязычники» в таком не нуждаются, так что придется все делать самостоятельно.

  4. Random пишет:

    Ну как же не нуждаются? А 1 comment, 23 comments?

  5. BIakaVeron пишет:

    Тут инфлектора достаточно, т.к. единственное число соответствует только значению 1. В русском языке все намного сложнее.

  6. zeek пишет:

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

  7. BIakaVeron пишет:

    @zeek
    Да, к сожалению, группировать фразы теперь можно только в сообщениях (message)… Хотя, конечно, никто не мешает расширить стандартный I18n для поддержки подмассивов.

  8. alex пишет:

    А не подскажите оптимальный для Ко3 вариант реализации роутинга для мультиязычных сайтов? По типу domain/lang/controller ? Для Ко2 было несколько реализаций, а вот как лучше реализовать в Ко3 пока не совсем ясно.

  9. BIakaVeron пишет:

    На данный момент можно выбрать одно из решений, описанных тут и тут. Первое плохо тем, что оно требует корректировки системных классов Request и URL, второе использует полное название языка вместе с регионом (‘en-us’, ‘ru-ru’ и т.д.), что не очень удобно. Это связано с тем, что Ko3 пока что не поддерживает языков без кода региона.



Можно включить подсветку кода: <code><pre lang="">...</pre></code>
Разрешены некоторые HTML теги

или используйте trackback.