Multilingual Engine — небольшой плагин, который добавляет к сайту на WordPress языковые версии статей через префикс в URL (/en/, /de/) и не тянет за собой ни одной сторонней библиотеки. Язык каждой записи хранится в служебной таксономии, а не в метаполе, поэтому выборки остаются быстрыми даже на тысячах записей. Ниже — как это устроено внутри, как включить за десять минут и на каких местах спотыкаются чаще всего.
Материал написан по исходникам плагина версии 1.09.5 (файл MultilingualEngine.php), а не по памяти: всё, что описано, можно проверить в коде. Если вы ждёте «просто ещё один переводчик интерфейса» — это не он: здесь переводятся материалы, а не строки темы.
Важно: плагин не из каталога WordPress.org (в шапке файла
Plugin URIуказывает наexample.com), ставится копированием каталога вwp-content/plugins/. Это ваш собственный код: обновлений «из коробки» не будет, обновлять нужно вручную.
TL;DR: включить за десять минут
- Скопируйте каталог
multilingual-engineвwp-content/plugins/и активируйте плагин. - Откройте в админке отдельное меню Multi Lang (иконка перевода,
admin.php?page=multilang-settings) — не «Настройки → Общие». - Задайте
ml_available_languages(напримерru,en,de) иml_default_language(напримерru). - Откройте главную: плагин добавит префикс языка и поставит куку
ml_lang. - Вставьте переключатель: шорткод
[language_switcher]или виджет Language Switcher.
Проверка за минуту:
# список языков и язык по умолчанию
wp option get ml_available_languages
wp option get ml_default_language
# заголовки: должен быть 302 с /about/ на /ru/about/ (если перевод существует)
curl -I https://example.com/about/
Ожидаемо: 302 и Location: https://example.com/ru/about/. Если редиректа нет — проверьте, что запись действительно существует по целевому адресу (плагин намеренно не перенаправляет на несуществующие URL).
Оглавление
- Что решает плагин и когда он уместен
- Как это устроено внутри
- Установка и требования
- Настройки: меню Multi Lang
- Определение языка: реальный порядок приоритетов
- URL, редиректы и канонические адреса
- Контент: оригинал, переводы и черновики
- REST API: метаполя и внешние сценарии
- Переключатель языка: шорткод, виджет, PHP
- SEO: hreflang и что он не делает
- Обслуживание через WP-CLI
- Подводные камни
- Показательные случаи
- Чек-лист после внедрения
- FAQ
- Итог
Что решает плагин и когда он уместен
Задача типовая: сайт на WordPress, материалы нужны на двух-трёх языках, а тяжеловесные коммерческие решения не хочется ставить по трём причинам — лицензия, вес, вмешательство в структуру URL и таблицы.
Multilingual Engine решает узкий круг задач:
| Задача | Решает | Комментарий |
|---|---|---|
| Языковые версии записей и страниц в одной установке | Да | Отдельная запись на язык, общая группа переводов |
Языковой префикс в URL (/en/post-slug/) |
Да | Редирект 302 на префикс, если его нет |
| Быстрая выборка по языку | Да | Таксономия ml_language, а не метазапросы |
hreflang в <head> |
Да | Ссылки на альтернативные версии |
| REST-мета для внешних сценариев | Да | _ml_language, _ml_group_id |
| Перевод интерфейса темы и плагинов (строки) | Нет | Это задача load_plugin_textdomain и .po/.mo |
| Мультисайт (по сайту на язык) | Нет | Плагин рассчитан на одиночную установку |
Выбор между этим плагином и Polylang/WPML — вопрос не «лучше/хуже», а «кто владеет структурой данных». Здесь структура простая и полностью ваша: две мета-записи, одна таксономия, никаких переопределений запросов в ядре.
Как это устроено внутри
Четыре сущности, вокруг которых крутится всё остальное:
| Сущность | Имя в коде | Назначение |
|---|---|---|
| Таксономия языка | ml_language |
Привязка записи к языку; непубличная (public = false, show_ui = false, show_in_rest = false), только для post и page |
| Мета «группа переводов» | _ml_group_id |
Общий идентификатор всех версий одной статьи, например mlg_64b034b4b7e44.18830524 |
| Мета «язык» | _ml_language |
Код языка записи, доступен в REST (show_in_rest = true) |
| Кука | ml_lang |
Последний выбранный язык: 1 год, path=/, SameSite=Lax, secure при HTTPS, HttpOnly — нет |
Таксономия создаётся не «при активации», а на init — вместе с регистрацией метаполей. Схему базы плагин не меняет: таксономия и мета живут в стандартных таблицах WordPress.
Почему таксономия, а не мета: выборка по термину идёт по индексированной связи term_relationships, тогда как meta_query на больших объёмах превращается в JOIN по postmeta и заметно тормозит. Это ровно тот случай, когда «ещё одна таксономия» экономит секунды на листингах.
Установка и требования
| Компонент | Минимум | Рекомендовано |
|---|---|---|
| WordPress | 6.8 | актуальная LTS |
| PHP | 8.3 | 8.3.x |
| Веб-сервер | Apache/Nginx | текущий стабильный |
| База данных | MySQL 5.7 / MariaDB 10.3 | новые версии InnoDB |
Установка:
cd /var/www/example.com/wp-content/plugins
unzip multilingual-engine.zip
chown -R www-data:www-data multilingual-engine
wp plugin activate multilingual-engine
Права важны: если каталог принадлежит root, автообновления и правки через админку работать не будут, а часть хостингов отдаст 500 при попытке записи в лог.
Настройки: меню Multi Lang
Плагин добавляет отдельный пункт меню в админке — Multi Lang с иконкой dashicons-translation. Настройки лежат на странице admin.php?page=multilang-settings, а не в «Настройки → Общие» (это частая ошибка при переносе инструкций со старых версий).
| Опция | Что задаёт | Значение по умолчанию |
|---|---|---|
ml_available_languages |
Список кодов ISO через запятую | en |
ml_default_language |
Язык-фолбэк и язык интерфейса для гостей | первый из списка |
ml_auto_create_drafts |
Создавать пустые черновики переводов при сохранении записи | включено (1) |
ml_redirect_missing |
Редирект на язык по умолчанию, если перевода нет | выключено (0) |
ml_copy_home |
Копировать главную страницу для языков | выключено (0) |
ml_user_passwords |
Пароли служебных учётных записей языков | генерируются |
Список языков нормализуется: обрезка пробелов, нижний регистр, удаление дублей — то есть RU, En , ru превратится в ru,en без ругани в интерфейсе.
Отдельный блок на странице — массовое назначение языка для старого контента и пользователи языков. Второе нужно редко, но объясняет странность, которую легко принять за баг: плагин создаёт учётные записи вида lang_ru, lang_en. Ник такой учётки участвует в определении языка (см. ниже), а пароли складываются в ml_user_passwords.
Определение языка: реальный порядок приоритетов
Порядок в коде currentLang() — важная деталь, потому что в старых описаниях он перепутан.
| Приоритет | Источник | Условие | Пример |
|---|---|---|---|
| 1 | Ник авторизованного пользователя | Ник вида lang_xx |
lang_de → немецкая версия |
| 2 | Префикс URL | Первый сегмент пути совпадает с кодом языка | /de/about/ |
| 3 | Кука ml_lang |
Выбрана ранее и входит в список | ml_lang=fr |
| 4 | Язык по умолчанию | Фолбэк | ru |
Два следствия, которые ломают ожидания:
- Заголовок
Accept-Languageне учитывается. Автоматического «угадывания» по браузеру нет: гость с немецким браузером увидит язык по умолчанию, пока не переключит вручную. Для сайта с аудиторией из разных стран это надо помнить. - Кука сильнее отсутствия префикса. Если пользователь однажды выбрал
de, а потом открыл «чистый» URL, плагин предпочтёт куку — и уведёт на/de/.
Кука ставится только при редиректе и живёт год. HttpOnly не выставлен, поэтому значение доступно скриптам на странице — если это важно, проще не хранить в ней ничего, кроме кода языка (так и есть).
URL, редиректы и канонические адреса
Логика maybeRedirect() намеренно осторожная:
- Админка, AJAX, ленты и предпросмотр не трогаются.
- Если первый сегмент пути уже равен текущему языку — редиректа нет.
- Иначе строится целевой адрес
/{lang}/{путь}и проверяется его существование черезurl_to_postid(). - Если целевой адрес не существует и это не корень языка — редирект не выполняется. Так исключаются петли и «прыжки» на 404.
Корень языка (/en/ без пути) обрабатывается отдельно: для него есть обход канонического редиректа (bypassCanonicalForLangRoot), иначе WordPress уводил бы /en/ на главную без префикса.
Практический нюанс: редирект отдаётся кодом 302, а не 301. Для поисковых систем это значит «временный переход»; при стабильной структуре разумнее отдавать 301 на уровне nginx, оставив плагину только выбор контента:
# 301 для «чистых» URL языка по умолчанию — только если так решено на уровне проекта
location = /about/ {
return 301 /ru/about/;
}
Рекомендация: не смешивайте два механизма. Либо префикс ставит только плагин, либо только nginx — иначе получите цепочку из двух редиректов и потеряете часть веса ссылок.
Контент: оригинал, переводы и черновики
Модель данных простая: одна статья = одна запись; перевод = отдельная запись с тем же _ml_group_id и другим _ml_language. Никаких «полей-переводов» внутри одной записи.
Автоматические черновики. Если ml_auto_create_drafts включено, при сохранении записи плагин создаёт пустые черновики для остальных языков и связывает их общей группой. Дальше редактор просто открывает нужный язык и пишет текст — структура уже готова.
Ручное создание. Выключите автогенерацию и создайте запись вручную, задав два метаполя:
wp post create --post_type=post --post_status=draft \
--post_title="Hello world" \
--meta_input='{"_ml_language":"de","_ml_group_id":"mlg_64b034b4b7e44.18830524"}'
Язык в списках записей. В таблице записей появляются ссылки вида Edit DE, Edit EN — они открывают соответствующую языковую версию. Фильтр ?ml_language=de в адресе списка оставляет только записи этого языка.
REST API: метаполя и внешние сценарии
Оба метаполя зарегистрированы через register_post_meta('', ...) с show_in_rest = true, то есть доступны в стандартном REST API для всех типов записей. Права — edit_posts.
# создать немецкую версию через REST (аутентификация — Application Password)
curl -X POST https://example.com/wp-json/wp/v2/posts \
-H "Authorization: Basic $(printf 'user:app-password' | base64)" \
-H "Content-Type: application/json" \
-d '{
"title": "Hallo Welt!",
"content": "<p>Deutsche Version…</p>",
"status": "publish",
"meta": {
"_ml_language": "de",
"_ml_group_id": "mlg_64b034b4b7e44.18830524"
}
}'
Важно: группа переводов — произвольная строка. Плагин сравнивает её как есть, поэтому
mlg_...иMLG_...— разные группы. Генерируйте значение один раз и храните.
Переключатель языка: шорткод, виджет, PHP
Три способа вывести переключатель:
[language_switcher]
// в шаблоне темы: ссылка на французскую версию текущего URL
$plugin = \MultilingualEngine\Plugin::instance();
echo esc_url( $plugin::translateUrl( get_permalink(), 'fr', $plugin->langs ) );
Третий — виджет Language Switcher в «Внешний вид → Виджеты». Все три используют один и тот же хелпер translateUrl(), который подменяет первый сегмент пути, сохраняя остальную часть адреса.
SEO: hreflang и что он не делает
Плагин вставляет в <head> ссылки <link rel="alternate" hreflang="…" href="…"> на доступные переводы. Настройки не требуются.
Чего он не делает:
- не формирует
sitemap.xmlс языковыми версиями (это отдельная задача, обычно решается SEO-плагином); - не проставляет
canonicalна «правильную» языковую версию; - не мешает дублированию текста, если перевод сделан машинно и совпадает с оригиналом.
Для мультиязычного сайта минимальный комплект — hreflang + собственный canonical + разные <title> и <meta name="description"> на каждом языке. Первое даёт плагин, остальное — тема и SEO-плагин.
Обслуживание через WP-CLI
Поиск записей, у которых не хватает переводов:
wp eval '
$plugin = \MultilingualEngine\Plugin::instance();
$langs = $plugin->langs;
foreach ( get_posts(["post_type"=>"post","posts_per_page"=>-1,"fields"=>"ids"]) as $id ) {
$have = wp_get_post_terms($id, "ml_language", ["fields"=>"slugs"]);
if ( count($have) < count($langs) ) {
printf("%d\t%s\t%s\n", $id, get_post_meta($id, "_ml_group_id", true), implode(",", $have));
}
}'
Массовое назначение языка старым записям делает кнопка в настройках, но то же можно сделать из консоли:
wp post term set 1234 ml_language ru --by=id
Важно: удаление черновиков — операция необратимая. Сначала список, потом бэкап, только потом удаление:
# 1) посмотреть, что попадёт под удаление
wp post list --post_status=draft --meta_key=_ml_group_id --fields=ID,post_title --format=table
# 2) бэкап
wp db export backup-before-cleanup.sql
# 3) удаление (только после проверки списка)
wp post list --post_status=draft --meta_key=_ml_group_id --format=ids \
| xargs -r -n1 wp post delete --force
Подводные камни
1. Деактивация ломает URL. Префиксы перестают обрабатываться, и /en/post/ отдаёт 404. Перед отключением подготовьте редиректы на «чистые» адреса.
2. Удаление плагина не удаляет данные. Таксономия, мета и опции остаются в базе. Это осознанное решение: контент важнее чистоты. Чистить — вручную и после бэкапа.
3. 302 в редиректах. Для стабильной структуры поисковику нужен 301; если это принципиально, делайте редирект на уровне веб-сервера.
4. Нет Accept-Language. Гость из Германии по умолчанию увидит русскую версию. Для международного трафика стоит добавить баннер выбора языка или переопределить детектор своим фильтром.
5. Кука HttpOnly выключена. Код языка читается скриптами. Само значение безобидно, но не превращайте эту куку в хранилище пользовательских предпочтений.
6. Мультисайт не поддерживается. Для сети сайтов нужна другая архитектура: отдельный сайт на язык либо Polylang Pro/WPML.
7. Дублирование контента. Каждый перевод — отдельная запись со своим URL. Если переводы генерируются автоматически и совпадают с оригиналом, поисковики увидят дубли; hreflang помогает, но не заменяет нормальный перевод.
8. Расхождение списка языков и данных. Убрали язык из настроек — записи с этим кодом остались в таксономии. Их нужно переназначить, иначе они выпадут из листингов.
Показательные случаи
Случай 1: «редирект в никуда». После добавления языка de в список главная начала уводить на /de/, где не было ни одной записи, — и это выглядело как поломка сайта. Причина: ml_copy_home выключен, копия главной не создана, а сам редирект на корень языка плагин выполняет без проверки существования записей. Лечится кнопкой копирования главной или отключением лишнего языка.
Случай 2: «переводы есть, а листинг пуст». Редактор создавал переводы вручную и не заполнял _ml_language: записи существовали, но фильтр по языку их не видел. Лечится массовым назначением языка и привычкой проверять метаполе после импорта.
Чек-лист после внедрения
- [ ] Список языков и язык по умолчанию сохранены, лишних кодов нет.
- [ ] Главная и ключевые страницы открываются с префиксом и без него (302).
- [ ] В
<head>естьhreflangна все языки, включаяx-default(если нужен — ставится темой). - [ ] Переключатель выведен в шапке или подвале и не ломает вёрстку на мобильном.
- [ ] Кука
ml_langставится и сбрасывается корректно. - [ ] В REST метаполя видны и записываются (
_ml_language,_ml_group_id). - [ ] Для старых записей язык назначен массово, «безымянных» записей нет.
- [ ] Бэкап базы снят до любых массовых операций.
- [ ] Проверено поведение при деактивации: план редиректов готов.
FAQ
Нужен ли плагин, если языков всего два и переводов мало?
Для двух языков и десятка страниц достаточно префиксов и hreflang. Плагин начинает окупаться, когда записей сотни и нужен предсказуемый листинг по языку.
Почему /en/post/ отдаёт 404, хотя перевод существует?
Проверьте _ml_language у записи и наличие языка в ml_available_languages. Если язык убрали из списка, префикс перестаёт распознаваться.
Можно ли отдавать 301 вместо 302? Да, но на уровне nginx/Apache: плагин всегда отдаёт 302. Два механизма одновременно держать не стоит.
Как добавить x-default в hreflang?
Штатно — никак: плагин выводит только реальные языковые версии. x-default добавляется темой через wp_head с приоритетом ниже плагина.
Что будет с сайтом при удалении плагина? Контент и метаданные останутся, но префиксы перестанут обрабатываться. Сначала редиректы, потом удаление.
Работает ли с WooCommerce?
Таксономия ml_language регистрируется для post и page, поэтому товары нужно подключать отдельно — расширением списка типов записей в коде.
Итог
Multilingual Engine закрывает узкую, но частую задачу: языковые версии материалов в одной установке WordPress, без внешних зависимостей и без вмешательства в ядро. Сильные стороны — простая модель данных, быстрый листинг по языку и готовый hreflang; слабые — ручное управление переводами, 302 в редиректах и отсутствие мультисайта.
Если языков два-три и контент переводится вручную, этого достаточно. Если нужны автоматические переводы, отдельные домены под языки и синхронизация с внешними системами — смотрите в сторону зрелых плагинов, но готовьтесь к их структуре данных.
Цитата: Мультиязычность ломается не на переводе текста, а на структуре URL: как только появляется второй способ адресовать ту же страницу, кто-то обязательно придёт с вопросом «почему у меня 404».
Связанные материалы
- PHP MD.Viewer: один файл вместо CMS — практика написания собственного инструмента на PHP без фреймворка.
- GUID Cleaner для ItGalaxy WooCommerce Synchronization 1C — работа с данными WordPress изнутри, включая метаполя и таксономии.
- Английская версия этого сайта — пример того, как выглядит языковая версия в живом проекте.
- Рубрика «Системное администрирование» — остальные материалы по эксплуатации серверов и сайтов.
LLM Summary
- Тема: мультиязычный WordPress на языковых префиксах URL без WPML и Polylang.
- Задача: дать сайту языковые версии статей, сохранив быстрые выборки и корректный
hreflang. - Платформа: WordPress 6.8+, PHP 8.3+; плагин Multilingual Engine 1.09.5, одиночная установка (не Multisite).
- Краткое решение: включить плагин, задать
ml_available_languagesиml_default_languageв менюMulti Lang, вывести[language_switcher]; язык записи хранится в таксономииml_language, переводы связываются метаполем_ml_group_id. - Ключевые команды/действия:
wp option get ml_available_languages;wp post term set <ID> ml_language <code> --by=id;wp evalдля поиска неполных переводов; RESTPOST /wp-json/wp/v2/postsсmeta._ml_languageиmeta._ml_group_id. - Основные риски: редирект
302вместо301; отсутствие учётаAccept-Language; 404 после деактивации плагина; дубли контента при машинном переводе; необратимое удаление черновиков. - Когда применять: 2–3 языка, ручной перевод, одна установка WordPress, нужен быстрый листинг по языку и
hreflang. - Когда не применять: мультисайт, автоматический перевод больших объёмов, требование «сайт на язык в отдельном домене».
- Проверка результата:
curl -I /about/возвращает302на/ru/about/; в<head>естьhreflangна все языки; метаполя видны в REST; записи без_ml_languageотсутствуют.