Одной из причин, по которой для реализации программного комплекса я выбрал полноценный веб-фреймворк, стала необходимость поддержки трех языков: русского, украинского и английского. В системах управления содержимым (content management system), таких как Joomla, WordPress или Drupal, для поддержки нескольких языков необходимы сторонние дополнения. В то же время, в современных веб-фреймворках, использующих архитектуру Model — View — Controller, модуль поддержки языков встроен в систему и доступен без каких-либо манипуляций со стороны разработчика.
Поддержка нескольких языков интерфейса присутствует и в выбранном для разработки программного комплекса Yii Framework. С переводом представлений проблем нет; в Yii существуют для него два базовых механизма:
- при помощи сообщений;
- путем создания специфичных для языка файлов представлений.
Определенные трудности возникают при настройке преобразования URL. В отличие от некоторых других веб-фреймворков (например, Django), Yii «из коробки» не поддерживает работу с URL на сайтах с несколькими языками. Для того, чтобы устранить этот недостаток, надо рассмотреть, как происходит обработка URL в Yii.
В процессе обработки запроса из URL извлекается информация о:
- контроллере;
- действии (action) контроллера;
- параметрах действия.
Например, гипотетическому URL вида /book/5 (вместо 5 может быть другое целое число — идентификатор книги) может соответствовать контроллер BookController (идентификатор book), его метод viewAction (идентификатор view) и параметр действия $id = 5 .
<?php
class BookController extends Controller {
public function viewAction($id) {
// отобразить информацию о книге
// с заданным идентификатором
}
}
Обратное преобразование используется, например, в отображениях. По идентификаторам действия и контроллера и параметрам действия создается соответствующий URL (например, с помощью метода CController::createUrl).
Соответствие между URL и действиями осуществляет модуль Yii urlManager. Информация о преобразованиях URL хранится в главном файле конфигурации Yii (config/main.php) виде набора правил, каждое из которых состоит из двух частей:
- Шаблон — выражение для URL, определяющий область применения правила. По своему синтаксису шаблоны напоминают регулярные выражения с именованными группами.
- Действие, соответствующее URL. Действие состоит из двух частей: идентификаторов контроллера и действия в этом контроллере, разделенных косой чертой
/.
Например, приведенное выше правило для URL вида /book/5 в файле конфигурации записывается как
'book/<id:\d+>'=>'book/view'
Наиболее очевидный способ добавления поддержки нескольких языков — усложнение используемых правил:
'<lang:(ru|uk|en)>/book/<id:\d+>'=>'book/view'
<?php
class BookController extends Controller {
public function viewAction($id, $lang) {
// установить язык интерфейса
setupLanguage($lang);
// отобразить информацию о книге
// с заданным идентификатором
}
}
Однако этот способ не очень продуктивен:
- Во все действия приходится добавлять новый параметр
$langи вызов методаsetupLanguage. - Параметр
langнужно добавлять и во все методы создания URL.
Значительно более эффективный способ поддержки нескольких языков — использование специального класса правил. Модуль urlManager позволяет заменить класс для всех правил работы с URL с используемого по умолчанию CUrlRule на произвольный другой класс с двумя методами:
- parseUrl — преобразование из URL в идентификатор действия и параметры;
- createUrl — обратное преобразование.
При преобразовании URL в действие и обратно модуль Yii urlManager пытается применить соответствующую функцию каждого из правил, объявленных в файле конфигурации. Для того чтобы показать, что правило не применимо для заданного набора аргументов, указанные выше функции должны возвращать значение false. Для того, чтобы заменить класс всех правил, достаточно указать в файле конфигурации значение для поля urlRuleClass в модуле urlManager.
<?php return array( // код настройки для других модулей 'urlManager'=>array( 'urlFormat'=>'path', 'showScriptName'=>false, 'urlRuleClass'=>'CustomUrlRule', 'rules'=>array( 'book/<id:\d+>'=>'book/view' // другие правила ) ) );
При этом класс правил (в примере выше — CustomUrlRule) должен находиться в известном для Yii месте; имеет смысл поместить его в директорию components/.
Как и обычные правила CUrlRule, правила с новым классом инициализируются с использованием двух переменных: шаблона и идентификатора действия. Дополнительные параметры для создания правил можно передавать, если заменить идентификатор действия на массив:
'book/<id:\d+>'=>array('book/view', 'foo'=>'bar', 'foo2'=>'bar2')
В случае использования правил для поддержки нескольких языков класс правил (LangUrlRule) должен содержать три фрагмента.
Разбор URL
Метод parseUrl нового класса должен извлекать из предоставленного адреса информацию и устанавливать язык для отображения данных (переменную Yii::app()->language ). Для сайта программного комплекса язык определяется по первому фрагменту URL (en — для английского языка, ru — для русского, uk — для украинского). Если сведения о языке отсутствуют в URL, запрос должен перенаправляться на адрес с явно заданным языком. Например, если запрашивается адрес /book/1, после перенаправления URL станет равным /ru/book/1.
<?php
class LangUrlRule extends CUrlRule {
const LANG_REGEX = '(ru|uk|en)';
public function parseUrl($manager, $request, $pathInfo, $rawPathInfo) {
// Язык должен находиться в начале пути $pathInfo
// и отделяться слэшем '/'
if (preg_match('%^'.self::LANG_REGEX.'(/.*)?$%',
$pathInfo, $matches)) {
// Установить язык исходя из URL
Yii::app()->language = $matches[1];
$pathInfo = (count($matches) > 2)
? substr($matches[2], 1) : '';
return parent::parseUrl($manager, $request,
$pathInfo, $rawPathInfo);
} else if (parent::parseUrl($manager, $request,
$pathInfo, $rawPathInfo) !== false) {
// Установить язык по умолчанию
$lang = 'ru';
$url = Yii::app()->baseUrl . '/' . $lang . '/' . $pathInfo;
if (strlen($request->queryString) > 0) {
$url .= '?' . $request->queryString;
}
$request->redirect($url);
return false;
}
}
}
В приведенном коде при отсутствии языка в URL выбирается фиксированный язык (русский); более продвинутый способ — использование предпочитаемых языков, передаваемых браузером (Yii хранит его в переменной Yii::app()->request->preferredLanguages), однако может оказаться, что эти языки не поддерживаются.
Создание URL
Метод createUrl должен добавлять к началу адреса информацию о языке.
<?php
class LangUrlRule extends CUrlRule {
public function createUrl($manager, $route, $params, $ampersand) {
$lang = isset($params['lang'])
? $params['lang'] : Yii::app()->language;
$lang = substr($lang, 0, 2); // Обрезать информацию о стране
unset($params['lang']);
$url = parent::createUrl($manager, $route, $params, $ampersand);
if ($url === false) return false;
return $lang.'/'.$url;
}
}
Обратная совместимость
Должны поддерживаться нейтральные запросы, не зависящие от языка. Например, если на сайте используется капча, не имеет никакого смысла изменять ее URL в зависимости от языка отображения данных (более того, встроенный в Yii модуль капчи от этого перестает адекватно работать). Таким образом, должна быть возможность объявить правило вроде
'captcha'=>array('site/captcha', 'neutral'=>true)
<?php
class LangUrlRule extends CUrlRule {
/**
* Если значение булевой переменно, то правило ведет себя
* в точности как CUrlRule.
*
* @var boolean
*/
public $neutral = false;
public function __construct($route, $pattern) {
parent::__construct($route, $pattern);
if (is_array($route) && isset($route['neutral'])) {
$this->neutral = $route['neutral'];
}
}
public function createUrl($manager, $route, $params, $ampersand) {
if ($this->neutral) {
return parent::createUrl($manager, $route,
$params, $ampersand);
}
// код из фрагмента выше
}
public function parseUrl($manager, $request, $pathInfo, $rawPathInfo) {
if ($this->neutral) {
return parent::parseUrl($manager, $request,
$pathInfo, $rawPathInfo);
}
// код из фрагмента выше
}
}
Итак, с помощью простых приемов можно научить Yii Framework работать с URL для сайтов с поддержкой нескольких языков. Насколько я понимаю, с минимальными изменениями тот же прием можно подходит и для Yii 2.x.