Yii и мультиязычный сайт. Правильные URL и гибкость в работе


При написании одного проекта, возникла необходимость в организации мультиязычности на сайте. Причем количество языков не должно ограничиваться двумя и URL должны быть человеко-понятные и SEO оптимизированные. Тоесть ссылки на сайте должны быть вида:
http://mysupersite.ru/ru/contacts для русского языка
http://mysupersite.ru/en/contacts для английского языка
Так как опыт у меня не очень большой, я начал вопрошать гугл. Вариантов, как оказалось, достаточно много, однако из всех мне приглянулся один вариант, который я использовал и слегка модифицировал.

1. Расширяем CUrlManager.


Создаем файл 'components/UrlManager.php' со следующим содержимым:
<?php
class UrlManager extends CUrlManager
{
    public function createUrl($route,$params=array(),$ampersand='&')
    {
        if (!isset($params['language'])) {
            if (Yii::app()->user->hasState('language'))
                Yii::app()->language = Yii::app()->user->getState('language');
            else if(isset(Yii::app()->request->cookies['language']))
                Yii::app()->language = Yii::app()->request->cookies['language']->value;
            $params['language']=Yii::app()->language;
        }
        return parent::createUrl($route, $params, $ampersand);
    }
}
?>

Согласно нашему условию, выбранный язык должен быть частью URL. Это значит, что $_GET['language'] должен быть определен. Для реализации этого мы переопределяем функцию createUrl() класса CUrlManager. Если язык в строке не указан, тогда мы его ищем в переменной сессии, затем в кукисах, и если до этого пользователь не менял язык то устанавливаем язык приложения по умолчанию. И затем формируем правильную строку URL уже с языком как параметр.

2. Редактируем наш Controller


Добавляем следующий код в 'components/Controller.php'
<?php
public function __construct($id,$module=null){
    parent::__construct($id,$module);
    // If there is a post-request, redirect the application to the provided url of the selected language 
    if(isset($_POST['language'])) {
        $lang = $_POST['language'];
        $MultilangReturnUrl = $_POST[$lang];
        $this->redirect($MultilangReturnUrl);
    }
    // Set the application language if provided by GET, session or cookie
    if(isset($_GET['language'])) {
        Yii::app()->language = $_GET['language'];
        Yii::app()->user->setState('language', $_GET['language']); 
        $cookie = new CHttpCookie('language', $_GET['language']);
        $cookie->expire = time() + (60*60*24*365); // (1 year)
        Yii::app()->request->cookies['language'] = $cookie; 
    }
    else if (Yii::app()->user->hasState('language'))
        Yii::app()->language = Yii::app()->user->getState('language');
    else if(isset(Yii::app()->request->cookies['language']))
        Yii::app()->language = Yii::app()->request->cookies['language']->value;
}
public function createMultilanguageReturnUrl($lang='en'){
    if (count($_GET)>0){
        $arr = $_GET;
        $arr['language']= $lang;
    }
    else 
        $arr = array('language'=>$lang);
    return $this->createUrl('', $arr);
}
?>


Мы расширяем конструктор класса и добавляем язык для приложения. Так как все контроллеры будут наследоваться с этого контроллера, язык приложения будет установлен явно на каждый запрос.
Если не установленYii::app()->language явно для каждого запроса в URL, он будет браться из конфигурационного файла приложения. Если же он не указан в конфигурационном фале, он будет идентичен Yii::app()->sourceLanguage, который по умолчанию 'en_us'.
Все эти параметры можно изменить в конфигурационном файле protected\config\main.php
'sourceLanguage'=>'en',
'language'=>'ru',

3. Создаем Language Selector Widget


Создаем файл в 'components/widgets/LanguageSelector.php' со следующим содержимым:
<?php
class LanguageSelector extends CWidget
{
    public function run()
    {
        $currentLang = Yii::app()->language;
        $languages = Yii::app()->params->languages;
        $this->render('languageSelector', array('currentLang' => $currentLang, 'languages'=>$languages));
    }
}
?>


И вьюху для нашего виджета 'components/widgets/views/languageSelector.php':

<div id="language-select">
<?php
    if(sizeof($languages) < 4) { // если языков меньше четырех - отображаем в строчку
        // Если хотим видить в виде флагов то используем этот код
        foreach($languages as $key=>$lang) {
            if($key != $currentLang) {
                echo CHtml::link(
                     '<img src="/images/'.$key.'.gif" title="'.$lang.'" style="padding: 1px;" width=16 height=11>', 
                     $this->getOwner()->createMultilanguageReturnUrl($key));                };
        }
        // Если хотим в виде текста то этот код
        /*
        $lastElement = end($languages);
        foreach($languages as $key=>$lang) {
            if($key != $currentLang) {
                echo CHtml::link(
                     $lang, 
                     $this->getOwner()->createMultilanguageReturnUrl($key));
            } else echo '<b>'.$lang.'</b>';
            if($lang != $lastElement) echo ' | ';
        }
        */
    }
    else {
        // Render options as dropDownList
        echo CHtml::form();
        foreach($languages as $key=>$lang) {
            echo CHtml::hiddenField(
                $key, 
                $this->getOwner()->createMultilanguageReturnUrl($key));
        }
        echo CHtml::dropDownList('language', $currentLang, $languages,
            array(
                'submit'=>'',
            )
        ); 
        echo CHtml::endForm();
    }
?>
</div>

Для отображения флагов, необходимо разместить в папке /images/ указатели языков с именами типа en.gif, ru.gif, ua.gif, md.gif.

4. Размещаем Widget на сайте


Добавлем следующий код внутри header-div в 'views/layouts/main.php'

<div  id="language-selector" style="float:right; margin:5px;">
    <?php 
        $this->widget('application.components.widgets.LanguageSelector');
    ?>
</div>

5. Редактируем Конфигурационный файл приложения


<?php
'components'=>array(
    ...
    'request'=>array(
        'enableCookieValidation'=>true,
        'enableCsrfValidation'=>true,
    ),
    'urlManager'=>array(
        'class'=>'application.components.UrlManager',
        'urlFormat'=>'path',
        'showScriptName'=>false,
        'rules'=>array(
            '<language:(ru|ua|en)>/' => 'site/index',
            '<language:(ru|ua|en)>/<action:(contact|login|logout)>/*' => 'site/<action>',
            '<language:(ru|ua|en)>/<controller:\w+>/<id:\d+>'=>'<controller>/view',
            '<language:(ru|ua|en)>/<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',
            '<language:(ru|ua|en)>/<controller:\w+>/<action:\w+>/*'=>'<controller>/<action>',
        ),
    ),
),
'params'=>array(
    'languages'=>array('ru'=>'Русский', 'ua'=>'Українська', 'en'=>'English'),
),
?>

6. Добавляем .htaccess


RewriteEngine on

# if a directory or a file exists, use it directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

# otherwise forward it to index.php
RewriteRule . index.php


Вот вроде бы и все. У меня заработало и глюков пока не замечено. На вопросы отвечу.
Информацию почти всю взял отсюда: SEO-conform Multilingual URLs + Language Selector Widget (i18n)
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 67
  • +3
    В общем неплохо, но меня смутил один момент.
    Вот часть кода назначения локали вообще без какой либо проверки

    if(isset($_GET['language'])) {
        Yii::app()->language = $_GET['language'];


    Если потом во view кто-то напишет например так:
    echo 'Your locale: '.Yii::app()->language

    то через урл можно сделать xss. (теоретически)
    • 0
      Надо так:
      Yii::app()->request->getParam('language');
      • +4
        чем вам поможет getParam?
        • 0
          Оу… Параметр не эскейпится. Ушел гуглить.
      • 0
        Мне кажется если будет ХСС то это вопрос к тому, кто писал view. Заранее ескейпить не стоит. т.к данные до того момента когда они нужны должны оставаться as is.
        • 0
          Я придерживаюсь мнения что лучше 2 раза защитится чем схлопотать потом. Тем более что если параметр language будет содержать что-то кроме 2 латинских букв (в крайнем случае больше двух) то понятно что он не валидный и что-то пошло не так. А «лишняя» информация тут потребоваться не может.
          • 0
            Если два раза защититься, то вылезут символы экранирования пользователю. Так что нифига не лучше 2 раза защититься. Лучше 1 раз защититься нормально.
            Ну а проверить lang на 2 латинских буквы — да, вполне можно.
            • 0
              Этот пример скорее исключение. Конечно если мы 2 раза экранируем символы ничего хорошего не будет. Но если мы 2 раза проверим id товара на integer или если 2 раза проверим язык на допустимый, или 2 раза из введенного текста вырежем зараженные фрагменты ничего плохого не будет. Я не говорю что нужно делать все 2 раза. Понятно что если мы 100% уверены что этот фрагмент кода получит безопасный контент проверять что либо бессмысленно. Но если есть сомнения, я лучше второй раз сделаю проверку.
              • 0
                Ну, имхо, два раза делать что-то впринципе — глупо. Надо чётко знать, что и где необходимо экранировать, иначе прям антипаттерн получается — программирование наугад. «А давайте ещё тут поставим проверку, на всякий случай».
                Я согласен, что необходимо сделать валидацию в данном конкретном случае, но надо не просто проверить на «две латинских буквы», а проверить, есть ли такой язык и если нету, то переадресовать пользователя на язык-по-умолчанию. Ведь пользователь может увидеть, скажем где-то в адресе: eng.example.com и заменить его на rus.example.com, которого в системе может и не быть.
                • 0
                  Просто в данном случае валидацию языка надо делать ДО каких-либо других действий, связанных с выбранным языком. Как минимум надо проверить поддерживает ли сайт вообще указанный пользователем китайский язык или нет. А валидированный код языка эскейпить уже ни к чему. Ну, вобщем, это скорее не к вам коммент, а к таралу.
                  • 0
                    Далеко не всегда четко знаешь какие данные 100% валидные а какие стоит фильтровать. Тем более что бывают ситуации когда все указывает что тут могут быть только безопасные данные, а на самом деле все может быть по другому. В больших проектах эта ситуация еще больше заметна. Хорошие пример к сожалению в голову сейчас не приходит.
                    • 0
                      У меня обычно каждый объект который принимает данные из вне делает хоть и минимальную но проверку (конечно не всех данных, а скорее ключевых для него).
                      • 0
                        MVC же! О чем вы вообще говорите?
                        • 0
                          И это что означает что у меня не может быть объектов которые принимают данные? Вы хотите сказать что в MVC кроме как представления, контроллера и модели ничего нету?
                          • 0
                            Ещё может быть (судя по реализациям — должен быть) фронт-контроллер :)
                          • 0
                            Если у меня в рамках серверного MVC должен вводиться JS-код, исполняемый со стороны клиента? тупая экранизация всего подряд здесь не поможет.
                • 0
                  А тут уже понадобились для присваивания Yii::app()->language со вполне определенной семантикой и фиксированным набором допустимых значений.
                • +1
                  echo CHtml::encode($str) спасет отца демократии.
                • 0
                  Ну впринципе можно было просто выбранный язык просмотреть существует ли он в локали, или взять из запроса. Я думаю можно было сделать и попроще, щас заминусуют правда :) Вся задача то сводится: поправить urlManager и проверить наличие языка в системе, а тут нагородили)
                  • +2
                    Ой. Теперь понял. Проверка происходит в роутах.
                    <language:(ru|ua|en)>
                    • 0
                      В общеобразовательных целях спрашиваю — сильно ли плохо — не иметь параметра «язык» в uri? (язык хранится в сессии\кукисах).
                      • 0
                        Поисковая машина будет индексировать ваш сайт всегда на одном языке. Ни один из поисковиков не передает выданные ему ранее cookie.
                        • +2
                          Человек обновил страницу — текст исчез, появился другой.
                          Поисковик не нашёл одну из версий. Заказчик теряет клиентов на одном из языков. Выясняется не сразу.,
                          Два разных бота одного поисковика решили, что их обманывают и по одному URL два разных текста ботам и людям выдают. Сайт выкидывают из индекса.
                          Человек добавил страницу в закладки, вышел, зашёл — там не понятно.
                          Человек кинул ссылку другу, а там другу ничего не понятно.
                          Делаем суперпроизводительный сайт в кластере, отказались от сессий, сайт сломался.

                          Сильно ли плохо — решайте сами. Моё мнение — ужасно.
                        • +1
                          переименуйте ссылки-примеры, сейчас сайт по феншую переживает адскую нагрузку :-)
                          • 0
                            173 посетителя фигня.
                          • 0
                            А почему вы используете «ua» для обозначения украинского языка вместо «uk»?
                            • 0
                              uk больше united kingdom
                              • +1
                                uk — Великобритания, en — английский язык
                                ua — Украина, uk — украинский язык
                                • 0
                                  очевидно же, что пользователи будут путаться. а вообще есть нормальное обозначение локалей вида UK_en, US_en, RU_ru и т.д. )
                                  • 0
                                    конечно пользователи будут путаться если смешивать коды разных сущностей (стран и языков) в одном наборе.
                                    еще можно использовать 3-х символьные коды языков (rus, eng, ukr и т.п.)
                            • +1
                              Не относительно темы, но все же: меня в Yii всегда смущала любовь к статическим методам и классам. Взять тот же CHtml. Захочу я переделать метод для текстового поля, чтобы он мне, допустим, дефолтный класс приписывал, так придется или в исходник лезть, либо по всему проекту менять вызов с CHtml на мой собственный наследник. Если была бы возможность получать объект из реестра или через $this->chml во вьюхе, было бы в разы проще.

                              Опять же, в кажом проекте нужно повторять одну и ту же вещь — создавать собственный базовый контроллер и наследовать свои контроллеры уже от него, чтобы в очередной раз не изменять все контроллеры когда придется вычленить общий функционал в базовый контроллер.
                              • 0
                                Например, мне достаточно CHtml, а вам нет.
                                Если во фреймворке заранее будет излишняя слоистость, то я от неё не смогу избавиться, а если наоборот, то всё ок, т.к. вы всегда сможете добавить ещё один слой.
                                Или тормоза у всех, или только у тех, кому это надо для реализации задачи.
                                • 0
                                  в том и дело, что в, например, Zend Framework можно на более позднем уровне заменить класс хелпера своим без необходимости править 90% вьюшек. В случае с Chtml вы не сможете добавить еще один слой в рабочий проект. Была бы возможность в PHP динамически подменять методы в классе, все ок было бы, чем собственно и любит заниматься Rails — идейный вдохновитель Yii.

                                  Опять же, торомоза от того, что вместо статического класса используется объект, который инициализируется в бутстрапе — звучит странно.
                                  • 0
                                    Для переопределения методов можно использовать runkit_method_redefine. Правда надо добавить runkit в php для этого.
                                • 0
                                  Интересно чем же это обычная практика использования ООП может быть такой назойливой. Не хотите для каждого проекта создавать свой базовый контроллер — не создавайте. Это не значит что разработчики заранее должны предугадать все Ваши хотелки.
                                  • +1
                                    Это, кстати, совершенно не обычная практика ооп. Архитектура на самом деле хромает.
                                    С другой стороны на практике оно реально мало когда нужно.
                                    • +2
                                      Сравнить с теми же Rails, в которых можно без проблем делать monkey patching или дописывать в существующий класс, даже там есть базовый ApplicationController, в котором описываются всякие вызовы к acl, auth и прочие общие вызовы. Просто это реально удобней, и я не припомню проектов, где не приходилось бы создавать собственный контроллер, за исключением каких-то визиток.

                                      Поймите, мне нравится Yii. По сравнению с ололо-энтерпрайз-спринг-на-пхп Симфони 2 или давайте-будем-юзать-как-можно-больше-паттерном ЗФ Yii просто няшка, но на проект побольше пришлось взять ЗФ, потому что в нем можно вносить изменения по необходимости, а не предугадывать их в начале разработки. К примеру, возникла необходимость добавить логирование переводимых фраз. В зф это решается подключением собственного адаптера при инициализации приложения. В Yii нужно было либо заменять все вызовы Yii::t, либо хачить фреймворк (может сейчас уже другая архитектура, я говорю за два года назад). Аналогично и со всеми вызовами статики.

                                      Я считаю, что Yii стоит подтянуть менеджмент зависимостей в коде если он хочет добиться того же положения, что и Rails в своей среде. Понятно, что из-за особенностей пхп придется кое где пожертвовать парой символов, но жизнь это облегчит основательно.
                                      • 0
                                        Не могу сказать как было раньше, но сейчас думаю задачу с логгированием переводимых фраз точно так же можно решить конфигурированием компонента приложения MessageSource.
                                        • 0
                                          «По сравнению с ололо-энтерпрайз-спринг-на-пхп Симфони 2 или давайте-будем-юзать-как-можно-больше-паттерном ЗФ Yii просто няшка»

                                          очень мило )
                                      • 0
                                        На счет базового контроллера согласен, могли бы его в стандартном webapp генерировать.
                                        • –1
                                          Этом случае вам помогут поведения.
                                          • Прошу прощения за бред выше. Вообще не о том подумал.
                                          • –1
                                            Вроде ж, в новой версии Yii можно подменять фреймворковые классы своими. Всякие CHtml и прочие. Ни одна IDE, конечно, не распознает этого финта, поэтому автодополнения кода не дождаться, но на уровне фреймворка — можно, работать будет. Хотя сам пока не пробовал.
                                          • +1
                                            *Українська
                                            • +1
                                              Почему в контроллере используется __construct а не init?
                                              • +5
                                                Или еще лучше вынести в поведения (behaviors), что бы не засорять контроллер, например:

                                                class LanguageBehavior extends CBehavior
                                                {
                                                
                                                    public function attach($owner)
                                                    {
                                                        $owner->attachEventHandler('onBeginRequest', array($this, 'handleLanguageBehavior'));
                                                    }
                                                
                                                    public function handleLanguageBehavior($event)
                                                    {
                                                        $app  = Yii::app();
                                                        $user = $app->user;
                                                
                                                        if (isset($_GET['_lang']))
                                                        {
                                                            $app->language = $_GET['_lang'];
                                                            $user->setState('_lang', $_GET['_lang']);
                                                            $cookie = new CHttpCookie('_lang', $_GET['_lang']);
                                                            $cookie->expire = time() + (60 * 60 * 24 * 365); // (1 year)
                                                            $app->request->cookies['_lang'] = $cookie;
                                                            /*
                                                            * другой код, например обновление кеша некоторых компонентов, которые изменяются при смене языка
                                                            */
                                                        }
                                                        else if ($user->hasState('_lang'))
                                                            $app->language = $user->getState('_lang');
                                                        else if (isset($app->request->cookies['_lang']))
                                                            $app->language = $app->request->cookies['_lang']->value;
                                                    }
                                                
                                                }
                                                


                                                Ну и в конфиге:

                                                    'behaviors' => array(
                                                        ...
                                                        'onBeginRequest' => array(
                                                            'class'  => 'application.components.behaviors.LanguageBehavior'
                                                        ),
                                                    ),
                                                


                                                Кстати, довольная мощная штука, особенно если их динамически цеплять.
                                                • 0
                                                  5. Редактируем Конфигационный файл приложения

                                                  только мне это режет глаз? :)
                                                  • 0
                                                    Спасибо что обратили внимание, подправил.
                                                  • 0
                                                    Почему все поголовно забывают, что дефолтный язык передается браузером в хедерах? Не забывайте про это!
                                                    • 0
                                                      А поисковики передают? Не получится так, что у гугла и яндекса разные главные страницы?
                                                      • 0
                                                        Смотрите, у вас есть следующие источники языка в порядке приоритета:

                                                        1. Идентификатор в URL.
                                                        2. Данные в сессии/кукисе (но зачем? но пусть будет, например).
                                                        3. Хедер в браузере.
                                                        4. Некий дефолт самого приложения.

                                                        Если вы не находите идентификатор в каком-то из источников, вы топаете на уровень ниже и делаете редирект 302 на страницу с правильным URL-ом (иначе поисковики сломаются). Таким образом вас вообще не волнует что шлют поисковые пауки — у вас всегда есть свой дефолт. К тому же вы на каждом этапе должны проверять поддерживает ли указанный язык сайт или нет, только дефолт не проверяется. Ну и всё, problem solved!
                                                    • 0
                                                      mysupersite.ru/ru/contacts для русского языка
                                                      mysupersite.ru/en/contacts для английского языка

                                                      Не лучше ли было бы так?
                                                      mysupersite.ru/ru/kontakty / mysupersite.ru/ru/контакты
                                                      mysupersite.ru/en/contacts

                                                      Плюс можно было UrlManager сделать так, чтобы «language:(ru|ua|en)» в каждом роуте не писать — всё-таки копипаста. Добавление/удаление языка выливается в найти/заменить. Пока языков мало, выглядит сносно, а как быть с тридцатью языками?
                                                      • 0
                                                        Для пользователя абсолютно всё-равно, что написано в урле. Но это сикрет, никому не говорите!
                                                        • 0
                                                          В целом да. Чую, скоро урлы вообще скроют из интерфейса браузеров и не дадут включать. :) Но поисковикам пока не совсем пофиг, да и когда в аське кидаешь ссылку — текст полезен.
                                                          • 0
                                                            Для поисковика урлы разные, если в них хотя бы один разный символ. То есть /ru/contacts и /en/contacts — разные URL-ы. А для программиста и контент-менеджера писать для каждого языка свой брэдкрамб — это пипец. Так что вот, не придумывайте сущности без их необходимости. KISS, так сказать.
                                                            • +1
                                                              Заказчик может захотеть индивидуальные урлы для каждого языка. Если страниц и языков относительно немного, то вполне себе адекватное желание. Если перевод страницы контактов одолели, то перевести хлебную крошку для неё — это сразу «пипец»? :)

                                                              Я-то сам предпочитаю иметь единые английские урлы, но не отвечать же заказчикам из-за этого: «Не сделаю, ты хочешь странного».
                                                              • 0
                                                                Если заказчик попросит, то мы сделаем translates на поле и всё, problem solved. Ну я хз как это будет в PHP, но в рельсах вот так, одной строчкой.
                                                              • 0
                                                                Точно знаю, что поисковики обрабатывают транслит и кириллицу в урлах на предмет релевантности запросу. А вот переведут они запрос с русского на английский — под сомнением.
                                                                • 0
                                                                  Хаха, а ещё надо мета теги заполнять, да?
                                                                  • 0
                                                                    Над урлами я лично проводил эксперименты. /kontakty вс /contacts показали преимущество в 100% запросов по гуглу и яндексу, что я придумал со словом «контакты», включая такие популярные как «рога и копыта контакты».

                                                                    Никаких внешних ссылок, никакого склеивания, одинаковые (в рамках эксперимента) пинги. Даже давал фору — contacts добавлял на несколько секунд раньше. Не помогла фора.
                                                        • 0
                                                          А чем LangUrlManager не устроил?
                                                          • 0
                                                            А как будет работать с модулями?
                                                            • +1
                                                              А что именно смущает?
                                                              • 0
                                                                Полагаю что для модулей нужно прописывать отдельные роутинги.
                                                                • 0
                                                                  В целом да. Параметры в URL передаются, и для верной отработки надо будет в модуль добавить что-то типа

                                                                  class AdminModule extends CWebModule {
                                                                     public function init() {
                                                                          Yii::app()->getUrlManager()->addRules(
                                                                            array(
                                                                                '<language:(ru|ua|en)>/admin' => 'admin',
                                                                                '<language:(ru|ua|en)>/admin/<controller:\w+>' => 'admin/<controller>',
                                                                                '<language:(ru|ua|en)>/admin/<controller:\w+>/<action:\w+>' => 'admin/<controller>/<action>',
                                                                           ));
                                                                  ...
                                                                  

                                                                  (код не проверял, но думаю что должно работать)
                                                                  • 0
                                                                    В общем случае достаточно в конфиге приложения добавить:
                                                                    		'urlManager'=>array(
                                                                    			...
                                                                    			'rules'=>array(
                                                                    				...
                                                                    				'<language:(ru|ua|en)>/<module:\w+>/<controller:\w+>/<action:\w+>/<id:\d+>' => '<module>/<controller>/<action>/<id>',
                                                                    				'<language:(ru|ua|en)>/<module:\w+>/<controller:\w+>/<action:\w+>' => '<module>/<controller>/<action>',
                                                                    				...
                                                                    			),
                                                                    		),
                                                                    

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