Контент


Новый взгляд на I18n

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

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.

Теги: , , .


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

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

  1. ButscH пишет:

    Большое спасибо, Иван!

  2. maxnag пишет:

    что-то как-то запутано.

  3. biakaveron пишет:

    @maxnag
    Да все просто! Разница с обычным __() только в наличии контекста (обычно это число). Не настолько там уж много возможностей, чтобы запутаться. Надо просто попробовать :)

  4. Ant0ha пишет:

    Спасибо, как раз то что нужно

  5. stalker пишет:

    «.. пропала возможность использовать синтаксис из 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

  6. biakaveron пишет:

    @stalker
    Да, конечно, она есть. Речь в принципе шла о классе I18n.

    Кстати, зря делаете переводы файлов message. Типичная последовательность действий — перевести строку с точками в некое текстовое сообщение (это работа для Kohana::message()), которое потом будет автоматически переведено на нужный язык (внутри есть вызов __()). То есть в messages мы храним только расшифровку на дефолтном языке (обычно английский), а сами переводы как обычно — в I18n.

  7. stalker пишет:

    @biakaveron
    Не-не-не .. Это была заглушка на время перехода с KO2 на KO3. Сейчас её уже не используем. Я хотел обратить внимание на то, что сама «возможность использовать синтаксис из 2.x» все-таки осталась (хотя и не без костылей :)

Продолжение обсуждения

  1. Вопросы, появившиеся при использовании валидации kohana — Простые решения сложных проблем ссылается на эту запись on 29 января 2012

    [...] переменная, в которой значение 3? Ответ нашёл здесь, малость погуглив. Нужно [...]



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

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