Многоязычный сайт на WordPress обычно означает выбор: тяжёлый плагин со своими таблицами или ручная возня с копиями записей. ml-switcher — третий путь: переключатель языков WordPress на своём движке, где язык записи живёт в таксономии, адрес получает префикс /en/, а переводы связаны общей группой. Отдельная история — машинный перевод: модуль его не делает, но даёт каркас из REST-полей, к которому внешний скрипт подключается за вечер. Ниже — как это устроено, где документация расходится с кодом и что проверить до включения на боевом сайте.

ml switcher wordpress cover

TL;DR: что это и с чего начать

  1. Что это. Плагин WordPress: префикс языка в URL, фильтрация запросов по языку, плавающий переключатель с флагами.
  2. Как хранит. Язык — таксономия ml_language, группа переводов — мета _ml_group_id, выбранный язык посетителя — кука ml_lang.
  3. Что даёт для перевода. Мета-поля доступны в REST, поэтому внешний скрипт может читать черновики переводов и записывать в них текст.
  4. Что не делает. Не переводит сам: ни встроенного вызова OpenAI, ни DeepL в модуле нет.
  5. Первые три шага. Задать языки, назначить язык старым записям, проверить редирект и hreflang.
# список языков и язык по умолчанию
wp option get ml_available_languages
wp option get ml_default_language

# черновики переводов на испанский через REST
curl -u admin:app-password "https://example.com/wp-json/wp/v2/posts?status=draft&ml_language=es"

Проблема, которую решает модуль

Первая попытка сделать сайт двуязычным обычно выглядит так: копируют записи, вручную дописывают /en/ в адреса и надеются, что меню не разъедется. Через месяц обнаруживается, что часть страниц не имеет пары, часть ссылок ведёт на язык по умолчанию, а поисковая выдача смешивает версии.

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

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

Задача Решение
Отфильтровать записи по языку обычный tax_query, индекс есть
Связать переводы одной статьи мета _ml_group_id
Дать адрес с языком префикс в URL и 302 на него
Показать переключатель плавающая кнопка с флагами
Отдать переводы внешнему сервису REST-поля _ml_language и _ml_group_id

Схема базы при этом не меняется: всё лежит в стандартных таблицах WordPress.

Что такое ml-switcher

flowchart TB
  SITE["WordPress с модулем ml-switcher"] --> REST["REST API WordPress"]
  REST --> META["мета _ml_group_id и _ml_language, show_in_rest"]
  REST --> POSTS["записи: черновики переводов по языкам"]
  EXT["внешний скрипт на Python"] --> REST
  EXT --> LLM["сервис перевода: OpenAI, DeepL, другой"]
  LLM --> EXT
  EXT --> PUT["запись перевода обратно через PUT"]
  PUT --> POSTS
Свойство Значение
Тип плагин WordPress, одиночная установка
Требования WordPress 6.8+, PHP 8.3+
Состав MultilingualEngine.php, ml-switcher.css, ml-switcher.js
Клиентская часть плавающий переключатель: кнопка-флаг и выпадающий список
Серверная часть движок: определение языка, редирект, фильтр запросов, hreflang
Внешние зависимости нет
Мультисайт не поддерживается

Разделение на две части удобно тем, что интерфейс можно заменить: CSS и JS модуля отвечают только за кнопку, вся логика остаётся в PHP.

Возможности

Языки и контент

Что Как работает
Отдельная запись на язык каждая версия — обычная запись со своим термином таксономии
Связь переводов общий _ml_group_id у всех версий
Черновики переводов опция автосоздания: при сохранении записи появляются пустые версии на других языках
Массовое назначение языка кнопка для старых записей: без неё контент исчезает из выдачи, потому что фильтр по языку его не находит
Фильтр в админке список записей ограничивается выбранным языком
Учётные записи языков служебные пользователи lang_xx для определения языка у авторизованных

Адреса и SEO

Что Как работает
Префикс в URL первый сегмент пути — код языка
Редирект 302 на языковую версию, только если страница существует
hreflang ссылки rel="alternate" для всех переводов записи
Кука выбора запоминает язык посетителя, срок — год
Канонический адрес на языковую версию материала

Интеграция

Что Как работает
REST-мета _ml_language и _ml_group_id с show_in_rest
Фильтр REST запрос вида ?ml_language=es понимается модулем
Авторизация мета auth_callback требует права edit_posts
Внешний перевод скрипт читает черновик, переводит, записывает обратно через PUT
WP-CLI чтение и правка опций, массовые операции с языками

Как выбирается язык

flowchart LR
  REQ["запрос страницы"] --> P{"префикс языка в URL есть?"}
  P -- да --> SET["язык из URL"]
  P -- нет --> CK{"кука ml_lang есть?"}
  CK -- да --> SET2["язык из куки"]
  CK -- нет --> DEF["язык по умолчанию"]
  SET --> FILT["фильтр main query по таксономии"]
  SET2 --> FILT
  DEF --> FILT
  FILT --> OUT["страница нужного языка, переключатель и hreflang"]

Последовательность в документации модуля выглядит так: префикс URL → кука ml_lang → заголовок Accept-Language (первые две буквы, например en из en-US) → язык по умолчанию.

Здесь важная оговорка, которую я обязан сделать. В разборе того же движка по исходникам (MultilingualEngine.php) порядок другой и Accept-Language в нём вообще не участвует: сначала ник авторизованного пользователя вида lang_xx, затем префикс URL, затем кука, затем значение по умолчанию. Документация модуля и код расходятся — это требует проверки автором на живом сайте, и до проверки доверять стоит коду, а не описанию.

Шаг По документации модуля По разбору исходников
1 префикс URL ник lang_xx у авторизованного
2 кука ml_lang префикс URL
3 Accept-Language кука ml_lang
4 язык по умолчанию язык по умолчанию

Практическое следствие одно: не рассчитывайте на автоматическое «угадывание» языка по браузеру, пока не проверите сами. Гость с немецким браузером может увидеть русскую версию — и это нормально для модуля, у которого язык по умолчанию задан явно.

Переключатель языков

flowchart TD
  SET["список языков и язык по умолчанию"] --> OPT["опции ml_available_languages и ml_default_language"]
  OPT --> ASSIGN["массовое назначение языка старым записям"]
  ASSIGN --> DRAFT["автосоздание черновиков переводов"]
  DRAFT --> UI["плавающий переключатель: CSS и JS модуля"]
  UI --> SEO["hreflang на страницах записей"]

Из коробки модуль выводит плавающую кнопку в правом верхнем углу: флаг текущего языка, при клике — список доступных. Стили в ml-switcher.css, поведение в ml-switcher.js: открытие по клику и закрытие при клике вне списка.

Что Где настраивается
Состав языков опция со списком кодов через запятую
Порядок языков порядок в списке
Внешний вид ml-switcher.css темы или модуля
Поведение списка ml-switcher.js
Точка вывода хук wp_body_open или wp_footer темы

Требование к теме простое: если она не вызывает эти хуки, переключатель не появится, и это первое, что стоит проверить, когда «ничего не видно».

Замеры и цифры

Проверено 09.11.2026: на живом сайте, где установлен этот движок.

Что Значение Как
Каталог плагина wp-content/plugins/multilingual-engine ls на сервере
Версия установленной копии 1.09.0 wp plugin get multilingual-engine
Состояние inactive — установлен, выключен там же
Список языков в опциях en,ru wp option get ml_available_languages
Язык по умолчанию ru wp option get ml_default_language
Основной файл движка 1 290 строк wc -l MultilingualEngine.php
Таксономия в коде register_taxonomy(ML_TAXONOMY, ['post','page'], …) grep -n по файлу
Вывод hreflang <link rel="alternate" hreflang="…" href="…"/> там же
ml-switcher: что подтверждено на установленной копии
ml-switcher: что подтверждено на установленной копии

Два вывода. Первый: опции заполнены при выключенном плагине — настройки переживают деактивацию и включаются вместе с ней. Второй: версия на сайте (1.09.0) не совпадает с той, по которой писалась документация (1.09.5), поэтому любые правки в коде надо делать по фактическому файлу.

Чего замеры не показывают: работу редиректов, вид переключателя и hreflang в выдаче. Плагин выключен, а включение на боевом сайте меняет адреса страниц — это отдельное решение владельца, не техническая проверка.

Сильные стороны продукта

  • Структура остаётся вашей. Две меты и одна таксономия вместо собственных таблиц.
  • Быстрые выборки. Язык — термин таксономии, фильтр идёт по индексу, а не по postmeta.
  • Переключатель из коробки. Плавающая кнопка с флагами без правки темы — при условии, что тема вызывает нужные хуки.
  • Готовый каркас для машинного перевода. REST-поля и группы переводов уже есть, внешнему скрипту остаётся перевести текст.
  • Мелкий и читаемый код. 1 290 строк в основном файле — можно прочитать целиком.
  • Аккуратные редиректы. 302 ставится только на существующую страницу, а не на любой адрес.
  • Настройки переживают выключение. Проверено: плагин в статусе inactive, а опции на месте.
  • Нет жёсткой привязки к сервису перевода. OpenAI, DeepL, Яндекс — что угодно, модуль не диктует.

Установка

# файлы модуля копируются в каталог плагина
cp -r MultilingualEngine/ wp-content/plugins/
chown -R www-data:www-data wp-content/plugins/MultilingualEngine
wp plugin activate MultilingualEngine
Требование Значение
WordPress 6.8 или новее
PHP 8.3 или новее
Права на каталог владелец — пользователь веб-сервера
Хуки темы wp_body_open или wp_footer для переключателя

Права на каталог — не формальность: если файлы принадлежат root, правки через админку не пройдут, а часть хостингов отдаст ошибку 500 при записи лога.

Настройка

Порядок, при котором ничего не теряется:

Шаг Действие
1 задать список языков и язык по умолчанию
2 нажать массовое назначение языка для старых записей
3 решить, нужны ли автосоздаваемые черновики переводов
4 проверить переключатель на фронтенде
5 проверить редирект на префикс и hreflang в исходном коде страницы

Важная деталь про то, где искать настройки. Документация модуля говорит про «Настройки → Общие», а разбор исходников и практика — про отдельное меню Multi Lang со страницей admin.php?page=multilang-settings. Расхождение стоит проверить на своей версии: если в «Общих» ничего нет, смотрите отдельное меню.

Опция Что задаёт Значение по умолчанию
ml_available_languages коды языков через запятую en
ml_default_language язык для новых посетителей первый из списка
ml_auto_create_drafts создавать пустые переводы при сохранении включено

Отдельно про массовое назначение: без него старые записи не имеют термина языка, и фильтр по языку их просто не покажет. Это самая частая причина «сайт опустел после включения» — не ошибка модуля, а пропущенный шаг.

Машинный перевод через REST

Здесь заканчивается модуль и начинается ваш скрипт. Схема из четырёх шагов:

Шаг Запрос Что происходит
1 GET /wp-json/wp/v2/posts?status=draft&ml_language=es получаем черновики переводов
2 GET /wp-json/wp/v2/posts?meta_key=_ml_group_id&meta_value=mlg_… находим оригинал по группе
3 запрос к сервису перевода получаем текст на нужном языке
4 PUT /wp-json/wp/v2/posts/42 с title и content перевод попадает в запись

Три тонкости, без которых схема не работает.

Первая — авторизация. Мета-поля пишутся только пользователем с правом edit_posts, поэтому скрипту нужен либо Application Password, либо JWT, либо работа от администратора. Анонимный PUT вернёт отказ.

Вторая — таксономия закрыта от REST (show_in_rest = false), поэтому фильтровать проще по мете _ml_language или параметром ?ml_language=xx, который модуль понимает.

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

import requests

site = "https://example.com"
auth = ("editor", "app-password")
api = f"{site}/wp-json/wp/v2/posts"

drafts = requests.get(f"{api}?status=draft&ml_language=es", auth=auth).json()
for draft in drafts:
    group = draft["meta"]["_ml_group_id"]
    origin = requests.get(f"{api}?meta_key=_ml_group_id&meta_value={group}&status=publish", auth=auth).json()
    if not origin:
        continue
    source = origin[0]["content"]["rendered"]
    # здесь вызов вашего сервиса перевода
    translated = translate(source, "es")
    requests.put(f"{api}/{draft['id']}", auth=auth, json={"content": translated})

Оговорка: приведённый фрагмент показывает форму запросов, а не готовый конвейер; вызов translate() — ваш.

Данные и хранение

Что Где живёт Зачем
Язык записи таксономия ml_language фильтрация и адреса
Группа переводов мета _ml_group_id связать версии одной статьи
Код языка записи мета _ml_language доступно в REST
Выбранный язык кука ml_lang помнить выбор посетителя
Список языков опция ml_available_languages настройки сайта
Пароли служебных учёток опция ml_user_passwords учётные записи lang_xx
Событие Что происходит
Добавили язык существующим записям можно создать черновики на новом языке
Удалили язык записи этого языка остаются в базе, но перестают участвовать в навигации
Удалили запись группа переводов у остальных версий не меняется
Отключили плагин настройки сохраняются, адреса начинают работать без префикса

Ошибки и отладка

Симптом Вероятная причина Что проверить
после включения сайт опустел записям не назначен язык массовое назначение языка
переключателя не видно тема не вызывает wp_body_open/wp_footer шаблон темы
редиректа нет перевода не существует наличие записи по целевому адресу
пустые страницы на новом языке черновики не заполнены список черновиков в админке
PUT возвращает 401 не переданы учётные данные Application Password
PUT возвращает 403 у пользователя нет edit_posts роль учётной записи
hreflang отсутствует плагин выключен или тема перебивает wp_head исходный код страницы

Как это выглядит в работе

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

Шаг Что происходит Кто за это отвечает
1 открывает адрес без префикса хук template_redirect выбирает язык
2 получает 302 на /xx/адрес модуль, если перевод существует
3 видит страницу на своём языке фильтр pre_get_posts по таксономии
4 в углу — флаг текущего языка CSS и JS модуля
5 в HTML — hreflang для поисковика хук wp_head

Если на каком-то шаге поведение не то, искать надо именно там, а не «вообще в плагине»: каждый шаг живёт в своём хуке и ломается по своей причине.

Чем это отличается от тяжёлых плагинов

Критерий Крупный плагин локализации ml-switcher
Свои таблицы обычно есть нет
Перевод строк темы есть нет
Мультисайт есть нет
Структура URL своя логика префикс и штатный роутер
Машинный перевод иногда встроен отдельным скриптом
Перенос сайта разбор чужих схем обычный дамп базы

Выбор честно сводится к вопросу «нужна ли локализация продукта или только несколько языков материалов». Первое — крупный плагин, второе — этот модуль. Смешивать их бессмысленно: получится два источника истины об языке страницы.

Обслуживание

Что делать Как часто Зачем
проверять новые записи на наличие языка при каждой публикации фильтр не покажет запись без термина
смотреть черновики переводов раз в неделю пустые черновики копятся и мешают поиску
проверять hreflang после смены темы после обновления темы новая тема может не вывести wp_head
сбрасывать кеш после записи переводов по факту перевода иначе старый текст живёт в кеше
сверять версию модуля с документацией перед правками кода проверено: версии расходятся

Отдельно про версии: модуль не из каталога WordPress.org, автообновлений нет. Любая правка в файлах — это ваша ветка кода, и её стоит держать в git рядом с сайтом, иначе при обновлении всё потеряется.

Подводные камни

  • Порядок определения языка описан в документации и в коде по-разному. Документация включает Accept-Language, разбор исходников — нет. Проверять на своей версии, а не верить описанию.
  • Accept-Language может не работать. Если он не учитывается, международная аудитория увидит язык по умолчанию, пока не переключит вручную.
  • Кука сильнее чистого URL. Один раз выбранный язык уводит на префикс при следующем визите; сбрасывается удалением куки.
  • Массовое назначение языка обязательно. Пропуск шага выглядит как «всё сломалось», хотя это ожидаемое поведение фильтра.
  • Редирект только на существующую страницу. Это защита от цепочек на 404, но и причина «почему адрес без префикса не перекинуло».
  • Тема должна вызывать хуки. Без wp_body_open или wp_footer переключатель просто не выведется.
  • Таксономия закрыта от REST. Фильтровать надо по мете или параметру, а не через /wp/v2/ml_language.
  • Мета пишется только с edit_posts. Внешнему скрипту нужен токен, а не «просто curl».
  • Версия на сайте может отличаться от документации. Проверено: установлена 1.09.0 при документации на 1.09.5.
  • Плагин не обновляется из каталога. Его нет в WordPress.org: обновление — замена каталога, правки в коде ведутся отдельно.
  • Кеш страниц после перевода. Новый перевод может не появиться до сброса кеша.
  • Мультисайта нет. Схема «сайт на язык» — другой инструмент.

Чек-лист

  • [ ] Проверить требования: WordPress 6.8+, PHP 8.3+.
  • [ ] Скопировать файлы модуля и выставить владельца каталога.
  • [ ] Задать список языков и язык по умолчанию.
  • [ ] Выполнить массовое назначение языка для старых записей.
  • [ ] Проверить, что тема вызывает wp_body_open или wp_footer.
  • [ ] Убедиться, что переключатель виден и переключает.
  • [ ] Проверить 302 на префикс и его отсутствие для несуществующих адресов.
  • [ ] Проверить hreflang в исходном коде страницы.
  • [ ] Ограничить права записи мета-полей и выдать скрипту Application Password.
  • [ ] Записать версию модуля и дату проверки в паспорт сайта.

FAQ

Модуль переводит сам?

Нет. Он даёт структуру: язык записи, группа переводов, REST-доступ к мете. Перевод делает внешний скрипт и выбранный сервис.

Чем это отличается от Multilingual Engine того же автора?

Это тот же движок, но описанный со стороны интерфейса и интеграции: переключатель, REST и машинный перевод. Устройство данных то же — таксономия и две меты.

Почему после включения пропали записи?

У них нет термина языка. Нужно один раз выполнить массовое назначение языка по умолчанию.

Можно ли обойтись без переключателя?

Да. Логика работает и без кнопки: адреса с префиксом и hreflang остаются. Кнопка — только удобство.

Как автоматизировать перевод на новый язык?

Скрипт получает черновики через REST по ml_language, находит оригинал по _ml_group_id, переводит текст и записывает обратно PUT-запросом с авторизацией.

Нужен ли Accept-Language?

По документации — да, по коду — нет. Проверяйте на своей установке; если для вас важно поведение по браузеру, это первый тест.

Что было в старой документации и что изменилось

Исходный материал был документом на 87 808 знаков: введение, архитектура, алгоритм по шагам, установка, настройка интерфейса, серверная обработка, интеграция с ИИ, хранение данных, обслуживание, ошибки, примеры кода и глоссарий.

Было Стало
академическое описание «модуль предназначен для…» быстрый вход и проверяемые шаги
две диаграммы в тексте без соответствия странице три схемы в формате статьи
утверждения без пометок отдельно отмечено, что проверено, а что взято из документации
противоречия не замечены расхождение про Accept-Language и про место настроек выделено

Главное изменение — отношение к источнику. Документация писалась как описание замысла, и в двух местах она расходится с кодом того же движка. Поэтому таблицы выше разделены на «проверено 09.11.2026» и «по документации модуля», а споры отданы прогону на живом сайте.

Итог

ml-switcher решает задачу многоязычности без чужих таблиц: язык — термин таксономии, переводы связаны группой, адреса получают префикс, а переключатель выводится сам. За это платят ручными шагами: массовое назначение языка, заполнение черновиков, отдельный скрипт для машинного перевода. Проверка на живом сайте подтвердила имена опций и версию установленной копии (1.09.0, выключена, языки en,ru), а заодно показала главное: документация и код расходятся в определении языка, и решать этот спор должен прогон, а не вера в текст.

Цитата: Многоязычный сайт держится не на переключателе, а на том, что у каждой страницы есть язык — и что об этом помнят все запросы.

Связанные материалы

LLM Summary

  • Тема: ml-switcher — модуль мультиязычности WordPress: префикс языка в URL, таксономия ml_language, мета _ml_group_id и _ml_language, кука ml_lang, плавающий переключатель с флагами.
  • Требования: WordPress 6.8+, PHP 8.3+; файлы MultilingualEngine.php, ml-switcher.css, ml-switcher.js; мультисайт не поддерживается.
  • Порядок определения языка расходится: документация модуля — префикс URL → кука → Accept-Language → умолчание; разбор исходников того же движка — ник lang_xx → префикс → кука → умолчание, без Accept-Language. Требует проверки автором.
  • Проверено 09.11.2026 на живом сайте: каталог wp-content/plugins/multilingual-engine, версия 1.09.0, статус inactive, ml_available_languages = en,ru, ml_default_language = ru, основной файл 1 290 строк, register_taxonomy и вывод hreflang в коде присутствуют.
  • Машинный перевод: встроенного нет; каркас — REST-мета с show_in_rest, фильтр ?ml_language=xx, запись только с правом edit_posts; схема: черновики → оригинал по _ml_group_id → перевод → PUT.
  • Обязательный шаг внедрения: массовое назначение языка старым записям, иначе фильтр их не покажет.
  • Точки отказа: отсутствие хуков wp_body_open/wp_footer в теме, несуществующий перевод (нет 302), кеш страниц после записи перевода, таксономия закрыта от REST.
  • Не проверялось: редиректы, вид переключателя, hreflang в выдаче — плагин на сайте выключен, включение меняет адреса.
  • Ссылки: https://sysadministrator.ru/ml-switcher-wordpress/, https://sysadministrator.ru/multilingual-engine-wordpress/

Промт для агента (установка и настройка)

Готовый промт: скопируйте блок целиком. У модуля нет файла AGENTS.md — опора на код плагина и на проверки, описанные выше.

Задача: включить ml-switcher на сайте WordPress, сделать три языка рабочими по префиксу и убедиться, что старый контент не пропал.

Шаг 1. Снять состояние до работ:
wp plugin get multilingual-engine --fields=version,status
wp option get ml_available_languages
wp option get ml_default_language

Шаг 2. Скопировать файлы модуля в каталог плагинов, выставить владельца каталога пользователем веб-сервера, активировать плагин.

Шаг 3. Задать языки: wp option update ml_available_languages "ru,en,de"; wp option update ml_default_language "ru"

Шаг 4. Выполнить массовое назначение языка старым записям (кнопка в настройках модуля). Без этого шага контент исчезнет из выдачи.

Шаг 5. Проверить, что тема вызывает wp_body_open или wp_footer — иначе переключатель не появится.

Шаг 6. Проверить адреса: curl -I https://site/about/ → ожидается 302 на /ru/about/ при наличии перевода; для несуществующего перевода редиректа быть не должно.

Шаг 7. Проверить hreflang: в исходном коде страницы должны быть link rel="alternate" hreflang="…" для всех переводов.

Шаг 8. Для машинного перевода выдать скрипту Application Password, проверить GET /wp-json/wp/v2/posts?status=draft&ml_language=de и один PUT с обновлением title и content.

Шаг 9. Сбросить кеш страниц после записи переводов.

Запрещено:
- включать плагин на боевом сайте без массового назначения языка;
- рассчитывать на Accept-Language, не проверив порядок определения языка на своей версии (документация и код расходятся);
- писать мета-поля анонимным запросом: нужен пользователь с правом edit_posts;
- фильтровать по таксономии через REST: она закрыта, использовать мету или параметр ml_language;
- искать настройки только в «Настройки → Общие»: на части версий это отдельное меню Multi Lang;
- ожидать обновлений из каталога WordPress.org: модуль ставится копированием.

Что считать успехом: все языки открываются по префиксу, старые записи на месте, переключатель виден и работает, hreflang присутствует, REST-запрос черновиков возвращает список, один тестовый PUT обновил перевод.