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

TL;DR: что это и с чего начать
- Что это. Плагин WordPress: префикс языка в URL, фильтрация запросов по языку, плавающий переключатель с флагами.
- Как хранит. Язык — таксономия
ml_language, группа переводов — мета_ml_group_id, выбранный язык посетителя — кукаml_lang. - Что даёт для перевода. Мета-поля доступны в REST, поэтому внешний скрипт может читать черновики переводов и записывать в них текст.
- Что не делает. Не переводит сам: ни встроенного вызова OpenAI, ни DeepL в модуле нет.
- Первые три шага. Задать языки, назначить язык старым записям, проверить редирект и
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="…"/> |
там же |
Два вывода. Первый: опции заполнены при выключенном плагине — настройки переживают деактивацию и включаются вместе с ней. Второй: версия на сайте (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), а заодно показала главное: документация и код расходятся в определении языка, и решать этот спор должен прогон, а не вера в текст.
Цитата: Многоязычный сайт держится не на переключателе, а на том, что у каждой страницы есть язык — и что об этом помнят все запросы.
Связанные материалы
- Multilingual Engine: многоязычный WordPress без WPML и Polylang — разбор того же движка со стороны данных и кода.
- HTML.PHP: шаблонизатор PHP в одном файле — про шаблоны и локализацию без зависимостей.
- Cert Unifier 3.1.1 — проверка ключа сертификата до записи, та же дисциплина «сначала проверить».
- Bash WP-CLI Update 8.1.0 — обслуживание WordPress из консоли, включая работу с опциями.
- MD Viewer: просмотрщик Markdown без браузерного движка — как читать чужую документацию перед работой.
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 обновил перевод.