Мультиязычный сайт на Yii: Элементы интерфейса и URL



По многочисленным просьбам, поступающим в обратную связь, и по повышенной потребности аудитории 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'); ?>


чтобы вывести меню переключения любой страницы на все поддерживаемые языки.
Теги:
Мультиязычный сайт, Yii, Элементы интерфейса, URL
Добавлено: 16 Февраля 2015 20:47:00 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...