Yii2-advanced: Делаем интернационализацию с источником в Redis

В 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
  • +12
  • 6.8k
  • 2
Share post

Comments 2

Only users with full accounts can post comments. Log in, please.