Мультиязычный сайт на 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;
    }
}

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

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

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

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

<?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 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 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.

Другие статьи

Недавно мы познакомились с использованием HTMLPurifier. Этот компонент позволяет отфильтровать вредные элементы из HTML кода, обработать ссылки, закрыть незакрытые теги. Большой список возможностей позволяет использовать его для фильтрации полученного от пользователя контента. Вместо использования BBCode мы попробуем доработать HTMLPurifier для удобной работы тега <pre> в комментариях пользователей.

Большинство рецептов по Yii на этом сайте так или иначе сводятся к написанию поведения и подключению его к своему проекту. В обсуждении статьи о шаблонизаторе для вывода виджетов появился комментарий с просьбой подробно осветить работу с поведениями и, собственно, объяснить, для чего же они на самом деле нужны и почему их любят использовать некоторые продвинутые разработчики.

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

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

Комментарии

 

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

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

Ответить

 

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

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

Ответить

 

Аурел

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

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

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

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

Ответить

 

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

Спасибо.

Ответить

 

Одиночка Айс

Это все конечно хорошо, спасибо автору, даже было сам кинулся делать... Но что если у меня на сайте 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

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

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

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

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

Ответить

 

Андрей

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

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

 

Андрей

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

Ответить

 

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

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

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

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

Ответить

 

Андрей

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

Ответить

 

Александр

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

Ответить

 

Дмитрий Елисеев
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>