Мультиязычный сайт на 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 https://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()->language == Yii::app()->params['defaultLanguage']; if ($isHasLang && $isDefaultLang) array_shift($domains); if (!$isHasLang && !$isDefaultLang) array_unshift($domains, Yii::app()->language); $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');
чтобы вывести меню переключения любой страницы на все поддерживаемые языки.
Осталось теперь сделать модели мультиязычными.
lordiusДмитрий, отличный пост. Побольше бы таких постов....Думаю сделать мультиязычность на основе вашей статьи.
Александр – alexanderschilling.ruИсправьте "protested" на "protected".
Дмитрий ЕлисеевИсправил. Спасибо!
Роман Глебушкиня так и не понял, где они нам пригодятся. Об этих методах больше ни слова не сказано.
А вообще статья хорошая - есть выбор решений и сравнительный анализ.
Дмитрий ЕлисеевВо второй части.
Test – dyii.ruСпасибо за подробное объяснение задачи, лучшее решение!
Применил в своем проекте dyii.ru
Немного пришлось по-разбираться, язык не менялся при разных URI.
Оказалось, что в конфиге параметры для языка по умолчанию
должны быть одинаковые.
Так не работало:
'language'=>'ru', 'params'=>array( 'defaultLanguage'=>'en',
ТимурСпасибо. Сделал все так же как во втором способе, работает частично. После отправки данных через страницу "Контакты" язык сбрасывается на русский. Так же сброс происходит при авторизации.
Дмитрий ЕлисеевМожно переопределить в базовом контроллере метод refresh() для использования getOriginalUrl вместо getUrl:
class Controller extends Ccontroller { public function refresh($terminate=true, $anchor='') { $this->redirect(Yii::app()->getRequest()->getOriginalUrl() . $anchor, $terminate); } }Аналогично можно переопределить CController::redirect() или преобразовывать адрес вручную:
ТимурИспробовал, тот же самый результат.
Может быть при генерации форм (логина или контактов) изначально в action не вставляется язык. Т.е.:
SherzodИсправьте на
Дмитрий ЕлисеевИсправил. Спасибо.
SashaЗдравствуйте,
попробовал реализовать по вашему примеру и столкнулся со следующей ситуацией. Yii::app()->language устанавливается в DMultilangHelper::processLangInUrl(), а вот уже в контроллере и вьюшке ее нет
не могу понять на каком этапе может меняться это свойство
Дмитрий ЕлисеевПопробуйте вывести Yii::app()->language в разных местах, например в Controller::beforeAction.
SashaРешилось добавлением $_GET['language'] = $lang; сразу после Yii::app()->setLanguage($lang);
Владимири все-таки кто-то смог сделать, чтоб в формах генерировался action url в соответствии правилами, описанными во 2 части?
Сейчас у меня генерируется по дефолту. Подскажите что нужно переопределить и как.
Дмитрий ЕлисеевВ крайнем случае можно указать action форм вручную
'action'=>$this->createUrl(''),
ВладимирЭто не самый лучший способ, но крайний случай.
В CActiveForm для генерации action используется CHtml::normalizeUrl()
Можно переопределить метод, дописав это
class DLanguageCHtml extends CHtml { public static function normalizeUrl($url) { $url = parent::normalizeUrl($url); return DMultilangHelper::addLangToUrl($url); } }Но как потом использовать по умолчанию DLanguageCHtml вместо CHtml?
Вот так, добавив в конфиг, НЕ РАБОТАЕТ:
Дмитрий ЕлисеевДля такого перекрытия можно использовать classMap в файле index.php:
require_once($yii); Yii::$classMap=array( 'CHtml'=>'path/to/my/CHtml.php', ); Yii::createWebApplication($config)->run();При этом придётся скопировать весь класс полностью.
ВладимирКак оказалось - это плохая идея. Сломаются ссылки.
Для форм пришлось проставить action, тогда заработало.
Но дальше появились проблемы еще. Любые ссылки, которые проходят через кастомные маршруты, отказались работать корректно.
Пришлось еще расширить 1 класс.
class DLanguageUrlRule extends CUrlRule { public function createUrl($manager,$route,$params,$ampersand) { $url = parent::createUrl($manager,$route,$params,$ampersand); if (false !== $url) { return DMultilangHelper::addLangToUrl($url); } return false; } }И в конфиге чуть добавить:
'urlManager'=>array( 'class' => 'DLanguageUrlManager', 'urlRuleClass' => 'DLanguageUrlRule', ),Продолжаем использовать и тестировать. Дополните статью, наверняка пользователи столкнутся с такими же проблемами как и я
Дмитрий ЕлисеевСпасибо. Отдельные классы для правил я как-то не учёл.
Nikita – slovonline.ruЧтобы форма отправляла по правильному URL
Предлагаю заменить CActiveForm на
class DLanguageActiveForm extends CActiveForm { public function init() { $this->action = DMultilangHelper::addLangToUrl(CHtml::normalizeUrl($this->action)); parent::init(); } }
Одиночка Айс – www.daemonhk.kzВсе работает ))) Еще раз большое спасибо за рецепт, но вот вопрос, как зарезать мультиланг для модулей? К примеру для админки. Можно, конечно, не генерерировать линки, но можно и вручную вбить язык, и тогда ахтунг!
Дмитрий ЕлисеевМожно при инициализации модуля админки вернуть язык назад:
class AdminModule extends CWebModule { public function init() { Yii::app()->language = Yii::app()->params['defaultLanguage']; parent::init(); } }
Одиночка Айс – www.daemonhk.kzПроблема не столько в том, что модуль будет хавать текущий язык, а в том, что можно подставить его (язык) в URL и он опять пропишется в конфиге. Тем более, если мы его поменяем таким образом, как Вы предлагаете, то он станет таким для всего приложения. И еще вопрос, если каждый пользователь будет так извращаться, то какой язык будет в итоге? Допустим я выбрал английский, а кто то через секунду русский. Я весь сайт увижу на русском? Просто не знаю как ведет себя приложение при посещении более чем одним человеком. Просветите, плиз...
Дмитрий ЕлисеевНу если какой-либо модуль нужно выводить только на одном языке, то в нём можно просто делать редирект на нужный язык:
class AdminModule extends CWebModule { public function init() { if (Yii::app()->request->getUrl() != Yii::app()->request->getOriginalUrl()) { $this->redirect(Yii::app()->request->getUrl()); } } }Тогда какой бы язык в адресе пользователь ни написал, он будет перенаправлен на единственно правильный адрес. А так да, при заходе в этот модуль переведётся всё приложение.
AndreyПочему то если ставлю указанный init, выходит ошибка :
В классе SadminModule и его поведениях не найден метод или замыкание с именем "redirect".
Вот полностью класс модуля:
class SadminModule extends CWebModule { public function init() { if (Yii::app()->request->getUrl() != Yii::app()->request->getOriginalUrl()) { $this->redirect(Yii::app()->request->getUrl()); } $this->setImport(array( 'sadmin.models.*', 'sadmin.components.*', )); } public function beforeControllerAction($controller, $action) { if(parent::beforeControllerAction($controller, $action)) { return true; } else return false; } }
AzzzDMultilangHelper :
- processLangInUrl
- addLangToUrl
$domains[0] - хардкод. Не будет работать, если CUrlManager::getBaseUrl() будет возвращать что-то отличное от пустой строки.
Например, если проект находится по адресу http://localhost/yii-project/.
Предлагаю :
- Добавить метод _getLangIndex():
private static function _getLangIndex() { $index = 0; $baseUrl = ltrim(Yii::app()->getBaseUrl(), '/'); if (strlen($baseUrl)) { $baseUrlChunks = explode('/', $baseUrl); if (count($baseUrlChunks) > 0) $index = count($baseUrlChunks); } return $index; }- Изменить processLangInUrl:
public static function processLangInUrl($url) { if (self::enabled()) { $index = self::_getLangIndex(); $domains = explode('/', ltrim($url, '/')); $isLangExists = in_array($domains[$index], array_keys(Yii::app()->params['translatedLanguages'])); $isDefaultLang = $domains[$index] == Yii::app()->params['defaultLanguage']; if ($isLangExists && !$isDefaultLang) { Yii::app()->setLanguage($domains[$index]); array_splice($domains, $index, 1); } $url = '/' . implode('/', $domains); } return $url; }- Изменить addLangToUrl:
public static function addLangToUrl($url) { if (self::enabled()) { $index = self::_getLangIndex(); $domains = explode('/', ltrim($url, '/')); $isHasLang = in_array($domains[$index], array_keys(Yii::app()->params['translatedLanguages'])); $isDefaultLang = Yii::app()->getLanguage() == Yii::app()->params['defaultLanguage']; if ($isHasLang && $isDefaultLang) array_splice($domains, $index, 1); if (!$isHasLang && !$isDefaultLang) array_splice($domains, $index, 0, Yii::app()->getLanguage()); $url = '/' . implode('/', $domains); } return $url; }
Дмитрий ЕлисеевДа, спасибо. BaseUrl надо учесть.
AndreiBaseUrl надо учесть и в методе run - LanguageSwitcherWidget
Александр shaggy – avm.dp.uaПопробовал сделать по описанной методике (последний вариант)
у меня 2 языка - ru, en. по умолчанию - ru.
переключалка языков при переключении на en генерит адрес с префиксом: '_', т.е. на конце: '/_en/' и получаю ошибку 404.
зачем нужно это подчеркивание? если-бы не оно - все-бы работало красиво.
Дмитрий ЕлисеевИсправил. Заменил $suffix на trim($suffix, '_') в переключателе.
Александр shaggy – avm.dp.uaО, теперь все хорошо. работает, благодарю:)
РоманОтличная статья, но второй способ мне не подошел - 40 языков и возможное расширение. Поэтому сделал так:
Controller.php
public function init() { $this->sLangCode = $this->getLangCode(); parent::init(); } public function getLangCode() { // find in path languageCode preg_match('~^(\w{2})(?:/|$)~i', Yii::app()->request->getPathInfo(), $aLangs); if ( !empty($aLangs[1]) ) $sLangCode = $aLangs[1]; else $sLangCode = 'en'; Yii::app()->setLanguage($sLangCode); return $sLangCode; }UrlManager.php
class UrlManager extends CUrlManager { public function createUrl($sRoute, $aParams = array(), $ampersand = '&') { if ( !empty($aParams['lsLang']) ) { $lsLang = 1; unset($aParams['lsLang']); } $sUrl = parent::createUrl($sRoute, $aParams, $ampersand); if ( Yii::app()->sourceLanguage != Yii::app()->language && empty($lsLang) ) { $sUrl = '/' . Yii::app()->language . $sUrl; } return $sUrl; } }main.php
..... 'sourceLanguage'=> 'en', 'language' => 'en', .... '(<language:(\w{2})>/?)?user/<action:\w+>.html' ....Все отлично работает, но не хотелось использовать регулярки. А так как я разбираю Yii только 4й день, то очень хотелось бы услышать советы и критику по этому решению
Дмитрий ЕлисеевНу это распространённый подход как в пункте «Указание языка в адресе». А у Вас правило
'(<language:(\w{2})>/?)?user/<action:\w+>.html' => ...точно работает?
РоманДа. Работает. Но это одно из условий. Их больше, конечно.
(<language:(\w{2})>/?)?Такое идет первым
РоманСпасибо доброму модератору, который отредактировал мое сообщение и привел его в божеский вид.
КонстантинЗдравствуйте! Сделал мультиязычный сайт по вашей статье, основной язык русский, дополнительный английский. Теперь возникла необходимость, чтоб сайт открывался с начальной страницей на английском http://site/en, не получается это сделать. Не подскажите как можно это сделать? Спасибо!
Дмитрий ЕлисеевМожно сделать редирект в .htaccess:
АлександрОбрабатывать язык приложения лучше в поведении, которое присоеденяем к событию onbeginrequest.
ВячеславОтличные статьи! Спасибо Вам огромное за помощь в изучении Yii.
Только исправьте пожалуйста
<?php echo $this->widget('LanguageSwitcherWidget'); ?>на
<?php $this->widget('LanguageSwitcherWidget'); ?>
volofyaА куда кидать классы DLanguageHttpRequest и тд...?
Дмитрий ЕлисеевМожно в папку components или любую другую.
AkulenokПодскажите пожалуйста, как сделать чтобы после отправки формы урл оставался /en сбрасывается
Дмитрий ЕлисеевУ форм указывать action вручную через $this->createUrl('').
Akulenokвсе равно перекидывает на русский
вот форма <?php $form=$this->beginWidget('bootstrap.widgets.TbActiveForm', array(
'action'=>$this->createUrl(''),
в контроллере стоит
if(isset($_POST['LoginForm'])).......
$this->redirect(Yii::app()->homeUrl);
Дмитрий Елисеев
Nikita – slovonline.ruЧтобы форма отправляла запрос по правильному URL
можно заменить CActiveForm на
class DLanguageActiveForm extends CActiveForm { public function init() { $this->action = DMultilangHelper::addLangToUrl(CHtml::normalizeUrl($this->action)); parent::init(); } }
АбайЗдравствуйте Дмитрий! Спасибо за статью, отличная. Возник только один вопрос:
Что будет, если перейти из /en/... на /... (с английского на русский), в функции processLangInUrl, в условии:
if ($isLangExists && !$isDefaultLang) { $lang = array_shift($domains); Yii::app()->setLanguage($lang); }
Дмитрий ЕлисеевНичего. Условие не выполнится и останется тот язык, который указан в настройках.
Akulenokне знаю баг это или нет, но у вас
добавляет слеш / перед урлом
Дмитрий ЕлисеевА если вызвать просто $this->createAbsoluteUrl(...) ?
Akulenokтогда все отлично, без слеша
Евгенийдля правила
после de)> слеш не нужен
ведь в других правилах его нет
а если нужны слеши в конце то можно использовать urlSuffix=>/ для всех правил
Дмитрий ЕлисеевСпасибо! Исправил.
omlk – lomsoft.comУ вас допущена ошибка в классе:
class DLanguageHttpRequest extends CHttpRequest { ... public function getOriginalUrl() { return $this->getOriginalRequestUri(); } ... }Вместо getOriginalUrl() должно бить getUrl()
Дмитрий ЕлисеевЭто не ошибка, а дополнительный метод getOriginalUrl().
omlk – lomsoft.comТогда нужно добавить такой как я написал - не будет работать подстановка языка в формах
RinatВоспользовался вашим рецептом, но проблема возникла со стилями и рисунками, теперь все стили пытаются взяться по адресу:
тогда как они лежат в
RinatИзвиняюсь, разобрался, неправильный путь указал в стилях.
VadeamСпасибо за статью.
Использовал второй метод. Но перестали подхватываться сообщения для перевода из, например для английского, protected/messages/en/front.php, выводятся ключи вместо перевода.
В конфиге компонент задан:
'messages' => array( 'class' => 'CPhpMessageSource', ),На вскидку не могу понять причину. Выводятся все текстовые данные через Yii::t('some_category', 'some_var'), но для русского языка значения из папки protected/messages/ru/front.php работают, для английского нет. Подскажите, пожалуйста, направление.
Спасибо
Дмитрий ЕлисеевПодробнее с языками не разбирался, так что точного ответа не знаю.
VadeamВсем спасибо)
Как обычно водится, дело было не в бабине)
В конфиге было задано свойство приложения 'sourceLanguage'='en'.
Исправил значение на дефолтное 'en_us' и всё заработало как надо. Хотя ранее использовал способ, предложенный здесь, только "перепиленный" для использования кода языка в url, а не на основе поддоменов, и всё работало со значением 'en'.
EveHi Dmitry,
I am trying to implement yii2-multilingual-behavior into the advanced Yii template, but I am having issues with MultilingualBehavior::className() and getting "Class 'frontend\controllers\MultilingualBehavior' not found" error.
I am not sure if I need to add change "use", "namespace" or some similar setting, please let me know what seams to be the problem.
Thanks.
Eve
Дмитрий ЕлисеевHi!
Put your class into directory common/components and set same namespace for one:
namespace common\components; use yii\base\Behavior; class MultilingualBehavior extends Behavior { ... }And use the behavior as:
or:
Best, Dmitry!
EveHi Dmitry,
Thank you for your fast response!
Best,
Eve
Максим – ventures-club.netА можно решить эту задачу без использования createUrl() ? Поясню. У меня мультиязычный проект, переключение языков реализовано через спец. экшн и кукис, а по умолчанию назначается язык из $_SERVER[ACCEPT_LANGUAGE]. Все урлы в проекте абсолютные, и сгенерированы без использования createUrl().
Проблема такого решения - индексация поисковыми роботами, которые не устанавливают $_SERVER[ACCEPT_LANGUAGE] и, соответсвтенно, не принимают кукис.
Можно ли как-то показать текущий язык в URL, не переписывая всю генерацию ссылок проекта?
Дмитрий ЕлисеевНаверное можно.
Максим – ventures-club.netМне пришла идея с динамическим субдоменами языка:
ru.site.com
en.site.com
и т.д. буду пробовать внедрить её.
При таком подходе абсолютные ссылки вида /controller/action, сгенерированные без участия createUrl не поломаются
LAVRIK – lavrik-v.ruПошел вашим путем, но решил доработать...
Вторая статья - https://elisdn.ru/blog/40/yii-based-multilanguage-site-content-translations мне не очень понравилась тем, что по мне так лучше в данном случае все держать в отдельном файле с ассоциативном массиве. Это плохо, для каждой фразы делать запрос в БД.
Но речь не о том....
Нашел недоработку в этой статье!
Дело в том что мой проект лежит НЕ в корневой директории, поэтому processLangInUrl() отрабатывал неверно...
Багу пофиксил двумя строчками в начале данного метода:
$work_dir = str_replace('index.php', '', $_SERVER['SCRIPT_NAME']);
$url = str_replace($work_dir, '', $url);
вроде заработало...
пока ещё не добрался дальше, но подозреваю что addLangToUrl() тоже криво отработает....
LAVRIK – lavrik-v.ruесли честно, метод processLangInUrl() отработал криво...
Взял на себя смелость переписать его:
public static function processLangInUrl($url) { if (self::enabled()) { $domains = explode('/', ltrim($url, '/')); $lang = Yii::app()->params['defaultLanguage']; for ($i=0; $i<count($domains); $i++) { if ( in_array($domains[$i], array_keys(Yii::app()->params['translatedLanguages'])) ) { $lang = $domains[$i]; } } Yii::app()->setLanguage($lang); $url = str_replace($lang.'/', '', $url); // просто уберу его из ссылки, если он там есть } return $url; }
Дмитрий ЕлисеевДа, мой вариант не учитывает showScriptName и baseUrl. В комментариях выше уже об этом пожаловались.
AndreyДобрый вечер, попробовал сделать вариант как на Хабре, в последующем постараюсь разобраться в вашей статье и применить её на практике, возникла небольшая проблемка:
В модуле админа как и положено виджет CMenu формирует ссылки в соответствии с правилами protected/config/main.php , например :
website/ru/admin/page/create
Все бы ничего, но при переходе меня скидывает на какой то action , скорее всего actionIndex , т.е. с ru/ не получается обратиться к нужному экшену. Как быть?
AndreyУточнение:
'urlManager'=>array( 'class'=>'application.components.UrlManager', 'urlFormat'=>'path', 'showScriptName'=>false, 'rules'=>array( '<language:(ru|en|de|ch|tu|ar)>/' => 'site/index', '<language:(ru|en|de|ch|tu|ar)>/<action:(contact|login|logout)>/*' => 'site/<action>', '<language:(ru|en|de|ch|tu|ar)>/<controller:\w+>/<id:\d+>'=>'<controller>/view', '<language:(ru|en|de|ch|tu|ar)>/<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>', '<language:(ru|en|de|ch|tu|ar)>/<controller:\w+>/<action:\w+>/*'=>'<controller>/<action>', '<language:(ru|en|de|ch|tu|ar)>/<module:\w+>/<controller:\w+>/<action:\w+>/<id:\d+>' => '<module>/<controller>/<action>/<id>', '<language:(ru|en|de|ch|tu|ar)>/<module:\w+>/<controller:\w+>/<action:\w+>' => '<module>/<controller>/<action>', ), ),
Дмитрий ЕлисеевА в такой последовательности?
AтвкунХОУ! Теперь в модулее админке ссылки правильно направляют и actions считываются, но в пользовательской части при аналогичном url -
site/ru/page/item/1 ,
Error 400
Некорректный запрос.
И с ru/ в адресной строке и без него(
AndreyПопробовал сделать как тут - создать отдельный config в модуле, только ради rules для админки, но теперь меня из админки выкидывает в клиентскую с сообщением
AndreyЗдравствуйте Дмитрий, скажите пожалуйста:
1. В каком классе Вы пишете этот метод :
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)); }Я попробовал вставить в свой класс Page вместо actionView по умолчанию, но у меня exception вылез:
В классе Page и его поведениях не найден метод или замыкание с именем "getUrl".
2. И еще один момент; теперь пробую обкатать Ваш вариант решения. Вроде бы все как У Вас сделал, в частности эти правила:
или такие:
В любом случае почему - то не получается зайти в domen/module
В первой варианте страницы вида:
http://visakaz/ar/page/item/4
Выдают exception :
Как пробиться к модулям? Помогите Гуру!
AndreyСо вторым пунктом разобрался, выкинул все лишнее из SadminModule.
C первым не понял(
И еще странная штука, когда меня с модуля перекидывает в пользовательскую часть, то при введении логина и пароля вообще ничего не происходит, с чем это может быть связано? Вы выше писали про action вручную , если страничка вида
/login, то action нужно указывать в
$form=$this->beginWidget('CActiveForm', array( ... 'action' => ...?
Если да, то что в нем указывать?
AndreyПопробовал в виджет формы логина вписать :
'action'=>$this->createUrl(''),firebag выдал :
Вроде нормальный action, но почему то авторизация происходит довольно странно. Она проходит, как положено перекидывает site/index, Я вывожу все данные о пользователе, но почему - то Кнопка Login остается активной как при isGuest, в чем сожет быть причина?
AndreyЕще когда я пытаюсь распечатать request:
Выходят ошибки :
AndreyВ общем решил все вышеуказанные проблемы, 1 вопрос у меня. Где-то допустил ошибку. И теперь у меня формируются ссылки вида:
//de/page/item/3?language=de
Где может добавляться данный ?language=de и в каком месте могло пропасть имя домена?
AndreyC лишним куском в конце разобрался, теперь не могу одного понять, откуда лишний слэш в начале?
AndreyC этим разобрался.
AndreyДобрый день, все отлично переводится, единственный вопрос - почему языык автоматически меняется на русский при переходе по ссылкам? Имеется ввиду не флаги.
Например на главной был URL - website/news
Выбрал язык, стало - website/en/news - язык изменился
Перешел по ссылке - gallery/ , стало gallery/ и язык стал русский как по умолчанию.
Теряется выбранный язык.
Где стоит копать?
Дмитрий ЕлисеевНу так и переходите на website/en/gallery/.
AndreyСпасибо!
Т.е. мне все url прописывать вроде этого:
website/".$cur_lang."/gallery
В месте можно сей момент настроить для всех url - в конфиге или каком то классе?
Дмитрий ЕлисеевЭто уже автоматически происходит при использовании createUrl(...).
AndreyОтлично! Язык сохраняется, только ссылка имеет такой вид:
http://ferrum/en/service/item/id/7?language=en
В каком месте удаляется аппендицит?
Дмитрий ЕлисеевА Вы так генерируете:
$this->createUrl('/service/item', array('id' => $id));или ещё как-нибудь? И каким методом из статьи Вы пользуетесь?
AndreyНапример в списке новостей:
echo CHtml::link($item->getName(), Yii::app()->createUrl('news/item/id/'.$item->CODE));В виджете меню так:
... 'url' => Yii::app()->createUrl('/news/index') ...В последнем примере если без index , то language воспринимается как действие..
Попробую попробовать с вашим примером, но ведь не всегда id нужен, иногда ссылка в какую то категорию сущностей ведет или например в контакты.
Дмитрий ЕлисеевДолжно работать и с id, и без, если используете DLanguageUrlManager.
AndreyВ вашем примере у меня выходит такая ссылка:
/en/news/item/1?language=en
Дмитрий ЕлисеевПеределайте на нормальную генерацию адресов с $item->CODE и посмотрите, работает или нет. И скажите, каким методом из приведённых в статье Вы пользуетесь.
AndreyСпасибо, еще раз пересмотрю статью и маршрутизаторы, если выявлю свой косяк, опишу поконкретнее где он находится. Вообще пользовался вашим методом - Прозрачная работа... Наверное что-то упустил.
Виктор – guides.byСделал так как указано в статье. Все работает все здорово. Но возникла задача запомнить какой язык ранее выбрал пользователь, т.е положить ID языка в куку и если человек зашел на сайт на главную страницу - то отобразить ему все на выбранном языке.
Если человек заходит на site.com
то
public function init() { print Yii::app()->language; exit; }выдаст текущий русский язык - ru, а тем временем в куке может быть, скажем, английский.
Как мне определить что человек зашел на главнюу страницу и в урле нет указания на какой-то язык, ведь Yii::app()->request->getUrl() выдает урл без языка в адресной строке даже если адресная строка язык содержит
site.com
и
site.com/en
и
site.com/ru
то Yii::app()->request->getUrl() выдаст одно и тоже = /
Виктор – guides.byРешил пока что для себя таким образом:
public function init() { if($_SERVER['REQUEST_URI'] == '/') { if(!empty(Yii::app()->request->cookies['language']) && Yii::app()->request->cookies['language']!=Yii::app()->language) { Yii::app()->language = Yii::app()->request->cookies['language']; } } Yii::app()->language = (empty(Yii::app()->language) ? Yii::app()->params['defaultLanguage'] : trim(Yii::app()->language)); Yii::app()->request->cookies['language'] = new CHttpCookie('language', Yii::app()->language); parent::init(); }
CorusДобрый день!
Большое спасибо за ваши статьи Дмитрий, очень помогают в освоении фреймворка :)
У меня возникла небольшая проблема, чувствую что туплю в какой-то мелочи, но не могу опнять где:
Я все делал по вашему примеру, а новые классы положил в папку "protected/components/multilanguage". И при этом мой сайт постоянно выдает ошибку:
Если положить DMultilangHelper в корень папки "components", то все хорошо работает, но как правильно сделать, чтобы все новые файлы с классами лежали в одной папке?
Дмитрий ЕлисеевДобавьте этот путь в секцию import в конфиге.
СергейДобрый день, у меня возник маленький вопрос.
В описании на официальном сайте говорится:
>>"В процессе перевода участвуют объект перевода, исходный язык и конечный язык. В Yii исходный язык по умолчанию приравнивается к исходному языку приложения, а язык — к языку приложения. Если оба языка совпадают — перевод не производится."
А если я хочу использовать более расширенные имена полей, например:
у меня есть поле
то при выборе русского языка выведет 'Показывать Альбом'.
А также есть английская версия
Но при выборе английского языка будет выводиться "show", а не 'Show Album', и это понятно, потому что язык совпадает с языком приложения, как мне вывести именно расширенную строку 'Show Album'?
Дмитрий ЕлисеевПоменяйте исходный язык на какой-нибудь другой.
СергейТоже об этом подумал, даже сделал ключами массива полные названия, типа этого:
Но потом подумал, что длинные предложения будет неудобно писать в качестве ключа массива, так что смена языка более логична. Спасибо.
Name LastЗдравствуйте, Дмитрий. Я все сделал по вашему описанию, и все отлично работает. И у меня такой вопрос. Можно ли список языков(transatedLanguages) сделать динамическим. т.е. взять его(список) из базы?
Допустим, у меня есть таблица locales для языков с полями - id, lang,langFullName.
И в моделе locales написал такую функцию для вывода списка:
public function langsList() { $model = self::model()->findAll(array('select'=>'lang,langFullName')); $arr = array(); foreach($model as $key) { $arr[$key->lang] = $key->langFullName; } return $arr; }а для того чтоб массив translatedLanguages был динамическим, оставил его пустым, т.е так:
и наконец, в контроллере в функции init:
список-то правильно выводится, но вот только, переключалка не работает корректно, точнее вообще не работает. Когда меняешь язык, то в адресной строке префикс не меняется, а добовляется каждый раз, при нажатии на один из языков. вот так: site.loc/en/fr/ua/
Дмитрий ЕлисеевЗагружайте список раньше. Например, в событии beforeRequest объекта приложения.
Артем КошкинДобрый день!
У меня такая проблема, при использовании get переменных, ссылка строится неправильно т.е.
Должно быть так: site.ru/ru/animals?category=cats
Но ссылка в меню формируется так site.ru/animals?category=cats/language/ru
Если же генерировать через $this->createUrl('animals/index',array('category'=>'cats')) - ссылка в меню
выглядит так site.ru/en/animals?family=cats/language/en
Код url manager
AndreyВсе отлично работает,
Но шалит captcha, в ней не используется createUrl:
$this->widget('CCaptcha', [ 'captchaAction' => '/users/captcha', ] );Выдает:
domen/en/en/users/captcha/refresh/1?_=1456485384482
AndreyСсылка на обновление картинки и сама картинка аналогично ведут себя(
AndreyПрошу прощения,
Вместо domen/en/en/users/captcha/refresh/1?_=1456485384482
domen/en/users/captcha/refresh/1?_=1456485384482
Но все равно картинка не генерируется.
AndreyНабыдлокодил в хелпере и все решилось, только не могу понять, при чем тут Captcha и createUrl(), если он не используется в прямом контексте?
Виктор – guides.byДмитрий, спустя долгое время использования этого решения заметил одну штуку. Все страницы сайта открываются как с языком так и без.
например страница
/ru/cities/ и /cities/ открываются без проблем и отображаются на дефолтном русском языке. А как бы сделать так, чтобы без /ru/ не открывалось? Все равно прописывать в rules
и т.д.?
Такое происходит часто, когда используется, например, форма, или $this->refresh() тогда язык теряется.
Виктор – guides.byB в дополнении. Получается, если у сайта только 1 язык, то все урлы будут
example.com/cities/
а если добавить еще хотя бы одни то все ссылки резко станут
exmplae.com/ru/cities/
т.е дефолтный язык тоже начинает подставляться в урл
Дмитрий ЕлисеевВ последних примерах без приписывания language с Yii::app()->params['defaultLanguage'] как раз обрабатывается эта ситуация.
СабирЗдравствуйте, Дмитрий! Я в yii начинающий и изучаю его по вашим статьям. Вы все очень подробно и доходчиво объясняете, но вот у меня проблема второй день голову ломаю не могу понять как ее решить в DMultiLangHelper он не находит метод getLanguage() выводит ошибку:
Unknown Method – yii\base\UnknownMethodException
Calling unknown method: yii\web\Application::getLanguage()
Может вы знаете как ее устранить?
Дмитрий ЕлисеевПопробовать поменять на Yii::app()->language.
СергейДобрый день!
Дмитрий подскажите почему указание на язык приложения хранится в url, а не в cookies?
Это только вредит SEO оптимизации или еще что?
Дмитрий ЕлисеевДа, ради SEO. Чтобы поисковик мог все языки проиндексировать.
МихаилЗдравствуйте. Спасибо за такую хорошую статью.
Делал начиная с "Прозрачная работа с языковыми адресами". Все получилось, кроме формы входа на сайт. Примеры из комментариев не помогают. Подскажите, какой код необходимо исправить в моем:
во вьюшке прописано
<?php echo CHtml::submitButton('Вход', array('class'=>'btn btn-template-main')); ?>в контроллере
public function actionIndex(){ if (is_null(Yii::app()->user->id)) $this->redirect('/index.php/site/login'); ...
Дмитрий Елисеев$this->redirect(array('/site/login'));
МихаилПоясню: при выборе не дефолтного языка шастаю по сайту на выбранном языке, но при входе в личный кабинет язык становится дефолтным...
Дмитрий ЕлисеевА адреса в кабинете с языком указываются?
МихаилСпасибо большое за ответ и уделенное время!
Решил проблему, добавив к виджету (в его начале) форм, во вьюхе, следующее: 'action'=>$this->createUrl('/login'), и вышло так:
$form=$this->beginWidget('DLanguageActiveForm', array( 'id'=>'login-form', 'action'=>$this->createUrl('/login'), 'enableClientValidation'=>true, 'clientOptions'=>array( 'validateOnSubmit'=>true, ), ));
МихаилУ меня еще один вопрос возник, касательно LanguageSwitcherWidget.php
Можно ли его оформить, как выпадающий список, также добавив к языкам и флаги стран?
К примеру таким образом:
[флаг России] Русский
[флаг Украины] Український
Как это можно сделать?
Дмитрий ЕлисеевЭто уже как сверстаете.
Error202Дмитрий, не подскажите, делаю подобное на Yii2...
Вместо СHttpRequest использую Request
В нем, вместо метода getRequestUri() подменяю getUrl - Это правильно?
Все работает, вот только Url::home() - ссылка без языка, а если переопределить метод baseUrl, то слетают пути в assets.
Не подскажите, как победить?
СергейЗдравствуйте! Вопрос о LanguageSwitcherWidget.php. Если включить переключатель на другой язык с главной страницы, то получается такой урл - sait.ru/en/. Подскажите пожалуйста как сделать чтобы слеша не было при переключении на главной странице?