Одним из наиболее удачных решений для создания мультиязычного контента в Yii2 для меня стало поведение yii2-multilingual-behavior от автора OmgDef. Предлагаю ознакомиться с примером реализации.
TODO: когда-то, возможно, тут появится кусок реализации переключателя языка. В сети есть.
Нужен словарь, куда администратор сайта будет добавлять, собственно, слова, а также их словоформы. Затем мы будем вытаскивать по нужной словоформе слово в базовой форме и давать его перевод, а также позволим пользователям добавлять его в свой собственный словарь для дальнейшего изучения.
Прежде чем приступить к реализации, собственно, словаря, нам необходимо дать админу сайта возможность указывать для слова часть речи (существительное, прилагательное, глагол и т.д.). К слову, части речи также необходимо переводить на язык пользователя. Займемся этим.
Приступая к реализации, установим расширение Yii2 multilingual behavior через Composer:
composer require --prefer-dist omgdef/yii2-multilingual-behavior
Подготовим миграции. В основной таблице у нас будет всего два поля — id, на который в дальнейшем будут ссылаться слова из нашего словаря, и name — поле, по которому будет ориентироваться админ сайта.
public function safeUp() { /* Добавим этот кусок, чтобы точно быть увереными, что наша таблица будет нормально кушать юникод*/ $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } /* Создание таблицы с нужными полями */ $this->createTable('parts_of_speech', [ 'id' => $this->primaryKey(), 'name' => $this->string(32), ], $tableOptions); }
Применим миграцию, а затем создадим еще одну — для транслитерации наших данных. Пример ниже — переработка кода из документации расширения.
public function safeUp() { $sql = "CREATE TABLE IF NOT EXISTS `parts_of_speech_translate` ( `id` int(11) NOT NULL AUTO_INCREMENT, `parts_of_speech_id` int(11) NOT NULL, // ID части речи, на который будет ссылаться данная запись `language` varchar(6) NOT NULL, // Код языка `title` varchar(255) NOT NULL, // Первое поле, которое мы будем показывать пользователям как название части речи на родном для них языке `content` TEXT NOT NULL, // Возможно, в дальнейшем потребуется давать пользователям какую-то информацию о части речи. Попробуем это предусмотреть и создадим заготовку описания PRIMARY KEY (`id`), KEY `parts_of_speech_id` (`parts_of_speech_id`), KEY `language` (`language`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ALTER TABLE `parts_of_speech_translate` // Добавим внешний ключ для связности данных, укажем к какой таблице ADD CONSTRAINT `parts_of_speech_translate_ibfk_1` // какой ключ добавить FOREIGN KEY (`parts_of_speech_id`) // укажем имя поля в текущей таблице REFERENCES `parts_of_speech` (`id`) // с какой таблицей и каким полем связать ON DELETE CASCADE ON UPDATE CASCADE;"; // ну и как связывать $this->execute($sql); // затем выполним всю эту конструкцию, раз уж мы поленились перевести все это в ООП }
Создадим модель PartsOfSpeech при помощи GII. Поскольку она будет использоваться как в админке, так и на фронтэнде, положим ее в common/models.
Настроим модель согласно документации Yii2 multilingual behavior. Добавим в созданную модель следующий код:
#class PartsOfSpeech public function behaviors() { return [ 'ml' => [ 'class' => MultilingualBehavior::className(), 'languages' => [ 'ru' => 'Russian', // По факту, сервис разрабатывается как раз для русскоязычной и англоязычной аудитории 'en-US' => 'English', ], /* Закомментированные строки ниже можно удалить */ //'languageField' => 'language', // Т.к. мы не меняли поле языка (не нужно в большинстве случаев), это значение не нужно переопределять //'localizedPrefix' => '', // Префикс не используем //'requireTranslations' => false', // Не будем делать перевод обязательным //'dynamicLangClass' => true', // Надо почитать, что это такое =) //'langClassName' => PostLang::className(), // Не переопределяем основной класс 'defaultLanguage' => 'ru', // Укажем основной язык приложения 'tableName' => "{{%parts_of_speech_translate}}", // В какой таблице лежат наши переводы 'langForeignKey' => 'parts_of_speech_id', // Укажем, какое поле в таблице parts_of_speech_translate отвечает за связь с основной таблицей, в модели которой мы сейчас находимся 'attributes' => [ 'title', 'content', // Передаем массив переводимых полей ] ], ]; }
Также переопределим метод поиска:
#class PartsOfSpeech public static function find() { return new MultilingualQuery(get_called_class()); }
И, чтобы все действительно работало (у меня по документации завелось не все сразу), явно укажем используемые куски:
#class PartsOfSpeech use omgdef\multilingual\MultilingualTrait; use omgdef\multilingual\MultilingualQuery; use omgdef\multilingual\MultilingualBehavior;
Вот за что я люблю Yii2, так это за его способность быстро написать за тебя сотню-другую строк кода. Сгенерим CRUD для нашей модельки.
И перейдем по ссылке http://admin.<domain>/parts-of-speech.
Сейчас, используя экшн create, мы видим только одно поле — Name, которое есть в нашей основной таблице (ID генератор фреймворка разумно скрыл). Чтобы добавлять также и переводы, отредактируем форму /backend/views/parts-of-speech/_form.php
Добавим поля для сохранения переводов в вид:
<div class="row"> <div class="col-sm-6"> <h3>Перевод на русский</h3> <?= $form->field($model, 'title')->textInput(['maxlength' => 255]) ?> <?= $form->field($model, 'content')->textarea() ?> </div> <div class="col-sm-6"> <h3>English version</h3> <?= $form->field($model, 'title_en')->textInput(['maxlength' => 255]) ?> <?= $form->field($model, 'content_en')->textarea() ?> </div> </div>
После этого у нас должна получиться вот такая симпатичная верстка:
И она уже полноценно добавляет все нужные данные прямо в нашу базу. Почти.
Не забываем, что встроенный валидатор Yii2 не пропускает неизвестные данные в нашу модель, и — значит — в БД они не оказываются. Поэтому добавим валидатор safe (конечно, в продакшн так выпускать не стоит, но для примера — вполне подойдет, да и будет тема для еще одного поста).
public function rules() { return [ [['name'], 'string', 'max' => 32], [['title', 'title_en', 'content', 'content_en'], 'safe'], ]; }
А вот теперь все пишется в БД.
А вот выводится при редактировании — не все.
Чтобы корректно редактировать мультиязычные модели, в контроллере нужно изменить метод findModel($id).
protected function findModel($id) { if (($model = PartsOfSpeech::find()->where(['id' => $id])->multilingual()->one()) !== null) { return $model; } throw new NotFoundHttpException('The requested page does not exist.'); }
Самое приятное в использовании данного расширения — что нам не нужно писать тонны кода в виде, определяя, какой язык указан у пользователя. Выводится поле без каких-либо языковых префиксов:
<?php echo $model->title ?>
В данной переменной будет оказываться контент уже на языке пользователя. Но если вам по каким-то причинам надо вывести контент с конкретной локалью, можно воспользоваться нехитрым вызовом, который проигнорирует настройки языка пользователя:
<?php echo $model->title_en ?>
Описывать перевод CRUDа и т.д. тут я не вижу смысла — справитесь. А вот добавить на страницу просмотра записи недостающие поля — пожалуй, стоит. Впрочем, тут все еще проще:
<?= DetailView::widget([ 'model' => $model, 'attributes' => [ 'id', 'name', /* Добавим отсутствующие поля */ 'title', 'content', 'title_en', 'content_en' ], ]) ?>
На этом первую часть реализации словаря считаю законченной.
А можно ссылку на готовый проект, пожалуйста?
Проект приватный, репозитория не будет.