Думаю, многие из вас пользовались механизмом I18n в Kohana. Его недостатки в принципе очевидны, в первую очередь это отсутствие возможности учесть нюансы (контекст) выражения, такие как множественное число или падеж. Кто пытался реализовать надпись «:count комментария/ев» меня поймет. К тому же (возможно, это мое личное мнение), в 3.0 пропала удобнейшая возможность использовать синтаксис из 2.3.4 вида __('user.profile.fname')
. В поисках интересных решений я наткнулся на модуль I18n_Plural. Как вы понимаете, речь пойдет именно о нем.
Установка и настройка
Первым делом хватаем модуль с Github‘а и размещаем в своей папке modules. Естественно, надо объявить его в Kohana::modules() файла bootstrap.php
. Для полноценной работы необходимо внести изменения в функционал оригинального класса I18n — создаем файл application/classes/i18n.php
со следующим содержимым:
<?php defined('SYSPATH') OR die('No direct access allowed.'); class I18n extends I18n_Core {} |
Этот пустой класс позволяет переопределить методы I18n::get()
и I18n::load()
, чтобы обеспечить реализацию новых возможностей. Например, поиск сообщений в формате ‘foo.bar.baz‘. При этом перевод будет разыскиваться в любом случае, даже если язык перевода совпадает с текущим (в этом отличие от стандартной функции __()
).
Обратите внимание, что эта замена может привести к ошибкам в Kohana 3.1.x. Дело в том, что обычно вызов
I18n::lang()
прописан вbootstrap.php
выше, чемKohana::modules()
, и в итоге классI18n_Core
не будет найден. Просто перенесите вызовI18n::lang()
ниже и проблема будет решена.
Как и в случае со стандартным I18n, переводы хранятся в файлах папки i18n
, например i18n/ru.php
. Для начала ничего своего добавлять не будем, а посмотрим уже имеющиеся переводы, идущие с модулем.
Стандартные переводы
Основную часть в переводе файла modules/classes/i18n_plural/i18n/ru.php
(я привел путь по умолчанию, он может отличаться) занимает работа с датой/времен и сообщения валидации.
Работа с датой
Наверное вы знакомы с методом Date::formatted_time()
, который позволяет представить строковые (и не только) форматы времени представить в новом формате. Модуль I18n_Plural предлагает еще один метод — I18n_Date::format()
. Проще всего разобраться на примерах:
// используем вывод даты через уже готовые шаблоны $formats = array('long', 'db', 'compact', 'header', 'iso8601', 'rfc822', 'rfc2822', 'short'); foreach($formats as $format) { echo $format.': '.I18n_Date::format($time, $format).'<br/>'; } |
На экране увидим следующие значения
long: Апрель 29, 2011 01:50
db: 2011-04-29 01:50:14
compact: 20110429T015014
header: Fri, 29 Apr 2011 06:50:14 GMT
iso8601: 2011-04-29T01:50:14-05:00
rfc822: Пт, 29 апр 2011 01:50:14 -0500
rfc2822: Fri, 29 Apr 2011 01:50:14 -0500
short: 29 апр 01:50
Обратите внимание, что названия месяцев и дней недели не везде переведены! Это особенности формата, например header нужен для отправки в HTTP-заголовках.
Естественно, можно и собственные форматы использовать. Сама строка формата похожа на формат из функции date(), но перед символами формата надо ставить знак процента. Да и сами символы частенько не совпадают со стандартными, вот часть из них:
- %a — короткое обозначение дня недели (Mon/Tue для английского, Пн/Вт для русского)
- %A — полное обозначение дня недели (Monday/Понедельник и т.д.)
- %b — короткое обозначение месяца (Jan/Feb для английского, Янв/Фев для русского)
- %B — полное обозначение месяца (January/Январь и т.д.)
- %d — день месяца, с ведущим нулем (01-31)
- %D — короткое обозначение дня недели (без перевода, не путать с %a)
- %H — час в 24-часовом формате, с ведущим нулем (01-24)
- %I — час в 12-часовом формате без ведущего нуля (1-12)
- %k — то же, что %H, но вместо нулей будут пробелы
- %l — то же, что %I, но вместо нулей будут пробелы
- %m — порядковый номер месяца (01-12)
- %M — минута, с ведущим нулем (01-59)
- %o — аналог формата ‘jS’ из date(), возвращает номер дня с суффиксом, например 1st, 2nd и т.д. Перевода пока нет!
- %s — UNIX timestamp
- %S — секунды, с ведущим нулем (01-59)
- %w — порядковый номер дня недели (1-7)
- %x — интересный параметр, он позволяет использовать «любимый» формат из конфигурации локали. Лезет в i18n/ru.php за ключом ‘date.short_date’. Например, для русского там формат ‘%d.%m.%Y’ по умолчанию (т.е. ДД.ММ.ГГГГ)
- %X — аналогично, но для времени. Ключ ‘date.short_time’, для русского ‘%H:%M’ (ЧЧ:ММ)
- %y — год (две цифры, 01-99)
- %Y — год, полный (2011 и т.д.)
Эти параметры можно комбинировать, разбавлять прочими символами и т.д. Не очень удобно, что нельзя добавить свои короткие форматы (хотя бы через конфиг) без использования каскадной ФС, возможно чуть позже появится такая фишка. Вообще, метод не выглядит пока таким уж суперским, в большинстве случаев достаточно и обычного date()
. А вот если он научится переводить слова типа 1st/2nd или возвращать дату с учетом склонения (29 Апреля вместо 29 Апрель), то ему цены не будет.
Помимо вывода обычной даты, нередко необходимо показать смещение во времени. В Kohana есть подобный метод — Date::fuzzy_span()
, который возвращает фразы типа ‘1 minute ago‘. В модуле I18n_Plural пошли дальше, и искомая фраза переводится. Вот как это выглядит (сперва результат со стандартным fuzzy_span()
, потом используем метод из I18n_Plural
):
$time = time(); // moments ago echo Date::fuzzy_span($time - 10).'<br/>'; // меньше минуты назад echo I18n_Date::fuzzy_span($time-10).'<br/>'; // in moments echo Date::fuzzy_span($time + 10).'<br/>'; // меньше чем через минуту echo I18n_Date::fuzzy_span($time+10).'<br/>'; // никогда echo I18n_Date::fuzzy_span(FALSE).'<br/>'; |
Новый fuzzy_span()
не только преобразовывает смещение к читабельному текстовому виду, но и переводит его. Переводы хранятся все там же, в файле classes/i18n/ru.php
(для русского).
Сообщения валидации
Если вы уже посмотрели внутренности файлов перевода данного модуля, то заметили наличие сообщений валидации. С модулем идет класс I18n_Validation
, который надо по аналогии с I18n_Date
подложить между Validation
и Kohana_Validation
. В результате вывод ошибок (метод errors()
) будет учитывать потребности модуля локализации.
Казалось бы, зачем нужно переводить и так переведенные сообщения? А дело в основной фишке модуля, о которой я пока молчал — модуль умеет учитывать контекст сообщения и менять содержимое в зависимости от поданных на вход параметров. Например, все те же «1 комментарий», но «2 комментария». В сообщениях валидации это тоже есть, например при проверке на длину строки сообщение должно предусматривать либо «не менее 1 знака», либо «не менее 2 знаков».
Как это выглядит? Просто вызываем метод errors()
с пустой строкой в качестве параметра (т.е. errors('')
). Таким образом, изначальное сообщение будет найдено в файле перевода по ключу valid.правило
. А там уже оно описано в зависимости от контекста, например вот так:
'min_length' => array( 'one' => 'Поле :field должно иметь длину хотя бы :param2 знак', 'few' => 'Поле :field должно иметь длину хотя бы :param2 знака', 'other' => 'Поле :field должно иметь длину хотя бы :param2 знаков', ), |
Ключи one/few/other определяют результат в зависимости от переданного контекста, в роли которого выступает обычно первый численный параметр после самого значения. Таким образом, если мы объявляем правило
->rules('foo', array( array('min_length', array(':value', 5)) ) |
то в случае ошибки получим сообщение под ключом ‘other’, а если минимальная длина 4, то уже ‘few’. Правила для различных языков можно посмотреть в данном документе.
Описанные выше переводы используют dot-нотацию сообщений, т.е. в стиле I18n из 2.3.4. Другой способ перевести стандартные сообщения — скопировать их текст из
system/messages/validation.php
и в них уже подставить соответствующие контексты из имеющихся вclasses/i18n/ru.php
. Например, для правилаdecimals
получим такой перевод:
':field must be a decimal with :param2 places' => array( 'one' => 'Поле :field должно содержать число с :param2 десятичным местом', 'other' => 'Поле :field должно содержать число с :param2 десятичными местами', ),Теперь можно обращаться к методу
errors()
с указанием конкретного имени файла, т.е.errors('validation')
.
С валидацией все понятно, давайте уже поближе познакомимся с контекстами.
Контексты
Общая концепция такова — в файле перевода указываются различные формы перевода, в зависимости от переданного параметра. Это необязательно формы числительных (one/few/many), но могут быть вообще любые ключи. Вот пример из документации:
// в файле перевода объявили два собственных контекста, f и m return array( 'Their name is :name' => array( 'f' => 'Her name is :name', 'm' => 'His name is :name', ), ); // используем в коде приложения, не забываем передать контекст, в данном случае это пол echo ___('Their name is :name', $gender, array(':name' => $name)); |
Используется новая функция для перевода — с тремя подчеркиваниями. Она отличается от стандартной тем, что добавлен второй параметр — это контекст. В остальном все то же самое.
Замечу, что если контекст не передан (а он ожидается в виде строки или числа), то можно использовать синтаксис стандартной функции
__()
, т.е.___('translate me, :user', array(':user' => 'username'))
будет работать.
Давайте рассмотрим пример из дефолтного перевода:
// Plural test ':count files' => array( 'one' => ':count файл', 'few' => ':count файла', 'many' => ':count файлов', 'other' => ':count файла', ), |
Ну и соответственно различные варианты использования:
$files = array(0,1,4,6,10,11,27,31); foreach($files as $count) { echo ____(':count files', $count, array(':count' => $count)).'<br/>'; } |
Тут мы проверили корректность работы с различным количеством файлов. Работает как часы. Наверняка вы обратили внимание на наличие ключа ‘other’ — это ключ для получения дефолтного контекста. Так как он совпадает с ключом ‘few’, то можно было ключ ‘few’ и не писать.
Переводы можно вкладывать друг в друга, как в этом примере из обсуждения на форуме:
echo ___('I\'ve scanned :where and found :what', array( ':where' => ___(':count directories', $x, array(':count' => $x)), ':what' => ___(':count files', $y, array(':count' => $y)), )); |
Итоги
Очень любопытный модуль. Если вы не планируете разворачивать у себя в проекте gettext, но при этом есть желание создать мультиязычный сайт, то I18n_Plural будет весьма кстати. Автор достаточно оперативно реагировал на мои поправки во время создания данной статьи, так что сомнений в дальнейшей поддержке и развитии модуля у меня лично нет.
Большое спасибо, Иван!
что-то как-то запутано.
@maxnag
Да все просто! Разница с обычным __() только в наличии контекста (обычно это число). Не настолько там уж много возможностей, чтобы запутаться. Надо просто попробовать
Спасибо, как раз то что нужно
«.. пропала возможность использовать синтаксис из 2.3.4 вида __(‘user.profile.fname’)».
Насколько Я помню она не пропала — ее заменили на конструкцию вида Kohana::message(…). По крайней мере shadowhand где-то на форуме рекомендовал использовать именно ее. У меня в init.php основного модуля расширения Kohana 3.x написана такая конструкция:
if ( ! function_exists(‘__ko2′)):
// —
function __ko2($key, $default = NULL)
{
if (strpos($key, ‘.’) === FALSE)
return $default;
list($file, $path) = explode(‘.’, $key, 2);
return Kohana::message(I18n::$lang.DIRECTORY_SEPARATOR.$file, $path, $default);
}
// —
endif;
Работает точно также как в Kohana 2.x
@stalker
Да, конечно, она есть. Речь в принципе шла о классе I18n.
Кстати, зря делаете переводы файлов message. Типичная последовательность действий — перевести строку с точками в некое текстовое сообщение (это работа для Kohana::message()), которое потом будет автоматически переведено на нужный язык (внутри есть вызов __()). То есть в messages мы храним только расшифровку на дефолтном языке (обычно английский), а сами переводы как обычно — в I18n.
@biakaveron
Не-не-не .. Это была заглушка на время перехода с KO2 на KO3. Сейчас её уже не используем. Я хотел обратить внимание на то, что сама «возможность использовать синтаксис из 2.x» все-таки осталась (хотя и не без костылей