Долгожданный выход Yii 2.0 Beta дал стимул многим разработчикам, использующих Yii, к переходу на вторую версию фреймворка. Разработчики фреймворка указали, что постараются не трогать обратную совместимость и в основном будут сосредоточены над исправлением ошибок и заканчивать документацию. Это дает еще больший импульс к использованию Yii2 в реальных проектах.
Мы решили не отставать от новшеств и выбрали именно вторую версию замечательного фреймворка Yii. При разработке проекта возникла необходимость в организации мультиязычности на сайте.
1. Количество языков неограниченно.
2. URL сайта представлены как ЧПУ и SEO оптимизированы. Ссылки вида:
example.com/en/mypage
example.com/ru/mypage
example.com/de/mypage
3. Минимальные изменения в работе с фреймворком. Ресурс по ссылке example.com/mypage должен отдаваться на языке, установленным по умолчанию. Правила роутинга не должны изменяться в зависимости от количества языков.
Исходя из того, что количество языков неограниченно и должен указываться язык по умолчанию, то было решено хранить этот список в отдельной таблице БД. Создаем таблицу lang с такими полями:
id — идентификатор языка
url — буквенный идентификатор языка для отображения в URL(ru, en, de,...)
local — язык (локаль) пользователя
name — название(English, Русский,...)
default — флаг, указывающий на язык по умолчанию(1 — язык по умолчанию)
date_update — дата обновления(в unixtimestamp)
date_create — дата создания(в unixtimestamp)
И вносим в таблицу два языка, учитывая что один должен быть из значением default=1:
Или cоздаем миграцию, выполнив команду php yii migrate/create lang. В созданный файл вставляем:
Применяем миграцию командой php yii migrate.
С помощью gii создаем модель Lang и генерируем CRUD.
В модель добавляем поведение для автоматического обновления даты при редактировании и создании записи в таблице lang:
Так же добавим вспомогательные методы для работы с объектом языка в модель Lang:
Менеджер URL(urlManager) — встроенный компонент приложения для создания URL-адресов. Через этот компонент создаются все URL в приложении. Для добавления префикса буквенного идентификатора языка в URL достаточно переопределить метод createUrl класса UrlManager и в конфигурации приложения указать используемый менеджер URL.
В блок components конфигурационного файла config/main.php добавляем:
Создаем файл components/LangUrlManager.php и переопределяем createUrl:
Информация о идентификаторе языка храниться только в URL. Соответственно определить текущий язык можно лишь путем парсинга URL. Для этого переопределим метод resolvePathInfo класса Request и в конфигурационном файле приложения укажем используемый компоненте request. Метод resolvePathInfo — возвращает часть URL($pathInfo) до знака "?" и после срипта входа.
Что бы не переписывать rules в UrlManager с учетом буквенного идентификатора языка, его(буквенный идентификатор) можно убрать с $pathInfo, установить текущий язык через Lang::setCurrent и возвращать $pathInfo, но уже без префикса языка.
Создаем файл components/LangRequest.php и переопределяем resolvePathInfo:
В блок components конфигурационного файла config/main.php добавляем:
В переводе приложения участвуют два языка:
язык приложения ($language) — язык пользователя, который работает с приложением;
исходный язык приложения ($sourceLanguage) — язык, который используется в исходном коде приложения. По умолчанию $sourceLanguage = 'en'.
Перевод сообщений осуществляется с помощью метода Yii::t( $category, $message, $params = [], $language = null ).
Установим значения по умолчанию $sourceLanguage='en' и $language='ru-RU' в конфигурационном файле. Значение $language — устанавливается заново(метод Lang::setCurrent, строчка Yii::$app->language = self::$current->local;) при определении $pathInfo в LangRequest::resolvePathInfo, то есть при каждом HTTP запросе.
Переводы сообщений будем хранить в директории messages. Для каждого языка своя директория (message/ru и message/en), в которой хранится переводы по категориям.
В блок components конфигурационного файла config/main.php добавляем:
Более подробную информацию можно найти здесь.
Создаем frontend/widgets/Lang.php:
И отображение frontend/widgets/views/lang/view.php:
Вывод виджета:
Мы решили не отставать от новшеств и выбрали именно вторую версию замечательного фреймворка Yii. При разработке проекта возникла необходимость в организации мультиязычности на сайте.
Постановка задачи
1. Количество языков неограниченно.
2. URL сайта представлены как ЧПУ и SEO оптимизированы. Ссылки вида:
example.com/en/mypage
example.com/ru/mypage
example.com/de/mypage
3. Минимальные изменения в работе с фреймворком. Ресурс по ссылке example.com/mypage должен отдаваться на языке, установленным по умолчанию. Правила роутинга не должны изменяться в зависимости от количества языков.
Хранение языков
Исходя из того, что количество языков неограниченно и должен указываться язык по умолчанию, то было решено хранить этот список в отдельной таблице БД. Создаем таблицу lang с такими полями:
id — идентификатор языка
url — буквенный идентификатор языка для отображения в URL(ru, en, de,...)
local — язык (локаль) пользователя
name — название(English, Русский,...)
default — флаг, указывающий на язык по умолчанию(1 — язык по умолчанию)
date_update — дата обновления(в unixtimestamp)
date_create — дата создания(в unixtimestamp)
CREATE TABLE IF NOT EXISTS `lang` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`url` varchar(255) NOT NULL,
`local` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
`default` smallint(6) NOT NULL DEFAULT '0',
`date_update` int(11) NOT NULL,
`date_create` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
И вносим в таблицу два языка, учитывая что один должен быть из значением default=1:
INSERT INTO `lang` (`url`, `local`, `name`, `default`, `date_update`, `date_create`) VALUES
('en', 'en-EN', 'English', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
('ru', 'ru-RU', 'Русский', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
Или cоздаем миграцию, выполнив команду php yii migrate/create lang. В созданный файл вставляем:
public function safeUp()
{
$tableOptions = null;
if ($this->db->driverName === 'mysql') {
$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
}
$this->createTable('{{%lang}}', [
'id' => Schema::TYPE_PK,
'url' => Schema::TYPE_STRING . '(255) NOT NULL',
'local' => Schema::TYPE_STRING . '(255) NOT NULL',
'name' => Schema::TYPE_STRING . '(255) NOT NULL',
'default' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 0',
'date_update' => Schema::TYPE_INTEGER . ' NOT NULL',
'date_create' => Schema::TYPE_INTEGER . ' NOT NULL',
], $tableOptions);
$this->batchInsert('lang', ['url', 'local', 'name', 'default', 'date_update', 'date_create'], [
['en', 'en-EN', 'English', 0, time(), time()],
['ru', 'ru-RU', 'Русский', 1, time(), time()],
]);
}
public function safeDown()
{
$this->dropTable('{{%lang}}');
}
Применяем миграцию командой php yii migrate.
Модель языка
С помощью gii создаем модель Lang и генерируем CRUD.
В модель добавляем поведение для автоматического обновления даты при редактировании и создании записи в таблице lang:
public function behaviors()
{
return [
'timestamp' => [
'class' => 'yii\behaviors\TimestampBehavior',
'attributes' => [
\yii\db\ActiveRecord::EVENT_BEFORE_INSERT => ['date_create', 'date_update'],
\yii\db\ActiveRecord::EVENT_BEFORE_UPDATE => ['date_update'],
],
],
];
}
Так же добавим вспомогательные методы для работы с объектом языка в модель Lang:
//Переменная, для хранения текущего объекта языка
static $current = null;
//Получение текущего объекта языка
static function getCurrent()
{
if( self::$current === null ){
self::$current = self::getDefaultLang();
}
return self::$current;
}
//Установка текущего объекта языка и локаль пользователя
static function setCurrent($url = null)
{
$language = self::getLangByUrl($url);
self::$current = ($language === null) ? self::getDefaultLang() : $language;
Yii::$app->language = self::$current->local;
}
//Получения объекта языка по умолчанию
static function getDefaultLang()
{
return Lang::find()->where('`default` = :default', [':default' => 1])->one();
}
//Получения объекта языка по буквенному идентификатору
static function getLangByUrl($url = null)
{
if ($url === null) {
return null;
} else {
$language = Lang::find()->where('url = :url', [':url' => $url])->one();
if ( $language === null ) {
return null;
}else{
return $language;
}
}
}
Формирование URL
Менеджер URL(urlManager) — встроенный компонент приложения для создания URL-адресов. Через этот компонент создаются все URL в приложении. Для добавления префикса буквенного идентификатора языка в URL достаточно переопределить метод createUrl класса UrlManager и в конфигурации приложения указать используемый менеджер URL.
В блок components конфигурационного файла config/main.php добавляем:
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'class'=>'frontend\components\LangUrlManager',
'rules'=>[
'/' => 'site/index',
'<controller:\w+>/<action:\w+>/*'=>'<controller>/<action>',
]
],
Создаем файл components/LangUrlManager.php и переопределяем createUrl:
<?php
namespace frontend\components;
use yii\web\UrlManager;
use frontend\models\Lang;
class LangUrlManager extends UrlManager
{
public function createUrl($params)
{
if( isset($params['lang_id']) ){
//Если указан идентефикатор языка, то делаем попытку найти язык в БД,
//иначе работаем с языком по умолчанию
$lang = Lang::findOne($params['lang_id']);
if( $lang === null ){
$lang = Lang::getDefaultLang();
}
unset($params['lang_id']);
} else {
//Если не указан параметр языка, то работаем с текущим языком
$lang = Lang::getCurrent();
}
$url = parent::createUrl($params);
return $url == '/' ? '/'.$lang->url : '/'.$lang->url.$url;
}
}
Определения языка
Информация о идентификаторе языка храниться только в URL. Соответственно определить текущий язык можно лишь путем парсинга URL. Для этого переопределим метод resolvePathInfo класса Request и в конфигурационном файле приложения укажем используемый компоненте request. Метод resolvePathInfo — возвращает часть URL($pathInfo) до знака "?" и после срипта входа.
Что бы не переписывать rules в UrlManager с учетом буквенного идентификатора языка, его(буквенный идентификатор) можно убрать с $pathInfo, установить текущий язык через Lang::setCurrent и возвращать $pathInfo, но уже без префикса языка.
Создаем файл components/LangRequest.php и переопределяем resolvePathInfo:
<?php
namespace frontend\components;
use Yii;
use yii\web\Request;
use frontend\models\Lang;
class LangRequest extends Request
{
private $_lang_url;
public function getLangUrl()
{
if ($this->_lang_url === null) {
$this->_lang_url = $this->getUrl();
$url_list = explode('/', $this->_lang_url);
$lang_url = isset($url_list[1]) ? $url_list[1] : null;
Lang::setCurrent($lang_url);
if( $lang_url !== null && $lang_url === Lang::getCurrent()->url &&
strpos($this->_lang_url, Lang::getCurrent()->url) === 1 )
{
$this->_lang_url = substr($this->_lang_url, strlen(Lang::getCurrent()->url)+1);
}
}
return $this->_lang_url;
}
protected function resolvePathInfo()
{
$pathInfo = $this->getLangUrl();
if (($pos = strpos($pathInfo, '?')) !== false) {
$pathInfo = substr($pathInfo, 0, $pos);
}
$pathInfo = urldecode($pathInfo);
// try to encode in UTF8 if not so
// http://w3.org/International/questions/qa-forms-utf-8.html
if (!preg_match('%^(?:
[\x09\x0A\x0D\x20-\x7E] # ASCII
| [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
| \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
| \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
| \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
| [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
| \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
)*$%xs', $pathInfo)
) {
$pathInfo = utf8_encode($pathInfo);
}
$scriptUrl = $this->getScriptUrl();
$baseUrl = $this->getBaseUrl();
if (strpos($pathInfo, $scriptUrl) === 0) {
$pathInfo = substr($pathInfo, strlen($scriptUrl));
} elseif ($baseUrl === '' || strpos($pathInfo, $baseUrl) === 0) {
$pathInfo = substr($pathInfo, strlen($baseUrl));
} elseif (isset($_SERVER['PHP_SELF']) && strpos($_SERVER['PHP_SELF'], $scriptUrl) === 0) {
$pathInfo = substr($_SERVER['PHP_SELF'], strlen($scriptUrl));
} else {
throw new InvalidConfigException('Unable to determine the path info of the current request.');
}
if (isset($pathInfo[0]) && $pathInfo[0] === '/') {
$pathInfo = substr($pathInfo, 1);
}
return (string) $pathInfo;
}
}
В блок components конфигурационного файла config/main.php добавляем:
'request' => [
'class' => 'frontend\components\LangRequest'
],
Интернационализация приложения
В переводе приложения участвуют два языка:
язык приложения ($language) — язык пользователя, который работает с приложением;
исходный язык приложения ($sourceLanguage) — язык, который используется в исходном коде приложения. По умолчанию $sourceLanguage = 'en'.
Перевод сообщений осуществляется с помощью метода Yii::t( $category, $message, $params = [], $language = null ).
Установим значения по умолчанию $sourceLanguage='en' и $language='ru-RU' в конфигурационном файле. Значение $language — устанавливается заново(метод Lang::setCurrent, строчка Yii::$app->language = self::$current->local;) при определении $pathInfo в LangRequest::resolvePathInfo, то есть при каждом HTTP запросе.
Переводы сообщений будем хранить в директории messages. Для каждого языка своя директория (message/ru и message/en), в которой хранится переводы по категориям.
В блок components конфигурационного файла config/main.php добавляем:
'language'=>'ru-RU',
'i18n' => [
'translations' => [
'*' => [
'class' => 'yii\i18n\PhpMessageSource',
'basePath' => '@frontend/messages',
'sourceLanguage' => 'en',
'fileMap' => [
//'main' => 'main.php',
],
],
],
],
Более подробную информацию можно найти здесь.
Виджет переключения языков
Создаем frontend/widgets/Lang.php:
<?php
namespace frontend\widgets;
use frontend\models\Lang;
class WLang extends \yii\bootstrap\Widget
{
public function init(){}
public function run() {
return $this->render('lang/view', [
'current' => Lang::getCurrent(),
'langs' => Lang::find()->where('id != :current_id', [':current_id' => Lang::getCurrent()->id])->all(),
]);
}
}
И отображение frontend/widgets/views/lang/view.php:
<?php
use yii\helpers\Html;
?>
<div id="lang">
<span id="current-lang">
<?= $current->name;?> <span class="show-more-lang">▼</span>
</span>
<ul id="langs">
<?php foreach ($langs as $lang):?>
<li class="item-lang">
<?= Html::a($lang->name, '/'.$lang->url.Yii::$app->getRequest()->getLangUrl()) ?>
</li>
<?php endforeach;?>
</ul>
</div>
Вывод виджета:
<?php
use frontend\widgets\WLang;
?>
...
<?= WLang::widget();?>