- PVSM.RU - https://www.pvsm.ru -
В Yii2 есть возможность реализовать три варианта интернационализации:
1. Файл с массивом, вида: ключ=>перевод (гибко);
2. Файл с расширением .po,.mo бинарный (нужен компилятор, быстро);
3. База данных mysql, две таблицы для ключей и переводом (лучшая реализация при именовании уникальных категорий или привязанных к странице так как yii вытягивает по категории все ключи );
Или свой вариант взяв за основу хранения переводов в базе но со своим управлением (формирования ключей, переводов и их хранения).
Вызов перевода остается стандартным Yii::t(). Хранить переводы с ключами будем в MySQL. Временное хранилище по текущему языку будет в Redis. Сбор ключей (категорий) остается прежним.
Начнем с того что создадим конфигурационный файл сборщика ключей такой консольной командой:
php yii message/config @common/config/i18n.php
После этой консольной команды файл i18n.php появится в common/config/ или просто его создадим такого вида:
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'],
];
Далее создадим три таблицы для основного хранения всех языков с переводами:
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);
-- таблица по ключам
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;
-- таблица с переводами
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;
Теперь, до сбора ключей, переопределим консольный контроллер yiiconsolecontrollersMessageController который отвечает за сбор всех ключей. Для этого создам свой контроллер который наследуются от него.
Создадим файл consolecontrollersMessage2Controller.php такого вида:
namespace consolecontrollers;
use Yii;
use yiiconsoleException;
class Message2Controller extends yiiconsolecontrollersMessageController
{
/**
* Saves messages to database
*
* @param array $messages Это двухмерный массив ключей [[категори]=>[[значение],[...]] ,... ]
* @param yiidbConnection $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() [1].Но можно и другую таблицу использовать, тут мы уже решаем как нам лучше. Добавил удаление ключей, а с ними и переводов по ссылке внешнего ключа если на сайте этот ключ больше не используется.
Теперь можем вызвать сбор ключей командой к нашему контроллеру:
php yii message2/extract @common/config/i18n.php
В результате должны заполнится две таблицы (gr_dictionary и gr_dictionary_keys). По каждому языку из таблицы gr_language будет создана запись для перевода.
Далее добавляем в components конфигурационного файла commonconfigmain.php:
...
'language'=> 'ru-RU',
'sourceLanguage' => 'en-US',
'components' => [
'i18n' => [
'translations' => [
'*' => [
'class' => 'commonmodelsAltDbMessageSource',// переопределенный класс yiii18nDbMessageSource
],
],
],
'lng' => [
'class' => 'commoncomponentsLanguageExtension',
],
...
Класс отвечающий за перевод мы реализуем по своему
namespace commonmodels;
use Yii;
class AltDbMessageSource extends yiii18nMessageSource {
public $sourceLanguage;
public function init()
{
parent::init();
if ($this->sourceLanguage === null) {
$this->sourceLanguage = Yii::$app->sourceLanguage;
}
}
protected function translateMessage($category, $message, $language)
{
return Yii::$app->lng->translate($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_wiev','значение')
будут выглядеть в RedisAdmin так:
Что позволит делать с помощью Redis такие выборки:
$redis->executeCommand("KEYS",["KEY" => $lang_key.":".$type.":*"]);
$redis->executeCommand("KEYS",["KEY" => $lang_key.":*"]);
Ключ языка ru-RU и др. будем добавлять в момент заполнения Redis в компоненте commoncomponentsLanguageExtension.
Компонент нужен для возвращения перевода по ключу из Redis или массива если Redis отвалился.
Для инициализации компонента будем вызывать его в beforeAction контроллера
namespace commoncomponentsextensions;
use Yii;
use commoncomponentsexceptionsLanguageException;
use yiidbException;
use PDO;
/**
* Class LanguageExtension
* @package commoncomponentsextensions
* Задачи:
* Инициализация словаря
* Заполнение словаря redis
*/
class LanguageExtension extends yiibaseObject
{
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 yiiredisConnection([
'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 ( yiidbException $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 initModel()
{
return new LanguageExtension();
}
/**
* бизнес логика. Инициализация словаря. Проверка на существование словаря в редисе. Полное заполнение словаря в редис.
*/
public function init(){
try{
$this->storageConnection->open();
//if($this->storageConnection->getIsActive()==false) throw new LanguageException("No connect Redis ",0);
// загружен ли словарь в redis
if(!$this->isFullData()){
// загрузка из mysql базы слов в redis
$this->loadRedis();
}
$this->storageStatus = true;
} catch ( yiidbException $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=Yii::$app->db->createCommand("SELECT `id` FROM `gr_language` WHERE local=:local")->bindValue(":local", $this->language,PDO::PARAM_STR)->queryScalar();
$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 `type`,`key`, `value`,`translator` FROM `gr_dictionary` WHERE language_id=:language_id")
//->bindValue(":language_id", $language_id,PDO::PARAM_INT)->queryAll(PDO::FETCH_ASSOC | PDO::FETCH_GROUP ,1);
$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;
return false;
}
}
/**
* Очистить 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(yiii18nMessageSource::EVENT_MISSING_TRANSLATION)) {
$event = new yiii18nMissingTranslationEvent([
'category' => $key,
'message' => $key,
'language' => $this->language,
]);
$this->trigger(yiii18nMessageSource::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.":*"]);// keys test:1:*
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']]);
namespace frontendwidgetsWLang;
use frontendmodelsLang;
use Yii;
use PDO;
class WLang extends yiibootstrapWidget
{
public function init(){}
public function run() {
return $this->render('index', [
'current' => commoncomponentsLanguageExtension::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",commoncomponentsLanguageExtension::currentLang(),PDO::PARAM_STR)->queryOne(PDO::FETCH_OBJ),
'langs' =>Yii::$app->db->createCommand("SELECT `id`, `code_lang`, `local`, `name`, `default_lang`, `status` FROM `gr_language` WHERE 1")->queryAll(PDO::FETCH_OBJ),
]);
}
}
<div>
<?php
use yiihelpersHtml;
?>
<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 [2]
Автор: Jekshmek
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/redis/223615
Ссылки в тексте:
[1] Yii::t() : https://github.com/yiisoft/yii2/blob/master/docs/guide-ru/tutorial-i18n.md#Указатели-с-расширенным-форматированием
[2] Jekshmek: https://habrahabr.ru/users/jekshmek/
[3] Источник: https://habrahabr.ru/post/318102/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.