Пользователь
0,0
рейтинг
10 марта 2012 в 15:48

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

Yii*

При написании одного проекта, возникла необходимость в организации мультиязычности на сайте. Причем количество языков не должно ограничиваться двумя и 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)
Владимир Жуков @Bamburillo
карма
7,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (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
      Этом случае вам помогут поведения.
      • 0
        Прошу прощения за бред выше. Вообще не о том подумал.
    • –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>',
            				...
            			),
            		),
            
            • 0
              да, так и сделал )

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