Одним из наиболее удачных решений для создания мультиязычного контента в 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'
],
]) ?>
На этом первую часть реализации словаря считаю законченной.
А можно ссылку на готовый проект, пожалуйста?
Проект приватный, репозитория не будет.