В Yii2 есть возможность реализовать три варианта интернационализации:

  1. Файл с массивом, вида: ключ=>перевод (гибко)
    Help Link:

    Создадим конфигурационный файл для сборщика


    Выполним консольную команду
    php ./yii message/config @common/config/i18n.php
    //или с комментариями
    php ./yii message/config-template @common/config/i18n.php
    

    p.s. Для windows окружения php yii….Работа с console должна быть настроена т.е. необходимые компоненты должны быть подключены.

    Должен создаться файл /common/config/i18n.php:
    return [
        'sourcePath' => __DIR__. '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR,
        'languages' => ['pt-PT','ru-RU','uk-UA','en-US'], // языки на которые будут использоваться
        'translator' => 'Yii::t',
        'sort' => false,
        'removeUnused' => false,
        'only' => ['*.php'],// расширения файлов в которых искать категории
        'except' => [//исключить эти источники
            '.svn',
            '.git',
            '.gitignore',
            '.gitkeep',
            '.hgignore',
            '.hgkeep',
            '/messages',
            '/vendor',
        ],
        'format' => 'php',// формат хранения переводов
        'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'messages',
        'overwrite' => true, 
    ];
    


    Создайте папку common/messages для хранения переводов !

    Добавим компонент i18n


    В файле конфигурации common/main.php
    'language'=> 'pt-PT',//по умолчанию
    'sourceLanguage' => 'uk-UA',//исходный язык
    'components' => [
    ...
            'i18n' => [
                'translations' => [
                    'frontend*' => [
                        'class' => 'yii\i18n\PhpMessageSource',
                        'basePath' => '@common/messages',
                        /* fileMap определяет, какой файл будет подключаться для определённой категории.
    					иначе так название категории является именем файла
                        'fileMap' => [
                            'app'       => 'app.php',
                            'app/error' => 'error.php',
                        ],*/
                    ],
                    'backend*' => [
                        'class' => 'yii\i18n\PhpMessageSource',
                        'basePath' => '@common/messages',
                    ],
    				// или просто вместо перечисления категорий поставим * что  означает все категории
                    '*' => [
                        'class' => 'yii\i18n\PhpMessageSource',
    					'basePath' => '@common/messages',
    					 'on missingTranslation' => ['common\components\TranslationEventHandler', 'handleMissingTranslation']// обработчик не найденных переводов
                    ],
                ],
            ],
    ....
    


    Что должно быть в представлениях


    Примеры из документации Yii::t()
     // Если писать категорию через / то будут созданы папки словарей по страницам frontend/home.php
    echo \Yii::t('frontend/home', 'key home');
    echo \Yii::t('frontend/login', 'key login');
    

    Лучше, если названия категории будут содержать название страницы на которой они будут использоваться. При поиске перевода Yii делает выборку всей категории.

    Обработчик не найденных переводов


     common\components\TranslationEventHandler.php
     
    namespace common\components;
    use yii\i18n\MissingTranslationEvent;
    
    class TranslationEventHandler
    {
        public static function handleMissingTranslation(MissingTranslationEvent $event) {
    		// вывод в месте вызова
            $event->translatedMessage = "@MISSING: {$event->category}.{$event->message} FOR LANGUAGE {$event->language} @";
            // или писать лог
        }
    }
    


    Запуск построение ключей


    php yii message/extract @common/config/i18n.php
    После чего должны создатся файлы:
    • common\messages\ru-RU\frontend\home.php
    • common\messages\uk-UA\frontend\home.php
    • common\messages\en-US\frontend\home.php
    • common\messages\ru-RU\frontend\login.php
    • common\messages\uk-UA\frontend\login.php
    • common\messages\en-US\frontend\login.php
    • ....

    Структура папок зависит от названия категорий. Эта на основе frontend/login и frontend\home
    Если б мы использовали такого вида категории(ключи) Yii::t('frontend','home')
    то создался б все один файл по каждому языку( common\messages\ru-RU\frontend.php) в котором были бы все переводы и для перевода одного представления Yii загрузил бы все переводы которые нам и не нужны на этой странице.

    Файлы имею такой вид:
    return [
        'key home' => '',
        'key login' => ''
    ];
    

    Переводить мы будем уже сами.

    Если создать файл views/site/ru-RU/index.php и язык по умолчанию language=ru-RU то сначала рендерится эта вьюшка.


  2. Файл с расширением .po,.mo бинарный (нужен компилятор,быстро)
    Help Link:

    Создание конфигурационного файла


    Командой php yii message/config @common/config/i18n.php
    получим файл common/config/i18n.php
    return [
        'color' => null,
        'interactive' => true,
        'sourcePath' => __DIR__. '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR,
        'languages' => ['pt-PT','ru-RU','uk-UA','en-US'], 
        'translator' => 'Yii::t',
        'sort' => false,
        'removeUnused' => false,
        'only' => ['*.php'],
        'except' => [
            '.svn',
            '.git',
            '.gitignore',
            '.gitkeep',
            '.hgignore',
            '.hgkeep',
            '/messages',
            '/vendor',
        ],
        'format' => 'po',
        'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'messages',
        'overwrite' => true,
    ];
    

    Он такой же как и в случае с форматом php. Отличается только форматом :) 'format' => 'po'

    Создайте папку common/messages для хранения переводов !

    Отличие этого формата от php формата только в том, что после сбора ключей с приложения командой php yii message/extract @common/config/i18n.php в папке common\messages создадутся файлы с расширением .so (frontend.so) которые следует компилировать программкой, к примеру poedit windows и простым сохранением она создаст бинарные файлы с расширением .so.


  3. База данных mysql, две таблицы для ключей и переводов
    Help Link:

    Создадим конфигурационный файл common/config/i18n.php


    Выполнив команду
    php yii message/config @common/config/i18n.php
    Создаться такой файл common/config/i18n.php:
    return [
        'color' => null,
        'interactive' => true,
        'help' => null,
        'sourcePath' =>  __DIR__. '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR,
        'messagePath' =>  __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'messages',
        'languages' => ['pt-PT','ru-RU','uk-UA','en-US'],// языки на которые будет поддерживать приложение
        'translator' => 'Yii::t',
        'sort' => false,
        'overwrite' => true,
        'removeUnused' => false,
        'markUnused' => true,
        'except' => [
            '.svn',
            '.git',
            '.gitignore',
            '.gitkeep',
            '.hgignore',
            '.hgkeep',
            '/messages',
            '/BaseYii.php',
        ],
        'only' => [
            '*.php',
        ],
        'format' => 'db',
        'db' => 'db',
        'sourceMessageTable' => '{{%source_message}}',// название таблицы с исходными сообщениями
        'messageTable' => '{{%message}}',// название таблицы с переводами
        'ignoreCategories' => ['yii'],// игнорируем категории
    ];
    


    Добавим компонент


    Примеры компонента есть в варианте с php форматом
    В файле конфигурации common/main.php
    'language'=> 'ru-RU',//по умолчанию
    'sourceLanguage' => 'uk-UA',//исходный язык
    'components' => [
    ...
           'i18n' => [
                'translations' => [
    // * - все категории
                    '*' => [
                        'class' => 'yii\i18n\DbMessageSource',
                        'on missingTranslation' => ['common\components\TranslationEventHandler', 'handleMissingTranslation']
                    ],
                ],
            ],
    ....
    


    обработчик не найденных переводов common\components\TranslationEventHandler так же есть в примере с форматом php

    В представлениях должны быть вызовы метода компонента Yii::t() см.Help Link
    К примеру:
    echo \Yii::t('frontend/home', 'key home') ;
    echo \Yii::t('frontend/login', 'key login') ;
    


    Выполним migrate


    Для хранения переводов выполним скрипт создания таблиц
    php yii migrate --migrationPath=@yii/i18n/migrations
    Создадутся две таблицы:
    --таблица ключей
    CREATE TABLE IF NOT EXISTS `source_message` (
      `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
      `category` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
      `message` text COLLATE utf8_unicode_ci,
      KEY `idx_source_message_category` (`category`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    
    --таблица переводов
    CREATE TABLE IF NOT EXISTS `message` (
      `id` int(11) NOT NULL PRIMARY KEY,
      `language` varchar(16) COLLATE utf8_unicode_ci NOT NULL,
      `translation` text COLLATE utf8_unicode_ci,
      KEY `idx_message_language` (`language`),
      CONSTRAINT `fk_message_source_message` FOREIGN KEY (`id`) REFERENCES `source_message` (`id`) ON DELETE CASCADE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    


    Теперь можно выполнить консольную команду сбора всех категории с сохранением их в базу
    php yii message/extract @common/config/i18n.php

Также имеется возможность хранить переводы в Mongo:

Но сбор ключей придется писать самому.
yii\mongodb\i18n\MongoDbMessageSource
По аналогии…
Компонент i18n
return [
    'language'=> 'uk-UA',//'ru-RU',
    //....
    'components' => [
        // ...
        'i18n' => [
            'translations' => [
                '*' => [
                    'class' => 'yii\mongodb\i18n\MongoDbMessageSource'
                ]
            ]
        ],
    ]
];


Создаем коллекцию:

$messages=Yii::$app->mongodb->getCollection('messages');
$messages->insert([
   "_id"=>1,
   "category"=>"frontend/index",
   "language"=>"uk",
   "messages"=>["Hello world!"=>"Привіт Welt!","Hello"=>"Привіт"]
]);
$messages->insert([
   "_id"=>2,
   "category"=>"frontend/index",
   "language"=>"ru",
   "messages"=>["Hello world!"=>"Привет Welt!","Hello"=>"Привет"]
]);


получаем такие документы:
{
	"_id": 1,
	"category": "frontend/index",
	"language": "uk",
	"messages": {
		"Hello world!": "Привіт Welt!",
		"Hello": "Привіт"
	}
},
{
	"_id": 2,
	"category": "frontend/index",
	"language": "ru",
	"messages": {
		"Hello world!": "Hallo Welt!",
		"Hello": "Привет"
	}
}



И при вызове перевода

echo \Yii::t('frontend/index', 'Hello');
echo \Yii::t('frontend/index', 'Hello world!');

отработает выборка из коллекции messages по языку 'language'=> 'uk-UA'
и категории "category": "frontend/index".Получим все ключи категории сразу, имеет смысл определять категорию максимально использующую все переводы.


Или свой вариант, взяв за основу хранения переводов в базе но со своим управлением (формирования ключей, переводов и их хранения).

Основное


  • Вызов перевода остается стандартным Yii::t();
  • Хранить переводы с ключами будем в MySQL;
  • Временное хранилище по языкам в Redis;
  • Сбор ключей (категорий) остается прежним;

В чем плюсы:


  • быстрота;
  • гибкость;

Создание конфигурационного файла i18n.php


Начнем с того что создадим конфигурационный файл сборщика ключей такой консольной командой:

php yii message/config @common/config/i18n.php

После этой консольной команды файл i18n.php появится в common/config/ или просто его создадим такого вида:

common/config/i18n.php
 return [
    'color' => null,
    'interactive' => true,
    'help' => null,
	'sourcePath' =>  __DIR__. '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR,
    'languages' => ['ru-RU','uk-UA','en-US'],//языки перевода должны быть
    'translator' => 'Yii::t',
    'sort' => false,
    'overwrite' => true,
    'removeUnused' => false,
    'markUnused' => true,
    'except' => [
        '.svn',
        '.git',
        '.gitignore',
        '.gitkeep',
        '.hgignore',
        '.hgkeep',
        '/messages',
        '/BaseYii.php',
    ],
    'only' => [
        '*.php',
    ],
    'format' => 'db',
    'db' => 'db',
    //'messageTable' => '{{%message}}', // игнорируем так как будет своя  таблица gr_dictionary
    'sourceMessageTable' => '{{%gr_dictionary}}',// таблица переводов
    'ignoreCategories' => ['yii'],
];

Создание таблиц в MySQL


Далее создадим три таблицы для основного хранения всех языков с переводами:

gr_language (языки)
CREATE TABLE IF NOT EXISTS `gr_language` (
  `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `code_lang` varchar(255) NOT NULL,
  `local` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL, 
  `status` tinyint(4) NOT NULL DEFAULT '1',
  UNIQUE KEY `code_lang` (`code_lang`),
  UNIQUE KEY `local` (`local`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 INSERT INTO `gr_language` (`id`, `code_lang`, `local`, `name`, `status`) 
VALUES (1, 'en', 'en-US', 'English',  1),
(2, 'ru', 'ru-RU', 'Русский',  1),
(3, 'uk', 'uk-UA', 'Українська', 1);


gr_dictionary_keys (ключи)
 -- таблица по ключам
CREATE TABLE IF NOT EXISTS `gr_dictionary_keys` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `key` varchar(250) NOT NULL,
  UNIQUE KEY `id` (`id`),
  KEY `key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


gr_dictionary (переводы)
-- таблица с переводами
CREATE TABLE IF NOT EXISTS `gr_dictionary` (
  `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `language_id` smallint(5) unsigned NOT NULL,
  `key`  int(10) unsigned NOT NULL,
  `value` varchar(255) NOT NULL COMMENT 'шаблон',
  `translator` text NOT NULL  COMMENT 'перевод',
  `type` set('w','m') DEFAULT NULL COMMENT 'w/m слово/предложение',
  `status` tinyint(4) NOT NULL DEFAULT '1',
	CONSTRAINT `gr_dictionary_ibfk_1` 
	  FOREIGN KEY (`language_id`) 
	  REFERENCES `gr_language` (`id`) 
	  ON DELETE CASCADE 
	  ON UPDATE CASCADE,
	CONSTRAINT `gr_dictionary_ibfk_2` 
	  FOREIGN KEY (`key`) 
	  REFERENCES `gr_dictionary_keys` (`id`) 
	  ON DELETE CASCADE 
	  ON UPDATE CASCADE,
 UNIQUE KEY `language_id` (`language_id`,`key`,`type`),
 KEY `code_lang` (`language_id`),
 KEY `type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


Переопределим консольный контроллер


Теперь, до сбора ключей, переопределим консол��ный контроллер \yii\console\controllers\MessageController который отвечает за сбор всех ключей. Для этого создам свой контроллер который наследуются от него.

Создадим файл console\controllers\Message2Controller.php такого вида:

console\controllers\Message2Controller.php
namespace console\controllers;
use Yii;
use yii\console\Exception;

class Message2Controller extends \yii\console\controllers\MessageController
{
    /**
     * Saves messages to database
     *
     * @param array $messages  Это двухмерный массив ключей [[категори]=>[[значение],[...]] ,... ]
     * @param \yii\db\Connection $db
     * @param string $sourceMessageTable Наша таблица для переводов
     * @param string $messageTable  Не используем
     * @param boolean $removeUnused
     * @param array $languages  Это массив языков languages из i18n.php ['ru-RU',...]
     * @param boolean $markUnused
     */

    protected function saveMessagesToDb($messages, $db, $sourceMessageTable, $messageTable, $removeUnused, $languages, $markUnused)
    {
        try{
            $pr_iskey=Yii::$app->db->createCommand("SELECT `id`  FROM `gr_dictionary_keys` WHERE `key`=:key");
            $pr_inskey=Yii::$app->db->createCommand("INSERT INTO `gr_dictionary_keys`( `key`) VALUES (:key)");
            $pr_delkey=Yii::$app->db->createCommand("DELETE FROM `gr_dictionary_keys` WHERE `id`=:id");

            $id_lang=[];
            $pr_l=Yii::$app->db->createCommand("SELECT SQL_NO_CACHE id FROM gr_language WHERE local=:local LIMIT 1");
            foreach ($languages as $language) {
                if(!isset($id_lang[$language])){
                    $id_language=(int)$pr_l->bindValue(":local", $language,2)->queryScalar();
                    if(empty($id_language)){
                        continue;
                      //  throw new Exception("Unknow lang type $language");
                    }
                    $id_lang[$language]=(int)$id_language;
                }

            }

            if(empty($id_lang))throw new Exception("empty lang");
            //ALTER TABLE `yii2advanced`.`gr_dictionary` ADD UNIQUE (`language_id`, `key`, `type`);
            $pr_d=Yii::$app->db->createCommand("INSERT IGNORE INTO `gr_dictionary`( `language_id`, `key`, `value`, `type`) VALUES (:language_id,:key,:value,:type)");
            foreach ($messages as  $category => $msgs){
                list($type,$key)=explode(":", $category);

                if(empty($id=$pr_iskey->bindValue(":key", $key,2)->queryScalar())){
                    $pr_inskey->bindValue(":key", $key,2)->execute();
                    $id=Yii::$app->db->lastInsertID;
                }

                foreach ($id_lang as $id_language) {
                    $pr_d->bindValue(":language_id", $id_language,1)
                            ->bindValue(":key", $id,1)
                            ->bindValue(":value", $msgs[0],2)
                            ->bindValue(":type", $type,2)
                            ->execute();
                }
            }


            // удалить лишние ключи со status=1 (не используемые на страницах)
            $query=Yii::$app->db->createCommand("SELECT SQL_NO_CACHE dk.`id`,CONCAT(d.`type`,':',dk.`key`) as 'key_' FROM `gr_dictionary` d,gr_dictionary_keys dk WHERE d.`key`=dk.id AND  status=1")->query();
            //$pr_del=Yii::$app->db->createCommand("DELETE FROM `gr_dictionary` WHERE `key`=:key");
            while(($data=$query->read())!=false){
                if(array_key_exists($data['key_'], $messages)===false){
                    //$pr_del->bindValue(":key", $data['id'],1)->execute();
                    $pr_delkey->bindValue(":id", $data['id'],1)->execute();
                }
            }

            Yii::$app->db->createCommand("ALTER TABLE gr_dictionary AUTO_INCREMENT = 1;")->execute();
        }catch (\Exception $e){
           //пишем в лог
        }
    }
}



Суть тут в том, что нам нужен только один метод saveMessagesToDb, который заполняет таблицу gr_dictionary из конфигурационного файла common/config/i18n.php

'sourceMessageTable' => '{{%gr_dictionary}}'

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

Теперь можем вызвать сбор ключей командой к нашему контроллеру:

php yii message2/extract @common/config/i18n.php

В результате должны заполнится две таблицы (gr_dictionary и gr_dictionary_keys). По каждому языку из таблицы gr_language будет создана запись для перевода.

Добавляем components i18n


Далее добавляем в i18n component конфигурационного файла common\config\main.php:

common\config\main.php
 ...
 'language'=> 'ru-RU',
 'sourceLanguage' => 'en-US',
 'components' => [
	 'i18n' => [
				'translations' => [
					'*' => [
						'class' => 'common\models\AltDbMessageSource',
                                         // переопределенный класс  yii\i18n\DbMessageSource
					],
				],
			],
	'lng' => [
		'class' => '\common\components\LanguageExtension',
	],
	...


  1. Компонент Yii i18n будет срабатывать при вызовах Yii::$app->t().Класс отвечающий за это yii\i18n\DbMessageSource но мы его переопределим common\models\AltDbMessageSource.

  2. Компонент lng это наш класс \common\components\LanguageExtension отвечающий за работу с Redis

Переопределим yii\i18n\DbMessageSource


Класс отвечающий за перевод мы реализуем по своему

yii\i18n\DbMessageSource
namespace common\models;
use Yii;
 
class  AltDbMessageSource extends  \yii\i18n\MessageSource {

    public $sourceLanguage;

    public function init()
    {
        parent::init();
        if ($this->sourceLanguage === null) {
            $this->sourceLanguage = Yii::$app->sourceLanguage;
        }
    }
	
    protected function translateMessage($category, $message, $language)
    {   
       /* 
           узнаем из ключа (w:key) какой метод вызвать
           в моей реализации в компоненте lng есть два метода w и m
           можно пользоваться одним методом, чтобы не производить лишних манипуляций
           и вызывать сразу метод перевода.
      */
        list($type,) = explode(":",$category); 
        return  Yii::$app->lng->$type($category);
    }

    public function translate($category, $message, $language)
    {
        if ( $language !== $this->sourceLanguage ) {
            return $this->translateMessage($category, $message, $language);
        } else {
            return false;
        }
    }
}


Метод translateMessage вызывается когда мы вызываем Yii::t('категория','значение'). Тут важно как мы собираемся организовать вид ключа. Можно через сепаратор : с помощью которого в Redis будут созданы папки с иерархией, что дает наглядность. К примеру: такие ключи
Yii::t('ru-RU:type:uniq_view','значение') будут выглядеть в RedisAdmin так:

  • ru-RU:
    • type:
      • uniq_view: значение
      • uniq_view: значение
      • uniq_view: значение

Что позволит делать с помощью Redis такие выборки:
$redis->executeCommand("KEYS",["KEY" => $lang_key.":".$type.":*"]);
$redis->executeCommand("KEYS",["KEY" => $lang_key.":*"]);

Ключ языка ru-RU и др. будем добавлять в момент заполнения Redis в компоненте \common\components\LanguageExtension.

Напишем компонент \common\components\LanguageExtension


Компонент нужен для получения перевода по ключу из Redis или массива если Redis отвалился.

common\components\LanguageExtension
namespace common\components\extensions;

use  Yii;
use common\components\exceptions\LanguageException;
use yii\db\Exception;
use PDO;


/**
 * Class LanguageExtension
 * @package common\components\extensions
 * Задачи:
 * Инициализация словаря
 * Заполнение словаря redis
 */
class LanguageExtension extends \yii\base\Object
{
    private $language;                      // код языка - по умолчанию ru
    private $w = [];                        // словарь слов
    private $m = [];                        // словарь сообщений
    private $storageConnection;             // объект доступа к редису
    private $storageStatus;                 // статус редиса для словаря
    private $numbDb;                     // база redis
    private $default_key;                   // флаг заполненности словаря
    private $expire;

    public function __construct() {
        try{
            $this->expire = Yii::$app->params['secretKeyExpire']??(60 * 60 * 60);
            $this->language =  \Yii::$app->language;
            $language=LanguageExtension::currentLang();

            if(!empty($language)){
                if($this->idKeyLang($language)) {
                    $this->language= $language;
                }
            }
            $this->numbDb=Yii::$app->params['redisLangDB']??11;

            $this->storageStatus = false;
            $this->default_key=  $this->language.":index";
            $this->storageConnection = new \yii\redis\Connection([
                'hostname' => Yii::$app->params['redisHost'],
              //  'password' => '',
                'port' => 6379,
                'database' => $this->numbDb,
            ]);

            if(empty($this->language)) throw new LanguageException("not default language",0);
            $this->init();
        }catch ( LanguageException $event){
           // echo $event->getMessage();

        }catch ( \yii\db\Exception $event){
             $this->init();
        }catch (\Exception $event){
           // echo  $event->getMessage();
        }

    }

    public function __destruct() {
        try{
            if($this->storageConnection->isActive)  $this->storageConnection->close();
        }catch (\Exception $event){
        }
    }

    /**
     * бизнес логика. 
     * Инициализация словаря. Проверка на существование словаря в редисе. 
     * Полное заполнение словаря в редис.
     */
    public function init(){
        try{
            $this->storageConnection->open();
          
            // загружен ли словарь в redis
            if(!$this->isFullData()){
                // загрузка из mysql базы слов в redis
                $this->loadRedis();
            }
            $this->storageStatus = true;
        } catch ( \yii\db\Exception $event) {
            $this->storageStatus = false;
            // бизнес логика. 
           // Заполнение словаря в массивы $w и $m  согласно выбранному языку и интерфейсу.
            $this->loadVariable();
        }
    }

    public static function currentLang(){
        try{
            $language = isset($_COOKIE['userLang']) ? $_COOKIE['userLang'] : null;
            if(!$language && Yii::$app->session->has('userLang')) {
               $language = Yii::$app->session('userLang');
            }
            if(empty($language))$language=\Yii::$app->language;
              return $language;
        }
        catch (\Exception $e){
            print_r($e->getMessage());exit;
        }
    }

    private function idKeyLang(string $key){
        if(!empty($key)){
            return Yii::$app->db->createCommand("SELECT `id`  FROM `gr_language` WHERE local=:local")
              ->bindValue(":local", $key,PDO::PARAM_STR)
              ->queryScalar();
        }
        return false;
    }

    /**
     * @param string $type
     * @param string $key
     * @return string
     * Строит ключ
     */
    private function getKeyMD5(string $type,string $key):string {
        return $this->language.":".$type.":".md5($key);
    }

    /**
     * @return bool
     * Заполнение локальной переменной словарем
     */
    private function loadVariable():bool{
        try{
 
            // бизнес логика. 
           // Заполнение словаря в массивы $w  и $m  согласно выбранному языку и интерфейсу.
            $language_id=$this->idKeyLang($this->language);

            $res=\Yii::$app->db->createCommand("SELECT  d.`type`,d.`value`,d.`translator`, dk.`key`  FROM `gr_dictionary` d,gr_dictionary_keys dk WHERE d.`key`=dk.id AND d.language_id=:language_id")
                ->bindValue(":language_id", $language_id,PDO::PARAM_INT)->query();
            $this->w=$this->m=[];
            while(($data=$res->read())!=false){
                if(method_exists($this, $data['type'])){
                    $this->{$data['type']}[$this->getKeyMD5($data['type'],$data['key'])]=$data['translator'];
                }
            }

            return true;
        }catch (\Exception $event){
            echo $event->getLine()."|".$event->getMessage();exit;
            return false;
        }
    }

    /**
     * @return bool
     * Загрузка слов в redis из mysql (языка системы)
     */
    private function loadRedis():bool{
        try{
            $language_id=$this->idKeyLang($this->language);
            
            $res=\Yii::$app->db->createCommand("SELECT d.`type`,dk.`key`, d.`value`,d.`translator`  FROM `gr_dictionary` d,gr_dictionary_keys dk WHERE d.`key`=dk.id AND d.language_id=:language_id")
                ->bindValue(":language_id", $language_id,PDO::PARAM_INT)->query();

            $this->storageConnection->executeCommand('SETEX', [ "KEY" =>  $this->default_key,"SECONDS"=>$this->expire,"VALUE"=> "1"]);

            while(($data=$res->read())!=false){
                $this->storageConnection->executeCommand('SETEX', ["KEY" => $this->getKeyMD5($data['type'],$data['key']),"SECONDS"=>$this->expire,"VALUE"=> $data['translator']]);
            }

           if(empty($this->storageConnection->executeCommand('LASTSAVE', [] )))
            $this->storageConnection->executeCommand('BGSAVE', [] );

            return true;
        }catch (\Exception $event){
            echo $event->getMessage();exit;
        }
    }

    /**
     * Очистить Redis
     */
    public function flushdb(){
        try{
            if($this->storageConnection->isActive) $this->storageConnection->executeCommand('FLUSHDB');
            else {
                $this->w=[];
                $this->m=[];
            }
        }catch (\Exception $event){

        }
    }

    /**
     * @return bool
     * проверка существования в redis слов по дефолтному ключу и количество ключей словаря
     */
    private function isFullData():bool
    {
        try{
            $res= $this->storageConnection->executeCommand('INFO', [ ] );
            preg_match("/.*db$this->numbDb:keys=([\d])*.*?/uis",$res,$arr);
            if(isset($arr[1]) && $arr[1]>1){
                return  $this->exists($this->default_key);
            }
            return false;
        }catch (\Exception $event){
            echo $event->getMessage();
            return false;
        }
    }

    /**
     * @param string $key
     * @return string
     * Возвращает слово по его ключу из загруженного словаря
     */
    public function w(string $key) : string {
        return $this->getKeyValue($key, 'w');
    }

    /**
     * @param string $key
     * @return string
     * Возвращает предложение по его ключу из загруженного словаря
     */
    public function m(string $key) : string {
        return $this->getKeyValue($key, 'm');
    }

    /**
     * @param string $key
     * @param string $type
     * @return string
     * Интерфейс выбора значения
     * бизнес логика. Выборка из редиса или еще откуда-то.
     */
    private function getKeyValue ( string &$key, string $type ) : string {
        try{
            if(!$key=trim($key))
                throw new LanguageException("Error dictionary ".addslashes($key).". The ".addslashes($key)." can not be empty or contain only whitespace.", 777001);

            if($this->storageStatus)
                $value = $this->storageConnection->executeCommand("GET",["KEY" =>$this->getKeyMD5($type,$key)]);
            else{
                $value = @$this->$type[$this->getKeyMD5($type,$key)];
            }
              
            /*повесить свой обработчик if(!$value){
                if ($this->hasEventHandlers(\yii\i18n\MessageSource::EVENT_MISSING_TRANSLATION)) {
                    $event = new \yii\i18n\MissingTranslationEvent([
                        'category' => $key,
                        'message' => $key,
                        'language' => $this->language,
                    ]);
                    $this->trigger(\yii\i18n\MessageSource::EVENT_MISSING_TRANSLATION, $event);
                }
            }*/
            return $value ? $value : $key;
        }catch (\Exception $event){
            return $key;
        }
    }
 
    /**
     * @param $key
     * @return bool
     * Удалить ключ
     */
    public function del($key):bool{
        try{
            if($this->storageConnection->isActive){
                return $this->storageConnection->executeCommand("DEL",["KEY" =>$key]);// keys test:1:v
            }else{
                list($lang_key,$type,$key_)= explode(":", $key);
                if(method_exists($this, $type) && isset($this->$type[$key_])){
                    unset($this->$type[$key_]);
                    return true;
                }
                return false; 
            }
        }catch (\Exception $event){
            return false;
        }
    }

    /**
     * @param string $lang_key
     * @param null $type
     * @return bool
     * Удалить ключи по языку типа или всего языка
     */
    public function delAll(string $lang_key,$type=null){
        try{
            if($this->storageConnection->isActive){
                   $keys= $this->keys($lang_key,$type);
                    if(!empty($keys)){
                        foreach ($keys as $key){
                            $this->del($key);
                        }
                        if($type==null) $this->del($lang_key.":index");
                    }
            }else{
                $this->w=$this->m=[];
                    return true;
            }
        }catch (\Exception $event){
            return false;
        }
    }

    /**
     * @param $type
     * @param $key
     * @return array
     * Вернуть все ключи блока
     */
    public function keys(string $lang_key,$type=null):array{
        try{
            if($this->storageConnection->isActive){
                if($type!=null)
                    return $this->storageConnection->executeCommand("KEYS",["KEY" => $lang_key.":".$type.":*"]); 
                else
                    return $this->storageConnection->executeCommand("KEYS",["KEY" => $lang_key.":*"]);
            }else{
                if($type!=null){
                    return   $this->w+$this->m;
                }else{
                    if(method_exists($this, $type))return $this->$type;
                }
                return [];
            }
        }catch (\Exception $event){
            return [];
        }
    }

    /**
     * @param $type
     * @param $key
     * @return bool
     * Проверка существования ключа
     */
    public function exists($key):bool{
        try{
            if($this->storageConnection->isActive){
                return $this->storageConnection->executeCommand("EXISTS",["KEY" =>$key]);
            }else{
                // return (method_exists($this, $type) && isset($this->$type[$key]));
                list($lang_key,$type,$key_)= explode(":", $key);
                if(method_exists($this, $type))return isset($this->$type[$key_]);
                return false;
            }
            return false;
        }catch (\Exception $event){
            return false;
        }
    }
}


Суть


Хранение всего словаря языка по дефолтному Yii::$app->language значению языка, если нет COOKIE данных, в Redis или в массиве если Redis не сработал, по типу значения $this->w[] слово ,$this->m[] сообщение. Но это моя реализация, а у вас может быть все в одном буфере.

Как он работает


При инициализации проверяем коннект Redis. Если его нет то заполняем буфер, если он есть то заполняем его, а источник в обоих случаях MySQL.

Важный момент, перед заполнением мы конечно проверяем загружен ли язык уже в систему, путем проверки дефолного ключа ru-RU:index который мы устанавливаем если его нет при загрузки.

И так, в MySQL есть 4 языка. Идут коннекты от пользователей на ru-RU язык, что мы делаем? Мы грузим Redis из MySQL весь ru-RU если его там нет и раздаем его, далее есть коннект на en-US, подгружаем в Redis и этот язык, теперь у нас два языка в системе загружено.

Жить конечно они могут вечно но у меня в компоненте устанавливается время на ключ

$redis->executeCommand('SETEX', ["KEY" => $this->getKeyMD5($data['type'],$data['key']),"SECONDS"=>$this->expire,"VALUE"=> $data['translator']]);

Виджет смены языка


frontend\widgets\WLang
namespace frontend\widgets\WLang;
use frontend\models\Lang;
use Yii;
use PDO;

class WLang extends \yii\bootstrap\Widget
{
    public function init(){}

    public function run() {
        return $this->render('index', [
            'current' => \common\components\LanguageExtension::currentLang(),
            'default' => \Yii::$app->db->createCommand("SELECT `id`, `code_lang`, `local`, `name`, `default_lang`, `status` FROM `gr_language` WHERE local=:local LIMIT 1")->bindValue(":local",\common\components\LanguageExtension::currentLang(),PDO::PARAM_STR)->queryOne(PDO::FETCH_OBJ),
            'langs' =>\Yii::$app->db->createCommand("SELECT `id`, `code_lang`, `local`, `name`,  `status` FROM `gr_language` WHERE 1")->queryAll(PDO::FETCH_OBJ),
        ]);
    }
}
 
 
 
 
<div>
<?php
use yii\helpers\Html;
?>
    <script>
         function click_(el) {
            var lang = $(el).attr('data-lang'); 
            var date = new Date;
            date.setDate(date.getDate() + 1);
            document.cookie = "userLang=" + lang + "; path=/; expires=" + date.toUTCString() + ";";
            location.reload();
        }
        </script>

<div id="lang">
    <span id="current-lang">
        <span class="show-more-lang" >Текущий язык <?= $current;?></span>
    </span>
    <ul id="langs">
        <?php foreach ($langs as $lang):?>
            <li class="item-lang"><a href="#" onclick="click_(this)" data-lang="<?=$lang->local?>"><?=$lang->code_lang?></a></li>
        <?php endforeach;?>
    </ul>
</div>
 
</div>


Вся суть виджета — это отобразить все доступные языки и установить COOKIE данные.

Далее, когда добавятся еще переводы или удалятся на страницах нашего приложения мы просто вызываем сбор ключей и вносим в MySQL их значения.

good luck, Jekshmek