По многочисленным просьбам, поступающим в обратную связь, и по повышенной потребности аудитории Yii-разработчиков хочу поделиться некоторыми моментами в реализации поддержки нескольких языков на любом проекте, написанном на этом фрэймворке.
Постановка задачи
Итак, на нашем сайте необходимо:
Научиться извлекать текущий язык из адреса;
Заготовить варианты всех служебных надписей для элементов интерфейса (например, заголовки портлетов, attributeLabels полей моделей, текста уведомлений и т. д.) на разных языках;
Научить все необходимые модели хранить несколько вариантов заголовков и текстов для каждого языка;
Разместить ссылки для переключения на разные языки.
Использование интернационализации
Для вывода вариантов перевода надписей интерфейса достаточно ознакомиться с описанием интернационализации в руководстве.
Чтобы у нас всё работало корректно, необходимо указать приложению исходный и текущий язык:
return array(
'sourceLanguage'=>'en',
'language'=>'ru',
...
)
Пусть у нас есть модель Post с надписями:
class Post extends CActiveRecord
{
public function attributeLabels()
{
return array(
'title' => 'Заголовок',
'text' => 'Текст',
'category_id' => 'Категория',
);
}
}
И есть портлет для вывода последних записей:
<?php $this->widget('LatestPostsPortlet', array('title' => 'Последние записи')); ?>
Теперь в каталоге protected/messages/ru нужно создать файл blog.php со списком переводов на русский язык:
return array(
'Title' => 'Заголовок',
'Text' => 'Текст',
'Category' => 'Категория',
'Latest posts' => 'Последние записи',
...
);
И везде в коде перейти на использование функции Yii::t:
class Post extends CActiveRecord
{
public function attributeLabels()
{
return array(
'title' => Yii::t('blog', 'Title'),
'text' => Yii::t('blog', 'Text'),
'category_id' => Yii::t('blog', 'Category'),
);
}
}
<?php $this->widget('LatestPostsWidget', array('title' => Yii::t('blog', 'Latest posts'))); ?>
Аналогично нужно к этому переводу en→ru подготовить переводы en→de, en→fr и так далее. Переводить надписи фреймворка не надо, так как у него все свои переводы уже есть.
Теперь для вывода надписей нашего сайта на другом языке нужно изменить значение параметра language в конфигурационном файле:
return array(
'sourceLanguage'=>'en',
'language'=>'fr',
...
)
Теперь переводы будут браться из аналогичного файла blog.php из каталога protected/messages/fr.
Если на сайте используются модули, и Вы хотите хранить переводы в папках внутри модуля блога, то вместо
Yii::t('blog', 'Title');
используйте имя класса модуля:
Yii::t('BlogModule.blog', 'Title');
Теперь можно сложить файлы blog.php в языковые поддиректории папки protected/modules/blog/messages.
Теперь рассмотрим автоматическое переключение языка.
Указание языка в адресе
Параметры конфигурационного файла – это ни что иное, как свойства объекта CApplication, доступного через Yii::app(), поэтому мы в любом месте можем переключить текущий язык:
Yii::app()->language = 'fr';
Теперь нам необходимо обеспечить извлечение языка из URL текущей страницы и произвести присвоение Yii::app()->language.
Решений, на самом деле много. В большинстве своём они сводятся к указанию языка в виде префикса адреса.
'rules'=>array(
'<language:(ru|en|de)>' => 'site/index',
'<language:(ru|en|de)>/<action:(contact|login|logout)>' => 'site/<action>',
'<language:(ru|en|de)>/<controller:\w+>/<id:\d+>'=>'<controller>/view',
'<language:(ru|en|de)>/<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',
'<language:(ru|en|de)>/<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
),
то есть в начале масок всех правил необходимо добавить место для языка
<language:(ru|en|de)>.
Теперь где-нибудь до выполнения наших экшенов нужно указать текущий язык приложению. Чаще всего это делают при инициализации контроллера:
class Controller extends CController
{
public function init()
{
if (!empty($_GET['language']))
Yii::app()->language = $_GET['language'];
parent::init();
}
}
А при генерации адресов (используя переопределённый в своём классе метод UrlManager::createUrl) нужно форсировать добавление языка:
return array(
'components'=>array(
'urlManager'=>array(
'class'=>'UrlManager',
...
),
...
),
...
);
class UrlManager extends CUrlManager
{
public function createUrl($route, $params=array(), $ampersand='&')
{
if (empty($params['language'])) {
$params['language'] = Yii::app()->language;
}
return parent::createUrl($route, $params, $ampersand);
}
}
Теперь можно заходить на свой сайт с указанием языка в начале адреса:
http://site.ru/ru
http://site.ru/en
http://site.ru/ru/blog
http://site.ru/en/blog
Здесь и проявляется первый недостаток такого решения. А именно, мы бы хотели заходить на страницы нашего сайта без указания языка «ru»:
http://site.ru
http://site.ru/en
http://site.ru/blog
http://site.ru/en/blog
Чтобы указание языка сделать необязательным, потребуется продублировать все правила:
'rules'=>array(
'<language:(ru|en|de)>' => 'site/index',
'/' => 'site/index',
'<language:(ru|en|de)>/<action:(contact|login|logout)>' => 'site/<action>',
'<action:(contact|login|logout)>' => 'site/<action>',
'<language:(ru|en|de)>/<controller:\w+>/<id:\d+>'=>'<controller>/view',
'<controller:\w+>/<id:\d+>'=>'<controller>/view',
'<language:(ru|en|de)>/<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',
'<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',
'<language:(ru|en|de)>/<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
'<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
),
Теперь, чтобы по умолчанию использовался русский перевод, нужно указать язык явно:
class Controller extends CController
{
public function init()
{
if (empty($_GET['language']))
$_GET['language'] = 'ru';
Yii::app()->language = $_GET['language'];
parent::init();
}
}
И при генерации ссылок не надо дописывать язык, если он русский:
class UrlManager extends CUrlManager
{
public function createUrl($route, $params=array(), $ampersand='&')
{
if (empty($params['language']) && Yii::app()->language !== 'ru') {
$params['language'] = Yii::app()->language;
}
return parent::createUrl($route, $params, $ampersand);
}
}
Согласитесь, что это не очень простой подход, так как для него требуется перерабатывать все правила маршрутизации и немного «засорять» базовый контроллер.
Прозрачная работа с языковыми адресами
Попробуем разрешить проблему языковой адресации без добавления кода в контроллер и указания языка в правилах маршрутизации. В этом нет ничего сложного.
Во-первых, оставим в покое правила маршрутизации, убрав из них язык:
'rules'=>array(
'/' => 'site/index',
'<action:(contact|login|logout)>' => 'site/<action>',
'<controller:\w+>/<id:\d+>'=>'<controller>/view',
'<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',
'<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
),
Раньше мы указывали языки прямо в коде:
'<language:(ru|en|de)>'
...
$_GET['language'] = 'ru';
...
Yii::app()->language !== 'ru'
В нашем новом коде этого не будет, так как это не очень красиво и не гибко. Но теперь мы не знаем, сколько языков поддерживаем. Исправим этот недостаток:
return array(
'sourceLanguage'=>'en',
'language'=>'ru',
...
'params'=>array(
'translatedLanguages'=>array(
'ru'=>'Russian',
'en'=>'English',
'de'=>'Deutsch',
),
'defaultLanguage'=>'ru',
),
);
Мы добавили два параметра для хранения списка поддерживаемых языков и для указания языка, используемого по умолчанию. Кстати, имена их не случайны и мы к этому ещё вернёмся.
Для продолжения обычной работы стандартных правил маршрутизации нам нужно в CHttpRequest::getRequestUri перехватывать и удалять префикс языка, а при создании адреса через CUrlManager::createUrl – добавлять префикс текущего языка снова.
Другими словами, по какому бы адресу мы ни зашли:
http://site.ru/blog
http://site.ru/en/blog
http://site.ru/de/blog
метод Yii::app()->request->getUrl() должен отбрасывать язык и возвращать в любом случае:
/blog
/blog
/blog
А генератор адресов при вызове
Yii::app()->createUrl('blog/index');
в зависимости от текущего значения Yii::app()->language должен генерировать ссылки вида
/blog
/en/blog
/de/blog
То есть, фактически, система внутри кроме доступа к параметру Yii::app()->language вообще не должна понимать, в окружении какого языка она находится на данным момент.
Реализация обработки адресов
Напишем теперь всё, что мы имели в виду в предыдущем пункте. Переопределим стандартные классы своими наследниками.
Парсер адресов:
class DLanguageHttpRequest extends CHttpRequest
{
private $_requestUri;
public function getRequestUri()
{
if ($this->_requestUri === null)
$this->_requestUri = DMultilangHelper::processLangInUrl(parent::getRequestUri());
return $this->_requestUri;
}
public function getOriginalUrl()
{
return $this->getOriginalRequestUri();
}
public function getOriginalRequestUri()
{
return DMultilangHelper::addLangToUrl($this->getRequestUri());
}
}
Генератор адресов:
class DLanguageUrlManager extends CUrlManager
{
public function createUrl($route, $params=array(), $ampersand='&')
{
$url = parent::createUrl($route, $params, $ampersand);
return DMultilangHelper::addLangToUrl($url);
}
}
Теперь эти классы нужно указать в конфигурационном файле:
return array(
'sourceLanguage'=>'en',
'language'=>'ru',
'components'=>array(
'request'=>array(
'class'=>'DLanguageHttpRequest',
...
),
'urlManager'=>array(
'class'=>'DLanguageUrlManager',
...
),
),
...
'params'=>array(
'translatedLanguages'=>array(
'ru'=>'Russian',
'en'=>'English',
'de'=>'Deutsch',
),
'defaultLanguage'=>'ru',
),
);
Если в приложении уже имеются свои классы UrlManager и HttpRequest
class UrlManager extends CUrlManager {...}
class HttpRequest extends СHttpRequest {...}
то вы можете отнаследоваться прямо от них:
class DLanguageUrlManager extends UrlManager {...}
class DLanguageHttpRequest extends HttpRequest {...}
Теперь, собственно, сам DMultilangHelper:
/**
* @author ElisDN <mail@elisdn.ru>
* @link http://www.elisdn.ru
*/
class DMultilangHelper
{
public static function enabled()
{
return count(Yii::app()->params['translatedLanguages']) > 1;
}
public static function suffixList()
{
$list = array();
$enabled = self::enabled();
foreach (Yii::app()->params['translatedLanguages'] as $lang => $name)
{
if ($lang === Yii::app()->params['defaultLanguage']) {
$suffix = '';
$list[$suffix] = $enabled ? $name : '';
} else {
$suffix = '_' . $lang;
$list[$suffix] = $name;
}
}
return $list;
}
public static function processLangInUrl($url)
{
if (self::enabled())
{
$domains = explode('/', ltrim($url, '/'));
$isLangExists = in_array($domains[0], array_keys(Yii::app()->params['translatedLanguages']));
$isDefaultLang = $domains[0] == Yii::app()->params['defaultLanguage'];
if ($isLangExists && !$isDefaultLang)
{
$lang = array_shift($domains);
Yii::app()->setLanguage($lang);
}
$url = '/' . implode('/', $domains);
}
return $url;
}
public static function addLangToUrl($url)
{
if (self::enabled())
{
$domains = explode('/', ltrim($url, '/'));
$isHasLang = in_array($domains[0], array_keys(Yii::app()->params['translatedLanguages']));
$isDefaultLang = Yii::app()->getLanguage() == Yii::app()->params['defaultLanguage'];
if ($isHasLang && $isDefaultLang)
array_shift($domains);
if (!$isHasLang && !$isDefaultLang)
array_unshift($domains, Yii::app()->getLanguage());
$url = '/' . implode('/', $domains);
}
return $url;
}
}
Он и будет обрабатывать все адреса и на основе префикса адреса устанавливать язык приложения. Также мы добавили сюда два вспомогательных метода enabled() и suffixList(). Второй пригодится нам дальше.
Если теперь попробовать зайти по разным адресам
http://site.ru/
http://site.ru/en/
http://site.ru/de/
то строка
<?php echo Yii::app()->language; ?>
должна правильно выводить текущий язык, а выражение
<?php echo Yii::app()->createUrl('blog/view', array('id'=>1)); ?>
должно генерировать URL с префиксом, совпадающим с текущим языком.
Также мы добавили методы для получения «настоящего» адреса текущей страницы. Это может быть полезно, например, для реализации защиты от дубликатов страниц:
public function actionView($id)
{
$model = $this->loadModel($id);
if (Yii::app()->request->originalUrl !== $model->getUrl())
$this->redirect($model->getUrl());
$this->render('view', array('model'=>$model));
}
Виджет переключения языков
В нашем случае метод Yii::app()->request->getUrl() возвращает адрес без префикса, так что можно собрать ссылки на другие языки простой конкатенацией префиксов:
class LanguageSwitcherWidget extends CWidget
{
public function run()
{
$currentUrl = ltrim(Yii::app()->request->url, '/');
$links = array();
foreach (DMultilangHelper::suffixList() as $suffix => $name){
$url = '/' . ($suffix ? trim($suffix, '_') . '/' : '') . $currentUrl;
$links[] = CHtml::tag('li', array('class'=>$suffix), CHtml::link($name, $url));
}
echo CHtml::tag('ul', array('class'=>'language'), implode("\n", $links));
}
}
Теперь достаточно вывести этот виджет в шаблоне:
<?php $this->widget('LanguageSwitcherWidget'); ?>
чтобы вывести меню переключения любой страницы на все поддерживаемые языки.