Данным метод организации мультиязычности для сайта на фреймворке Yii2 я разработал для своих проэктов основываясь на изучении информации из различных источников. На удивление, найти все в одном месте, на просторах интернета большая проблема.
В основном показан пример использования yii2-translate-manager для перевода простых фраз и уведомлений. Реже можно встретить перевод статических страниц, еще реже перевод страниц хранящих контент в базе данных. И совсем редко вывод текущего языка в URL страницы. По последнему есть несколько готовых решений устанавливаемых с помощью Composer. Но опять же- URL будет содержать метку языка, а основную часть организации мультиязычности придется делать самому и не факт, что будет работать как надо и что-то не вылезет. Поэтому делаем все сами, это не так и сложно.

Перечислю, что мы будем иметь в итоге:

  • кол-во языков не ограничено. Используемые языки задаются в конфигурационном файле main.php.
  • указатель текущего языка (метка) указывается в URL страниц, что необходимо для SEO. Пример (русский использован в качестве основного языка и выбрана опция не выводить основной язык):

http://site.com
http://site.com/en
http://site.com/uk

http://site.com/contact
http://site.com/en/contact
http://site.com/uk/contact


  • основной язык приложения (например русский) можно как отображать в URL так и отключить в настройках модуля;
  • язык можно менять как выбрав соответствующую ссылку на странице так и прямо в адресной строке;
  • правила маршрутизации компонента приложения UrlManager останутся на своих местах и не требуют изменения.
  • сделаем перевод служебных и прочих одиночных сообщений (например меню, подвал сайта);
  • сделаем перевод статических страниц (контакты, о нас…) которые будут храниться в виде соответствующих файлов представлений переведенных на нужные языки;
  • сделаем перевод контента страниц (постов) из базы данных (кому это не надо – можно пропустить);

Широко охватим данную тему, достаточную для организации мультиязычности на большинстве сайтов и WEB-приложений Yii2.

В принципе, если не выводить указатель языка в URL страниц, то дело намного упрощается. Тогда можно сохранять выбранный пользователем язык в куку и потом, при каждой загрузке страницы, выводить контент на языке соответствующий сохраненному значению. Не нужно вставлять метку языка в ссылки и т.д. Но с точки зрения SEO оптимизации (индексирования сайта поисковыми системами) это плохо, т.к. страницы с разным содержимым (на разных языках) будут открываться по одному и тому же URL. Поэтому, приведу пример правильной и расширенной версии с учетом SEO.


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




Файл конфигурации frontend\config\main.php

1. В начало массива возвращаемого директивой return пишем:

'sourceLanguage' => 'ru', // использован в качестве ключей переводов
Параметр используется для yii2-translate-manager (подключается далее) при переводе отдельных фраз с помощью метода Yii::t(). Можно задать ключи общие для всех языков, по которым будут искаться слова, а можно как тут – указать язык по-умолчанию, который будет использован в качестве ключей и для него не нужно будет создавать отдельный файл с переводом.

2. Чуть ниже регистрируем модуль:
'modules' => [
    'languages' => [
        'class' => 'common\modules\languages\Module',
        //Языки используемые в приложении
        'languages' => [
            'English' => 'en',
            'Русский' => 'ru',
            'Українська' => 'uk',
        ],
        'default_language' => 'ru', //основной язык (по-умолчанию)
        'show_default' => false, //true - показывать в URL основной язык, false - нет
    ],
],
В параметре languages указываем, в виде ассоциативного массива, какие языки будут использованы в приложении. Ключи массивов далее можно использовать в качестве ссылок на смену текущего языка, а значения используются в виде языковых меток в URL. Так же значения используются при установки текущего языка приложения, поэтому нужно использовать стандартные обозначения.
В параметре default_language указываем какой язык устанавливать приложению по-умолчанию, например при вводе URL главной страницы.
В параметре show_default указываем нужно ли в адресной строке (в URL) отображать метку основного языка.

3. Еще ниже регистрируем класс предзагрузки:
'bootstrap' => [
    'log',
    'languages'
],
где указываем, что при выполнении первоначальной загрузки приложения (до обработки входящего запроса) нужно выполнить код из модуля 'languages', а именно его метод bootstrap(). Подробнее про предзагрузку читайте тут.
Это позволит установить язык приложения в зависимости от метки языка в URL.

4. В начало массива components – включаем перевод сообщений:
'i18n' => [
    'translations' => [
        'app' => [
            'class' => 'yii\i18n\PhpMessageSource',
            //'forceTranslation' => true,
            'basePath' => '@common/messages',
        ],
    ],
],
  • app – название категории к которой будут относиться переводы. Файл должен называться app.php и находиться по указанному пути. А текст для перевода указывается так: Yii::t('app', 'Блог')
  • forceTranslation - указывается если в качестве ключей использовать фразы-константы которые так же нужно переводить. В нашем примере указан параметр 'sourceLanguage', поэтому не нужно задавать.
  • в basePath передаем путь к папке с переводами. Т.к. русский указан в качестве ключей для перевода, нужно создать только языки на которые нужен перевод, например английский.

5. В массиве "components" есть вложенный массив "request", вставить в него строки:
'baseUrl' => '', // убрать frontend/web
'class' => 'common\components\Request'
Первой строкой убираем из URL ненужные надписи с папкой входного скрипта.
Во второй строке указываем класс LangRequest переопределяющий класс фреймворка yii\web\Request, подробности далее.


6. В массив urlManager (являющийся элементом массива components) включаем ЧПУ, добавляем свой класс и нужные правила:
'urlManager' => [
    'enablePrettyUrl' => true,
    'showScriptName' => false,

    'class' => 'common\components\UrlManager',
    'rules' => [
        'languages' => 'languages/default/index', //для модуля мультиязычности
        //далее создаем обычные правила
        '/' => 'site/index',
        '<action:(contact|login|logout|language|about|signup)>' => 'site/<action>',
    ],
],
Указываем одно обязательное правило, нужно для работы модуля, а далее все как всегда.
Так же переопределяем стандартный класс UrlManager'. Размещаем его как и Request в common\components т.к. он может понадобиться не только для модуля мультиязычности.


Файл common\components\UrlManager.php

<?php
/*
 * Добавляет указатель языка в ссылки
 */
namespace common\components;

use Yii;

class UrlManager extends \yii\web\UrlManager {

    public function createUrl($params) {

        //Получаем сформированную ссылку(без идентификатора языка)
        $url = parent::createUrl($params);

        if (empty($params['lang'])) {
            //текущий язык приложения
            $curentLang = Yii::$app->language;

            //Добавляем к URL префикс - буквенный идентификатор языка
            if ($url == '/') {
                return '/' . $curentLang;
            } else {
                return '/' . $curentLang . $url;
            }
        };

        return $url;
    }
}
Переопределяем метод createUrl() класса UrlManager, чтобы он добавлял указатель языка во все внутренние ссылки сайта.


Файл common/components/Request.php

<?php

namespace common\components;

use Yii;


class Request extends \yii\web\Request
{
    private $_lang_url;

    public function getLangUrl()
    {
            $this->_lang_url = $this->getUrl(); //полный URL

            $url_list = explode('/', $this->getUrl());

            $lang_url = isset($url_list[1]) ? $url_list[1] : null;

            //Удалить метку языка из URL
            if( $lang_url !== null && $lang_url === Yii::$app->language )
            {
                $url = preg_replace("/^\/$lang_url/", '', $this->_lang_url);
                return $url;
            }

        return $this->_lang_url;
    }

    /*
     * Переопределяем метод для того, чтобы он использовал URL без метки языка.
     * Это позволит использовать обычные правила в UrlManager.
     */
    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;
    }
}

Тут мы переопределяем метод resolvePathInfo() класса yii\web\Request. Это делается для того, чтобы URL используемый в запросе и далее разбираемый на составные части не содержал метки языка, что позволит использовать стандартные правила для UrlManager.
Первой строкой данного метода
$pathInfo = $this->getLangUrl();
мы получаем URL без метки языка, который подготавливает метод getLangUrl() и больше ничего не меняем.


Теперь создаем файлы модуля.

Проще всего создать модуль используя генератор GII.
Yii2 GII создание модуля мультиязычности











При создании файлов модуля в генераторе можно отключить строку создания файла представления:
…common\modules\languages\views\default\index.php
т.к. модуль будет использовать виджеты для вывода списка языков для выбора пользователем.

Итого, после использования генератора появится 2 файла – модуль и его контроллер, конечно можно создать их и вручную, как и остальные файлы модуля. Ниже привожу код всех файлов.


Файл common/modules/languages/Module.php

<?php

namespace common\modules\languages;

use common\modules\languages\models\LanguageKsl;
use yii\base\BootstrapInterface;


class Module extends \yii\base\Module implements BootstrapInterface
{

    public $controllerNamespace = 'common\modules\languages\controllers';

    public $languages; //Языки используемые в приложении

    public $default_language; //основной язык (по-умолчанию)

    public $show_default; //показывать в URL основной язык


    /*
     * Предзагрузка - выполнится до обработки входящего запроса.
     * Устанавливает язык приложения в зависимости от метки языка в URL,
     * а при ее отсутствии устанавливает в качестве метки текущий язык
     */
    public function bootstrap($app)
    {
        if(YII_ENV == 'test') return; //для тестового приложения отключаем.
        
        $url = $app->request->url;

        //Получаем список языков в виде строки
        $list_languages = LanguageKsl::list_languages();

        preg_match("#^/($list_languages)(.*)#", $url, $match_arr);

        //Если URL содержит указатель языка - сохраняем его в параметрах приложения и используем
        if (isset($match_arr[1]) && $match_arr[1] != '/' && $match_arr[1] != ''){

            /*
             * Если в настройках выбрано не показывать язык используемый по-умолчанию
             * убираем метку текущего языка из URL и перенаправляем на ту же страницу
             */
            if( !$this->show_default && $match_arr[1] == $this->default_language) {
                $url = $app->request->absoluteUrl; //Возвращает абсолютную ссылку
                $lang = $this->default_language; //язык используемый по-умолчанию
                $app->response->redirect(['languages/default/index', 'lang' => $lang, 'url' => $url]);
            }

            $app->language = $match_arr[1];
            $app->formatter->locale = $match_arr[1];
            $app->homeUrl = '/'.$match_arr[1];

        /*
         * Если URL не содержит указатель языка и отключен показ основного языка в URL
         */
        } elseif(!$this->show_default){

            $lang = $this->default_language; //язык используемый по-умолчанию

            $app->language = $lang;
            $app->formatter->locale = $lang;

        /*
         * Если URL не содержит указатель языка, а в настройках включен показ основного языка
         */
        } else {
            $url = $app->request->absoluteUrl; //Возвращает абсолютную ссылку

            $lang = $this->default_language;

            $app->response->redirect(['languages/default/index', 'lang' => $lang, 'url' => $url], 301);
        }
    }

}

В основном файле модуля указываем пространство имен контроллеров модуля, а так же свойства которые определяются в конфигурации. Модуль реализует интерфейс BootstrapInterface для того, чтобы выполнить метод bootstrap($app) до обработки входящего запроса. Эту возможность я использовал для установки языка приложения исходя из языковой метки в URL, а в случае если такая метка не обнаружена, будет подставлена метка языка используемого по-умолчанию, в примере это русский язык.


Файл контроллера - common/modules/languages/controllers/DefaultController.php

<?php

namespace common\modules\languages\controllers;

use Yii;
use yii\web\Controller;
use common\modules\languages\models\LanguageKsl;


class DefaultController extends Controller
{
    /**
     * Обрабатывает переход по ссылкам для смены языка
     * Перенаправляет на ту же страницу с новым URL.
     */
    public function actionIndex()
    {

        $language = Yii::$app->request->get('lang'); //язык на который будем менять

        /*
         * При перенаправлении сюда до разбора входящего запроса (метод run класса LanguageKsl)
         * передаем URL предыдущей страницы в get параметрах
         */
        $url_referrer = Yii::$app->request->get('url');
        /*
         * При перенаправлении сюда из виджета (нажатие по ссылке для смены языка)
         * получаем предыдущую страницу средствами Yii2
         */
        if(!$url_referrer) $url_referrer = Yii::$app->request->referrer; //предыдущая страница

        /*
         * Если все же предыдущая страница не получена - возвращаем на главную.
         */
        if (!$url_referrer) $url_referrer = Yii::$app->request->hostInfo . '/'. $language;

        //устанавливает/меняет метку языка
        $url = LanguageKsl::parsingUrl($language, $url_referrer);


        // перенаправление
        Yii::$app->response->redirect($url);
    }

}

Метод index() контроллера обрабатывает запрос с переданным ему GET параметром 'lang'. Данный параметр содержит метку языка на который пользователь желает переключиться. Далее в виджете мы сделаем соответствующие ссылки. Получив метку языка контроллер перенаправляет пользователя на ту же страницу на которой он нажал ссылку смены языка но уже с меткой выбранного языка.
Контроллер использует определенный функционал который вынесем в модель.


Файл common/modules/languages/models/LanguageKsl.php

<?php

namespace common\modules\languages\models;

use Yii;


class LanguageKsl
{

    static $list; //строка вида ru|uk|en|


    /*
     * Преобразование к строке вида ru|uk|en|
     * для использования в регулярных выражениях
     */
    public static function list_languages(){

        if(!self::$list){

            $languages = Yii::$app->getModule('languages')->languages;
            $list = '';

            array_walk($languages, function ($value) use (&$list){
                $list .= $value . '|';
            });
            self::$list = $list;
        }

        return self::$list;
    }


    /**
    * Создает URL с меткой языка
    * Разбивает URL на подмассив $match_arr
    * 0. http://site.loc/ru/contact
    * 1. http://site.loc
    * 2. ru или uk или en
    * 3. остальная часть
    */
    public static function parsingUrl($language, $url_referrer){

        $list_languages = self::list_languages(); //список языков
        $host = Yii::$app->request->hostInfo;

        preg_match("#^($host)/($list_languages)(.*)#", $url_referrer, $match_arr);

        //добавляем разделитель
        if (isset($match_arr[3]) && !empty($match_arr[3]) && !preg_match('#^\/#', $match_arr[3])){
            $separator = '/';
        } else {
            $separator = '';
        }


        $default_language = Yii::$app->getModule('languages')->default_language;
        $show_default = Yii::$app->getModule('languages')->show_default;

        //Удаляем основной язык из URL, если в настройках выбрано "не показывать"
        if($language == $default_language && !$show_default){
            $match_arr[2] = null;
        } else {
            $match_arr[2] = '/'.$language.$separator;
        }

        // создание нового URL
        $url = $match_arr[1].$match_arr[2].$match_arr[3];
        return $url;
    }
}

Метод parsingUrl() разбирает текущий URL на части и далее заменяет или добавляет нужную метку языка формируя новый URL с новой меткой. Метод list_languages() преобразует языки указанные в конфигурации модуля к виду нужному для использования в регулярных выражениях метода parsingUrl().


Осталось сделать вывод ссылок для переключения языков на страницах сайта. Сначала я сделал это в виде отдельного файла-представления, позже решил сделать в виде виджета, что позволит легко создать необходимый вид, если, например, потребуется вывести данные ссылки в другом виде или же в нескольких местах страницы.


Основной файл виджета common/modules/languages/widgets/ListWidget.php

<?php

namespace common\modules\languages\widgets;

use Yii;
use yii\base\Widget;
use yii\helpers\Html;


class ListWidget extends Widget{

    public $array_languages;

    public function init() {
        $language = Yii::$app->language; //текущий язык

        //Создаем массив ссылок всех языков с соответствующими GET параметрами
        $array_lang = [];
        foreach (Yii::$app->getModule('languages')->languages as $key => $value){
            $array_lang += [$value => Html::a($key, ['languages/default/index', 'lang' => $value])];
        }

        //ссылку на текущий язык не выводим
        if(isset($array_lang[$language])) unset($array_lang[$language]);
        $this->array_languages = $array_lang;
    }

    public function run() {
        return $this->render('list',[
            'array_lang' => $this->array_languages
        ]);
    }

}
тут формируем массив готовых ссылок для вывода в представлении виджета, которые будут передавать GET параметр 'lang' с меткой нужного языка в контроллер модуля, где уже будет осуществляться смена языковой метки.

В common/modules/languages/widgets создаем папку views с файлами-представлений виджета. Создадим там один простой файл для примера.


Файл common/modules/languages/widgets/views/list.php

<div class="languages-klisl">
    <?php foreach ($array_lang as $lang) {
        echo ' '.$lang.' ';
    } ?>
</div>
Для вывода виджета, в шаблоне или виде, например в блоке footer файла-шаблона frontend\views\layouts\main.php вставляем:

<?= common\modules\languages\widgets\ListWidget::widget() ?>



На данном этапе уже работает перевод фраз. Рассказываю как это делается.
Файлы с переводами должны называться как указано в конфигурации (файл main.php, массив компонента 'i18n'), в данном случае - app.php и размещаться (как указано там же) в common\messages\ и дальше папка с название языка (en, uk…).

Пример файла перевода common\messages\en\app.php:
<?php
return [
    'Блог' => 'Blog',
    'О нас' => 'About me',
    'Контакты' => 'Contact',
];
то есть в массив return нужно вписать все слова и фразы которые нужно переводить. В коде (обычно в шаблонах и файлах представлений), фразы которые требуют перевода заключать в вызов метода Yii::t().
Согласно нашей конфигурации так:
Yii::t('app', 'Блог')
Русский у нас указан в качестве языка по-умолчанию, поэтому если текущий язык – русский, выведется слово «Блог», а если английский - 'Blog'.


Делаем перевод статичных страниц, то есть тех, которые хранят текст в коде, а не берут контент из БД. Целые страницы содержат слишком много текста, в связи с чем нецелесообразно использовать метод Yii::t().

В нужном контроллере создаем действие для каждой такой страницы:
public function actionStat()
{
    $language = Yii::$app->language; //текущий язык
    //выводим вид соответствующий текущему языку
    return $this->render('statPages/stat-'.$language);     
}
то есть вторая часть название файла вида берется из названия языка.
В данном случае в папке с видами создаем отдельную папку для статичных файлов statPages (это не обязательно), а в ней файлы с контентом соответствующего языка:
- stat-ru.php
- stat-uk.php
- stat-en.php

Мы сделали перевод фраз и статичных страниц, текущий язык уже отображается в адресной строке, пользователь может свободно переходить с одного на другой. Для многих сайтов (например сайт-визитка и прочих) этого уже вполне достаточно. Для полноты картины осталось рассмотреть вариант перевода когда контент хранится в базе данных. Это я вынес в отдельную статью.


Если вас интересует готовое решение - предлагаю скачать мое расширение устанавливаемое с помощью Composer. Оно реализует все описанное в данной статье. Страница расширения на GitHub тут. Если понравится - не забудь нажать звездочку :)