Мультиязычный сайт на Yii: Перевод контента моделей

Флаги государств

Это продолжение статьи Мультиязычный сайт на Yii: Интерфейс и URL, в которой мы рассмотрели способы указания языка в URL адресе страницы, переопределив всего два метода пары стандартных компонентов Yii, и использование многоязычности. В этой части мы коснёмся непосредственно перевода текста наших динамических страниц и статей.

Мультиязычные модели

В простейшем случае у нас имеется одноязычная модель Post:

/**
 * @property integer $id
 * @property string $title
 * @property string $text
 * @property string $image
 * @property integer $public
 */
class Post extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
 
    public function tableName()
    {
        return '{{post}}';
    }
 
    public function rules()
    {
        return array(
            array('title, text', 'required'),
            array('title', 'length', 'max'=>'255'),
            array('public', 'numerical', 'integerOnly'=>true),
            array('image', 'file', 'types'=>'jpg,jpeg,gif,png', 'allowEmpty'=>true, 'safe'=>false),
            array('id, title, text, public', 'safe', 'on'=>'search'),
        );
    } 
 
    public function attributeLabels()
    {
        return array(
            'id' => 'ID',
            'title' => 'Заголовок',
            'text' => 'Текст',
            'image' => 'Изображение',
            'public' => 'Опубликовано',
        );
    }    
}

и связанная с ней таблица в базе данных.

Рассмотрим пару способов, как мы можем хранить несколько переводов контента на каждом языке.

Хранение вариантов перевода в одной таблице

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

Чтобы избежать такой избыточности, можно просто добавить в модель поля для разных языков:

/**
 * @property integer $id
 * @property string $title_ru
 * @property string $title_en
 * @property string $title_de
 * @property string $text_ru
 * @property string $text_en
 * @property string $text_de
 * @property string $image
 * @property integer $public
 */
class Post extends CActiveRecord
{
    // ...
 
    public function rules()
    {
        return array(
            array('title, text', 'safe'),
 
            array('title_ru, title_en, title_de', 'required'),
            array('text_ru, text_en, text_de', 'required'),            
            array('title_ru, title_en, title_de', 'length', 'max'=>'255'),
 
            array('public', 'numerical', 'integerOnly'=>true),
            array('id, title, text, public', 'safe', 'on'=>'search'),
        );
    }  
 
    public function attributeLabels()
    {
        return array(
            'id' => 'ID',
            'title' => Yii::t('BlogModule.blog', 'Title'),
            'text' => Yii::t('BlogModule.blog', 'Text'),
            'image' => Yii::t('BlogModule.blog', 'Image'),
            'public' => Yii::t('BlogModule.blog', 'Public'),
        );
    } 
 
    public function search()
    {
        $criteria = new CDbCriteria;
 
        $criteria->compare('t.id', $this->id);
 
        $criteria->compare('t.title_ru', $this->title_ru, true);
        $criteria->compare('t.title_en', $this->title_en, true);
        $criteria->compare('t.title_de', $this->title_de, true);
 
        $criteria->compare('t.text_ru', $this->text_ru, true);
        $criteria->compare('t.text_en', $this->text_en, true);
        $criteria->compare('t.text_de', $this->text_de, true);
 
        $criteria->compare('t.public', $this->public);
 
        return new CActiveDataProvider($this, array(
            'criteria' => $criteria;
        ));
    }
 
    public function getTitle()
    {
        $attribute = 'title_' . Yii::app()->getLanguage();
        return $this->{$attribute};
    }
 
    public function setTitle($value)
    {
        $attribute = 'title_' . Yii::app()->getLanguage();
        $this->{$attribute} = $value;
    }
 
    public function getText()
    {
        $attribute = 'text_' . Yii::app()->getLanguage();
        return $this->{$attribute};
    }
 
    public function setText($value)
    {
        $attribute = 'text_' . Yii::app()->getLanguage();
        $this->{$attribute} = $value;
    }
}

Как видно, мы добавили геттеры и сеттеры для доступа к нашим мультиязычным атрибутам. Теперь неизменный код в представлении

<?php
<h1><?php echo CHtml::encode($model->title); ?></h1>

обратится к геттеру и выведет нам заголовок на текущем языке. Также без изменений на каждом текущем языке будет работать, например, вывод в гриде, поиск.

В форме редактирования записи теперь необходимо указывать по несколько полей для каждого языка. Мы сделаем это в цикле:

<?php
<?php foreach (Yii::app()->params['translatedLanguages'] as $key=>$lang): ?>
    <div class="row">
        <?php echo $form->labelEx($model, 'title'); ?> <?php echo $lang; ?><br />
        <?php echo $form->textField($model, 'title_' . $key, array('maxlength'=>255)); ?><br />
        <?php echo $form->error($model, 'title_' . $key); ?>
    </div>
<?php endforeach; ?>

Теперь чтобы добавить ещё один язык нужно дописать новые поля в таблицы и в модели.

Хранение переводов в отдельной таблице

В предыдущем примере есть небольшая проблема. Она заключается в том, что поля для языков нужно прописывать вручную, и поэтому приложение трудно настраивать. Чтобы добавить новый язык или удалить лишний, нужно исправлять код всех моделей.

От нашего примера с геттерами и сеттерами для каждого свойства

public function getTitle()
{
    $attribute = 'title_' . Yii::app()->getLanguage();
    return $this->{$attribute};
}
 
public function setTitle($value)
{
    $attribute = 'title_' . Yii::app()->getLanguage();
    $this->{$attribute} = $value;
}

можно перейти к универсальным магическим методам __get и __set, через которые работать с переводами.

Также можно таким образом вообще перенести переводы в отдельную таблицу. Впоследствии полученную реализацию можно выделить в отдельное поведение.

Придумывать своё мы не будем, так как оно уже есть. Это MultilingualBehavior. Именно на его основе мы дали имена нашим параметрам translatedLanguages и defaultLanguage.

Достаточно перевести вашу таблицу {{post}} из MyISAM в InnoDB и создать таблицу {{post_lang}} с полями l_title и l_text для хранения переводов:

CREATE TABLE IF NOT EXISTS post_lang (
    l_id int(11) NOT NULL AUTO_INCREMENT,
    owner_id int(11) NOT NULL,
    lang_id varchar(6) NOT NULL,
    l_title varchar(255) NOT NULL,
    l_text TEXT NOT NULL,
    PRIMARY KEY (l_id),
    KEY owner_id (owner_id),
    KEY lang_id (lang_id),
    CONSTRAINT post_lang_owner FOREIGN KEY (owner_id) REFERENCES post (id) ON DELETE CASCADE ON UPDATE CASCADE;
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

Теперь нужно подключить поведение к модели, подправить метод search() и добавить метод defaultScope():

/**
 * @property integer $id
 * @property string $title
 * @property string $text
 * @property string $image
 * @property integer $public
 */
class Post extends CActiveRecord
{
    // ...
 
    public function rules()
    {
        return array(
            array('title, text', 'required'),
            array('title', 'length', 'max'=>'255'),
            array('public', 'numerical', 'integerOnly'=>true),
            array('image', 'file', 'types'=>'jpg,jpeg,gif,png', 'allowEmpty'=>true, 'safe'=>false),
            array('id, title, text, public', 'safe', 'on'=>'search'),
        );
    } 
 
    public function search()
    {
        $criteria = new CDbCriteria;
 
        $criteria->compare('t.id', $this->id);        
        $criteria->compare('t.title', $this->title, true);
        $criteria->compare('t.text', $this->text, true);        
        $criteria->compare('t.public', $this->public);
 
        return new CActiveDataProvider($this, array(
            'criteria' => $this->ml->modifySearchCriteria($criteria),
        ));
    }
 
    public function behaviors()
    {
        return array(
            'ml' => array(
                'class' => 'ext.multilangual.MultilingualBehavior',
                'localizedAttributes' => array(
                    'title',
                    'text',
                ),
                'langClassName' => 'PostLang',
                'langTableName' => '{{post_lang}}',
                'languages' => Yii::app()->params['translatedLanguages'],
                'defaultLanguage' => Yii::app()->params['defaultLanguage'],
                'langForeignKey' => 'owner_id',
                'dynamicLangClass' => true,
            ),
        );
    }
 
    public function defaultScope()
    {
        return $this->ml->localizedCriteria();
    }
}

Теперь стандартный вызов

$model = Post::model()->findByPk($id);
echo $model->title;

через геттер поведения будет возвращать для этой модели значение поля l_title, у которого lang_id будет равно значению Yii::app()->language.

При указании

'dynamicLangClass' => true,

по умолчанию поведение само создаст полупустой класс-модель PostLang для последующей работы. Но иногда бывают ситуации, что этот класс нужно изменить.

Например, если вы фильтруете HTML код или конвертируете исходный текст формата Markdown в HTML в методе beforeSave() модели, то такой же конвертор нужно вставить и внутрь модели PostLang. Вот для примера модель, использующая Markdown конвертер из поведения DPurifyTextBehavior для обработки контента из поля text в поле text_markdown в момент сохранения записи:

/**
 * @property integer $id
 * @property string $title
 * @property string $text
 * @property string $text_markdown
 * @property string $image
 * @property integer $public
*/
class Post extends CActiveRecord
{  
    // ...
 
    public function behaviors()
    {
        return array(
            'PurifyText'=>array(
                'class'=>'DPurifyTextBehavior',
                'sourceAttribute'=>'text',
                'destinationAttribute'=>'text_markdown',
                'enableMarkdown'=>true,
            ),
            'ml' => array(
                'class' => 'ext.multilangual.MultilingualBehavior',
                'localizedAttributes' => array(
                    'title',
                    'text',
                    'text_markdown',
                ),
                // ...
                'dynamicLangClass' => false,
            ),
        );
    }
}

В таком случае нужно указать

'dynamicLangClass' => false,

добавить поле l_text_markdown в таблицу переводов и самому вручную создать класс PostLang с таким же поведением, для аналогичных полей:

class PostLang extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
 
    public function tableName()
    {
        return '{{post_lang}}';
    }
 
    public function relations()
    {
        return array('BlogPost' => array(self::BELONGS_TO, 'Post', 'owner_id'));
    }
 
    public function behaviors()
    {
        return array(
            'PurifyText'=>array(
                'class'=>'DPurifyTextBehavior',
                'sourceAttribute'=>'l_text',
                'destinationAttribute'=>'l_text_markdown',
                'enableMarkdown'=>true,
            ),
        );
    }
}

Теперь будет конвертироваться тексты на каждом языке.

В форме редактирования записи в панели управления нужно использовать модернизированную загрузку модели:

$model = Post::model()->multilang()->findByPk($id);

И выводить по несколько полей для каждого языка таже в цикле:

<?php
<?php foreach (Yii::app()->params['translatedLanguages'] as $l => $lang) :
    if($l === Yii::app()->params['defaultLanguage']) 
        $suffix = '';
    else 
        $suffix = '_' . $l;
?>
<fieldset> 
    <div class="row">
        <?php echo $form->labelEx($model, 'title'); ?> <?php echo $lang; ?>
        <?php echo $form->textField($model, 'title' . $suffix, array('size'=>60, 'maxlength'=>255)); ?>
        <?php echo $form->error($model, 'title' . $suffix); ?>
    </div>
</fieldset>
<?php endforeach; ?>

Для языка по умолчанию (то есть в нашем случае для ru) суффикс не нужен. Иначе бы поле title не прошло валидацию required.

Чтобы не копировать из формы в форму данную конструкцию мы как раз и написали тот «лишний» метод suffixList в хэлпере DMultilangHelper. Он как раз возвращает нам такой список суффиксов с именами языков:

<?php
<?php foreach (DMultilangHelper::suffixList() as $suffix => $lang) : ?>
    <div class="row">
        <?php echo $form->labelEx($model, 'title'); ?> <?php echo $lang; ?><br />
        <?php echo $form->textField($model, 'title' . $suffix, array('size'=>60, 'maxlength'=>255)); ?><br />
        <?php echo $form->error($model, 'title' . $suffix); ?>
    </div>
<?php endforeach; ?>

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

UPD: Для Yii2 вышло аналогичное расширение yii2-multilingual-behavior.

Комментарии

 

Денис Наталевич

Спасибо за интересные статьи!
Скажите не планируете ли вы рассказать нам как вы работаете с изображениями? Я имею в виду когда под изображения отводится отдельная таблица, а не когда на картинку одно поле в таблице.

Ответить

 

Дмитрий Елисеев

Про изображения планировал написать. Про отдельную таблицу учту.

Ответить

 

Аурел

Привет! Присматривался к этому поведению, но всё никак не решался.

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

tbl_post - content, title === tbl_post_lang - l_content, l_title при lang = defaultLanguage

Если невозможно отказаться от полей в tbl_post (content, title), то есть ли возможность хотя бы не заносить туда значения?

Ответить

 

Дмитрий Елисеев

Так как поведение записывает языки после сохранения основной записи, то можно добавить обработку afterSave() в модель:

protected function afterSave(){
    parent::afterSave();
    $this->updateByPk($this->primaryKey, array(
        'title'=>'',
        'text'=>'',
    ));
}

Здесь мы даём поведению отработать в parent::afterSave(), и сразу же за ним стираем значения из главной таблицы.

Ответить

 

dramoturg

Мне нравятся ваши статьи. Не переставайте писать

Ответить

 

Дмитрий Елисеев

Спасибо.

Ответить

 

Одиночка Айс – www.daemonhk.kz

Это все конечно хорошо, спасибо автору, даже было сам кинулся делать... Но что если у меня на сайте 20 языков? Даже с 5-7 будет запара сделать как Вы описали, может легче добавить поле lang в каждую запись?

Ответить

 

Дмитрий Елисеев

Можно и так. В обоих случаях число языков может быть любым.

Ответить

 

Ivan Goose

Присматриваюсь к решению, приблизительно такое и надо, не знаю как буду делать, в с yii работаю не долго. Спасибо познавательно.

По поводу возможности реализовать 20 (да хоть 200) языков на сайте, вариант автора более гибкий и правильный, с точки зрения РБД.

Но тут все зависит от задачи, если у вас контент на разных языковых версиях будет разный - тогда и вариант с полем lang подойдет в таблице постов, и будет проще.

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

Ответить

 

script

Дмитрий, спасибо за такие качественные статьи.
С помощью ваших статей я смог разобраться более глубоко в Yii.
Не могли бы вы мне подсказать.
Можно ли динамически определить язык по умолчанию для данного расшиерення.
Например есть у меня дополнительное поле language

class Post extends CActiveRecord
{  
    public $language = 'ru';
    // ...
    public function behaviors()
    {
        return array(
            'ml' => array(
                'class' => 'ext.multilangual.MultilingualBehavior',
                'localizedAttributes' => array(
                    'title',
                    'text',
                ),
                'langClassName' => 'PostLang',
                'langTableName' => '{{post_lang}}',
                'languages' => Yii::app()->params['translatedLanguages'],
                'defaultLanguage' => $this->language,
                'langForeignKey' => 'owner_id',
                'dynamicLangClass' => true,
            ),
        ));
    }

Но так не работает если я изменяю динамически свойство $language, например при добавлении записи. Что посоветуете для динамического изминения языка?

Ответить

 

Дмитрий Елисеев

Посоветую переподключить поведение динамически сразу после того, как сменили язык. Например в beforeSave:

protected function beforeSave() {
    if (parent::beforeSave()) {
        $this->detachBehavior('ml');
        $this->attachBehavior('ml', array(
            ...
            'defaultLanguage' => $this->language,
            ...
        ));
        return true;
    }
    return false;
}
Ответить

 

script

Спасибо. Работает

Ответить

 

Дима

Кстате поведение старое и выдает ошибки, готовьтесь их исправлять. Предупрежден - значит вооружен.

Ответить

 

Александр shaggy – avm.dp.ua

Дмитрий, скажите, а нельзя-ли применить для поддержки мультиязычности "группы условий по умолчанию"?
Может в этой методике есть какие-то "подводные камни", которых я пока не вижу?

class Content extends CActiveRecord
{
    public function defaultScope()
    {
        return array(
            'condition'=>"language='".Yii::app()->language."'",
        );
    }
}


Методика описана тут:
http://www.yiiframework.com/doc/guide/1.1/ru/database.ar
там в самом низу, буквально пол страницы.
уж больно хочется как-то попроще :)

Ответить

 

Дмитрий Елисеев

Можно. Как здесь. Но тогда надо будет обдумать одновременное использование общих изображений и других общих полей. Да и ID записей для разных языков будут разными, что затруднит создание переключения языка.

Ответить

 

script

Добрый день.
Не подскажите как правильно сделать выборку данных в DataProvider для конкретного языка.
То есть я хочу получить только те записи которые соответствуют выбраному языку а далее передать dataprovider в CListView.

Ответить

 

Дмитрий Елисеев

Поведение подключает новое отношение. Можно обращаться через него прямо к языковой таблице:

$criteria->addCondition('PostLang.l_text <> "" and PostostLang.lang_id = :lang');
$criteria->params['lang'] = $lang;
Ответить

 

MrArthur

Очень полезные статьи, спасибо!

Если бы еще избавиться полностью от переводимых полей в основной таблице, было бы вообще замечательно...

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

Ответить

 

Дмитрий Елисеев

Тогда проще как во втором случае добавить по два поля title_ru, title_en, text_ru и text_en и геттеры getTitle и getText.

Ответить

 

MrArthur

А CDbMessageSource не подходит для этих целей? Что-то я так и не понял как он работает...

Варинт с title_ru, title_en - как-то не по душе... во первых таблицы будут большие, во вторых, подумал еще и понял, что все таки хочется расширяемости.. это такое дело, сегодня 2 языка надо, а завтра еще 3 надо будет добавлять:)

Ответить

 

Arthur Mr

Еще у меня сейчас в голове крутится такой вариант: сделать так же как с переводом элементов интерфейса, т.е. выбрать sourceLanguage для контента в бд, и его хранить в основной таблице. Для остальных языков использовать вторую таблицу с переводами. Например, если задали основной язык en а дополнительный ru, то при использовании английской версии сайта - вообще не делать запрос во вторую таблицу... Сложно такое реализовать?

Ответить

 

Дмитрий Елисеев

Можно любым вариантом, лишь бы в if-ах не заблудиться.

Ответить

 

Валерия

Здравствуйте!

Для сайта решила использовать подход с использованием MultilingualBehavior. Однако сразу же столкнулась с проблемой:

"Таблица "{{newsLang}}", упомянутая в записи active record класса "NewsLang", не найдена в базе данных.
/framework/db/ar/CActiveRecord.php(2312)"

В $tableName попадает news{{newsLang}}

Почему так происходит?

Ответить

 

Валерия

Может ли быть проблемой то, что класс Post наследуется не от CActiveRecord, а от другого класса, который наследует CActiveRecord, должен ли в таком случае класс PostLang наследоваться от того же класса, что и основная модель?

Ответить

 

Дмитрий Елисеев

В параметрах поведения при подключении укажите:

'langTableName' => '{{news_lang}}',

и создайте эту таблицу

Ответить

 

Валерия

Я назвала таблицу newsLang и она подключена в поведении.
Проблема решилась когда я создала свой класс для NewsLang, который наследует мой переопределенный класс CActiveRecord. Правда, еще не добилась корректного сохранения данные. Но, видимо, это тоже нужно подстраивать под мой класс...

Ответить

 

Vit And

У меня была такая же ошибка как у Валерии.
Полечилось, когда вместо

'langTableName' => '{{postlang}}'

написал:

'langTableName' => 'postlang'

То есть без фигурных скобок.

Хотя tableName со скобками (я использую префикс)

public function tableName()
{
	return '{{post}}';
}

Почему такое происходит с названием таблицы?

Ответить

 

Дмитрий Елисеев

Можно заглянуть в код поведения:

public function createLangClass() {
    if(!class_exists($this->langClassName, false)) {
        $owner_classname = get_class($this->getOwner());
        eval("class {$this->langClassName} extends CActiveRecord
        {
            public static function model(\$className=__CLASS__)
            {
                return parent::model(\$className);
            }

            public function tableName()
            {
                return '{{{$this->langTableName}}}';
            }

            ...
        }");
    }
}

Видим, что оно само добавляет {{ и }}.

Ответить

 

Vit And

Спасибо!
Значит можно смело писать без скобок: 'langTableName' => 'post_lang'

Ответить

 

Валерия

Прошу прощения, не могли бы Вы объяснить как происходит валидация при хранении переводов в одной таблице?

В методе поведения attach добавляются правила валидации для полей-переводов.Однако, если прописываешь в правилах не required, а, допустим unique, то появляются ошибки. Он проверяет эти поля как поля модели и не находит их в бд.

Возможно ли вообще применение других правил валидации к полям основной таблицы и полям перевода в рамках поведения MultilingualBehavior?

Ответить

 

Дмитрий Елисеев

Можно для unique попробовать указать нужный className. Или вписать правила в метод rules() самой модели PostLang.

Ответить

 

Дмитрий Елисеев

А для одной таблицы можно явно указать правила для text, text_ru, text_en и text_de, как там и указано:

array('text_ru, text_en, text_de', 'required'),
Ответить

 

Михал

Здравствуйте!

Большое спасибо за ваш труд!
Решил использовать MultilingualBehavior для своего сайта но столкнулся с проблемой.
У меня 3 языка en,ru,fr
таблицы Post и PostLang
Вопрос как сделать поиск по поле title
Пробовал

public function actionTest(){
    $title = $_POST['title'];
    $lang = Yii::app()->language; // ru

    if($lang != Yii::app()->params['defaultLanguage'])
        $model = Post::model()->multilang()->findByAttributes(array('title'.'_'.$lang=>$title));
    else
        $model = Post::model()->multilang()->findByAttributes(array('title'=>$title));
    var_dump($model);
}

но выдает ошибку если язык не равен языку по умолчанию, говорит что title_ru не принадлежит Post

Подскажите пожалуйста как это реализовать, какие параметры надо использовать а лучше всего пример поиска по полей-переводов

Ответить

 

Дмитрий Елисеев

Можно попробовать по аналогии с кодом метода search():

$criteria = new CDbCriteria();
$criteria->addCondition('title = :title');
$criteria->params['title'] = $title;
$criteria = Post::model()->ml->modifySearchCriteria($criteria);
$model = Post::model()->find($criteria);
Ответить

 

Михал

Спасибо большое за быстрый ответ!

Чуть покопавшись в код Multilangualbehavior-a и после долгих дебагов нашел в нем ошибочку, так как я использую Postgresql базу данных изменил строку 259 на следующую (просто изменил обычные ковычки на двойные):

$owner->getMetaData()->relations[$this->localizedRelation] = new $class($this->localizedRelation, $this->langClassName, $this->langForeignKey, array('on' => '"'.$this->localizedRelation . '"."' . $this->langField .'"=\''. $lang . "'", 'index' => $this->langField));

и в контролере использовал следующий код

$title = $_POST['title'];
$criteria = new CDbCriteria();
$criteria->compare('"i18nPost"."l_title"',$title);
$model = Post::model()->multilang()->find($criteria);

все работает прекрасно.

Спасибо за подсказку с CDbCriteria.

Ответить

 

Дмитрий Ермола

Доброго времени суток.
Здесь уже поднималась тема о том что неплохо бы избежать дублирования в таблице переводов с основной таблицей. Мне наиболее простым и удачным кажется вариант когда мы просто не делаем перевод для основного языка. Что бы добиться этого достаточно в параметре "languages" при инициализации behavior-a исключить "defaultLanguage". Можно сделать базовый класс для всех наших моделей и в нем определить метод который будет возвращать нам правильный список языков

BaseModel extends CActiveRecord
{

    	protected function getTranslatedLanguages()
	{
		$arr = Yii::app()->params['translatedLanguages'];
		unset($arr[Yii::app()->params['defaultLanguage']]);
		return $arr;
	}
}

далее наша модель

Post extends BaseModel
{
    public function behaviors()
    {
        return array(
            'ml' => array(
                'class' => 'application.models.behaviors.MultilingualBehavior',
                'localizedAttributes' => array('text'),
                'langClassName' => 'PostLang',
                'langTableName' => '{{post_lang}}',
                'languages' => $this->getTranslatedLanguages(),
                'defaultLanguage' => Yii::app()->params['defaultLanguage'],
                'langForeignKey' => 'post_id',
                'dynamicLangClass' => true,
            ),
        );
    }
}

вот и все :) ... поверхностно протестировал, все работает, если в чем то не прав, пожалуйста укажите.

Ответить

 

Дмитрий Елисеев

Достойное решение. Спасибо!

Ответить

 

vovik

у меня при создание таблицы выдает ошибку
#1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'CONSTRAINT `category_lang_owner` FOREIGN KEY (`owner_id`) REFERENCES `category` ' at line 10

даже если просто скопировать, из текста, тоже ошибка

Ответить

 

Дмитрий Елисеев

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

Ответить

 

Артем

Дмитрий, спасибо за статью.

Возник вопрос, если у статьи(или любого другого параметра) нет перевода, то при смене языка будет выводиться оригинальный текст, т.е. меняем язык например на английский, если нет перевода видим русский вариант.
Хотелось бы, чтобы контент в таком случае совсем не выводился.

Выше Вы писали пример решения для конкретной модели:

$criteria->addCondition('PostLang.l_text <> "" and PostostLang.lang_id = :lang');
$criteria->params['lang'] = $lang;

Может есть что-то более универсальное, чтобы не прописывать каждый раз доп условие.

Ответить

 

Дмитрий Елисеев

Предполагаю, что это можно добавить в defaultScope().

Ответить

 

vovik

У вас в каждом коде куча ошибок, пишите на коленке? ни разу еще не завелось с первого раза

Ответить

 

Дмитрий Елисеев

Да, именно так. Кое-что копирую из работающих и проверенных скриптов, а остальное пишу из головы, с лёту и не проверяя. Потом читатели находят ошибки и опечатки и указывают в комментариях, где что не так и как это подправить. Остальное не исправляю.

Спасибо за замечание!

Ответить

 

Дмитрий Ермола

Наткнулся на довольно неожиданную ошибку.
Update - записи работает как часы, но при попытке создать новую запись на этапе валидации выскакивает ошибка: "Не определено свойство "Post.name_en". Вернее эта ошибка выскакивает каждый раз когда у модели созданной через "new" метод "__get()" пытается получить свойство с переводом (name_en).
Подскажите пожалуйста что я упустил.

Ответить

 

Дмитрий Ермола

Решение оказалось таким же неожиданным как и сама ошибка :)
Дело в сценариях, для создания новой записи я в модели "Post" определил сценарий "create".
и новую модель создавал с этим сценарием "$model = Post("create");"
Так вот у "MultilingualBehavior" сценарий по умолчанию "insert", если сценарии бихевора и модели отличаются бихевор работать не будет.
Решается просто: в конфигах добавьте параметр "createScenario" и установите в нужное вам значение.

Post extends BaseModel
{
    public function behaviors()
    {
        return array(
            'ml' => array(
                'class' => 'application.models.behaviors.MultilingualBehavior',
                'localizedAttributes' => array('text'),
                'langClassName' => 'PostLang',
                'langTableName' => '{{post_lang}}',
                'languages' => $this->getTranslatedLanguages(),
                'defaultLanguage' => Yii::app()->params['defaultLanguage'],
                'createScenario' => 'create',
                'langForeignKey' => 'post_id',
                'dynamicLangClass' => true,
            ),
        );
    }
}
Ответить

 

Oleg Shadrin

Дмитрий, спасибо за интересную и познавательную статью.

Дмитрий, присоединяюсь к просьбе написанной ранее в этих комментах, если у вас будет желание и время, напишите статью о работе с изображениями.

Ответить

 

Андрей – thekilo.ru

Автор, в сёрче у модели внутри массива убери точку с запятой, пожалуйста.

array(
    'criteria' => $this->ml->modifySearchCriteria($criteria);
)
Ответить

 

Андрей – thekilo.ru

Так же в behaviors() лишняя скобка.
А, вообще, совет: проверь код чем-нибудь с проверкой php-синтаксиса, т.к. некоторые малоопытные кодеры пишут код в блокноте и без подсветки (сам таким был). Получается, что не проверив синтаксис и доверяясь сенсеям, у новичков сыпятся баги. Они плоюют на твой урок (или на Yii или даже на PHP) и идут качать вордпрессы :)

Ответить

 

Дмитрий Елисеев

Спасибо! Исправил.

Они плоюют на твой урок (или на Yii или даже на PHP) и идут качать вордпрессы :)

Но, согласитесь, в этом есть и здравое зерно, раз уж такого программиста способна загнать в ступор лишняя запятая и он не способен расшифровать текст ошибки. Своеобразный естественный отбор :)

Ответить

 

Андрей – thekilo.ru

Отчасти согласен. Но, если быть чуточку дальновиднее, то положившись на естественный отбор и не попытавшись взрастить качественного кодера, мы получаем оголтелую толпу «вебмастеров» с корявыми «вордпрессами». И с этими поделками, через какое-то время, к нам идут наши заказчики. И просят таки их дорабатывать! А мы не можем ему отказать - заказчик постоянный и у тебя на поддержке не один его сайт :)

Ответить

 

Александр

Дмитрий, подскажите, а как быть с теми записями для которых нет перевода в первом способе? У меня они выводятся с пустыми значениями, а хотелось бы что бы если отсутствует перевод - выводить на языке по умолчанию (например на русском).

Ответить

 

Дмитрий Елисеев
public function getTitle()
{
     $current = 'title_' . Yii::app()->getLanguage();
     $default = 'title_ru';
     return !empty($this->{$current}) ? $this->{$current} : $this->{$default};
}
Ответить

 

Александр

Спасибо огромное! Может есть смысл это решение указать в Вашем первом способе, дабы такие как я подобных вопросов не задавали?

Ответить

 

Дмитрий Елисеев

Дополнил. Спасибо!

Ответить

 

Андрей

возникла ошибка "Таблица ... .упомянутая в записи active record класса не найдена в базе данных"
Таблицы в базе были без префиксов. Нужно было в конфиге добавить 'tablePrefix' => '',

Ответить

 

Виталий

Дмитрий, здравствуйте! Снова нуждаюсь вашей помощи. Я пишу проект на Yii2.

Есть две таблицы "Название меню" и "Пункты меню" связаны один ко многим. Форму генерировал с помощью CRUD. В папке view есть файл _form.php

<?php $form = ActiveForm::begin(); ?>
    <?= $form->field($model, 'name_menu')->textInput(['maxlength' => true]) ?>
    <?= $form->field($model, 'name')->textInput();?>
    <div class="form-group">
        <?= Html::submitButton('Update', ['class' => 'btn btn-primary']) ?>
    </div>
<?php ActiveForm::end(); ?>

Поле: <?= $form->field($model, 'name_menu')->textInput(['maxlength' => true]) ?> с первой таблицы,
а поле: <?= $form->field($model, 'name')->textInput();?> уже со второй таблицы, дописал сам. Всё хорошо выводится, редактируется.

Проблема в том, как вывести все значения "$model, 'name'" со второй таблицы (которая связана с первичной связью один ко многим)?

Одно меню может иметь несколько пунктов. Как вывести для редактирования все пункты меню?

Ответить

 

Михаил

Здравствуйте Дмитрий! Не могли бы вы показать пример по пойску в таблице переводов в
Yii2-multilingual-behavior.

Ести таблицы:

    category
       id,image,sort_order
    
    category_lang
      category_id,language,name,slug,content

Вопрос как оргонизовать поиск по атрибуту "slug" используя Yii2-multilingual-behavior

Заранее спосибо!

Ответить

 

serhio

Добрый день.

Подскажите плиз. Как настроить валидацию для полей с переводами?

Добавление правил валидации в модель переводов результата не дает.

Ответить

 

serhio

Добрый день.

Валидация полей в таблице переводов. Может я до конца не разобрался, но после выполнения всех инструкций поля из таблице переводов не валидировались. Поэтому было сделано следующее.

Наследуемся от MultilingualBehavior, переопределяем метод afterSave и добавляем метод afterValidate

class MultilangBehavior extends MultilingualBehavior
{
    
    protected $owners = null;

    public function afterValidate($event)
    {
        parent::afterValidate($event);

        $main_owner = $this->getOwner();
        $ownerPk = $main_owner->getPrimaryKey();
        $rs = array();
        if (!$main_owner->isNewRecord) {
            $model = call_user_func(array($this->langClassName, 'model'));
            $c = new CdbCriteria();
            $c->condition = "{$this->langForeignKey}=:id";
            $c->params = array('id' => $ownerPk);
            $c->index = $this->langField;
            $rs = $model->findAll($c);
        }
        foreach ($this->languages as $lang) {
            $defaultLanguage = $lang == $this->defaultLanguage;
            if (!isset($rs[$lang])) {
                $owner = new $this->langClassName;
                $owner->{$this->langField} = $lang;
                $owner->{$this->langForeignKey} = $ownerPk;
            } else {
                $owner = $rs[$lang];
            }
            foreach ($this->localizedAttributes as $field) {
                if ($defaultLanguage) {
                    $value = $main_owner->$field;
                } else {
                    $value = $this->getLangAttribute($field . '_' . $lang);
                }
                if ($value !== null) {
                    $langfield = $this->localizedPrefix . $field;
                    $owner->$langfield = $value;
                }
            }
            $this->owners[] = $owner;
            if (!$owner->validate()){
                $main_owner->addErrors($owner->getErrors());
            }
        }
    }

    public function afterSave($event)
    {
        $main_owner = $this->getOwner();
        $ownerPk = $main_owner->getPrimaryKey();
        foreach ($this->owners as $owner) {
            $owner->{$this->langForeignKey} = $ownerPk;
            $owner->save(false);
        }
    }

}

настраиваем модели

class BedCore extends Bed
{

.....
    
    public function behaviors()
    {
        return array(
            'ml' => array(
                'class' => 'core.components.multilang.MultilangBehavior',
                'localizedAttributes' => array(
                    'type',
                ),
                'langClassName' => 'BedLang',
                'langTableName' => 'bed_lang',
                'languages' => Yii::app()->params['translatedLanguages'],
                'defaultLanguage' => Yii::app()->params['defaultLanguage'],
                'langForeignKey' => 'parent_id',
                'dynamicLangClass' => false,
            ),
        );
    }

.....

}
class BedLang extends CActiveRecord
{
    
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
 
    public function tableName()
    {
        return '{{bed_lang}}';
    }
 
    public function relations()
    {
        return array('bed' => array(self::BELONGS_TO, 'BedCore', 'parent_id'));
    }
    
    public function rules()
    {
        return array(
            array('l_type', 'required', 'message'=>'l_type '.$this->lang_id.' cannot be blank'),
            array('l_type', 'length', 'max' => 255),
        );
    }
    
}

Теперь все валидируеться с указанием языка поля.

P.S.
Если есть другое решение этой задачи буду рад ознакомиться.

Ответить

 

Станислав

Спасибо за статью, отлично расписано.
Но вот у меня такая проблема как автоматизировать вывод в CDetailView

$this->widget('zii.widgets.CDetailView', array(
    'data'=>$model,
    'attributes'=>array(
        'id',
        'title',
    ),
));

получается тут title выводится по умолчанию языка, а возможно ли что бы оно выводило к примеру и title_ru и title_en без явного указания их в attributes?

Ответить

 

Дмитрий Елисеев

Вроде нельзя. Заполните attributes также циклом.

Ответить

Оставить комментарий

Войти | Завести аккаунт | Войти через


(никто не увидит)





Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>