Переводы
Какие страницы — переводы друг друга: чтение и запись групп переводов Polylang и связывание перевода прямо при публикации.
Группа переводов — собственная конструкция Polylang: терм post_translations, в описании которого лежит сериализованная карта {lang: id}, и связь каждой страницы группы с этим термом. v2 пишет её построчным портом save_translations() и delete_translation() из Polylang, поэтому группа, записанная здесь, байт в байт совпадает с записанной самим Polylang в PHP: тот же порядок в карте, то же имя терма, та же уборка опустевших групп.
Из группы печатаются hreflang и переключатель языка (op/lang-switcher). У страницы вне своей группы нет ни того, ни другого, а в остальном она выглядит нормально, поэтому все проверки ниже выполняются до записи, и проваленная проверка ничего не пишет.
| Метод | Путь | Что делает |
|---|---|---|
| GET | /pages/{ref}/translations | группа страницы |
| PUT | /pages/{ref}/translations | записать группу целиком |
| POST | /pages/{ref}/translations/{lang} | добавить один перевод, остальное не трогать |
| DELETE | /pages/{ref}/translations | вывести страницу из группы |
{ref} — id страницы, слаг или путь с ~ вместо /: ~ru~docs-ru~vvedenie~ для /ru/docs-ru/vvedenie/. Путь с языковым префиксом разрешается в страницу этого языка; путь без префикса и голый слаг — в страницу языка по умолчанию. Внутри JSON-тела путь пишется обычным образом. Сайт без Polylang на любой вызов отвечает 409 polylang_inactive.
GET /pages/{ref}/translations
curl -s "$BASE/pages/6409/translations" \
-H "Authorization: Bearer $OP_TOKEN" \
-H "X-OnPress-Blog-Id: 235"
{
"success": true,
"blog_id": 235,
"page": { "id": 6409, "lang": "en", "title": "Pomelli — Free AI Marketing Tool by Google for Small Businesses" },
"translations": { "en": 6409, "ru": 6419 },
"pages": {
"en": { "id": 6409, "title": "Pomelli — Free AI Marketing Tool by Google for Small Businesses", "status": "publish", "url": "/pomelli-en/", "link": "https://example.onpress.pro/pomelli-en/" },
"ru": { "id": 6419, "title": "Pomelli — бесплатный AI-инструмент от Google для маркетинга малого бизнеса", "status": "publish", "url": "/ru/pomelli/", "link": "https://example.onpress.pro/ru/pomelli/" }
},
"missing_languages": []
}
В translations всегда есть сама страница под своим языком; несуществующие страницы пропускаются. url и link в pages — адреса, которые отдаёт сайт, с языковым префиксом по настройкам Polylang: у языка по умолчанию префикса нет, если он скрыт. У неопубликованной страницы link равно null. missing_languages перечисляет языки сайта, для которых в группе нет страницы.
PUT /pages/{ref}/translations
| Поле | Тип | Описание |
|---|---|---|
translations | object | {lang: ref} — группа целиком; ref — id, слаг или путь. null, "" или 0 — страницы на этом языке нет |
curl -s -X PUT "$BASE/pages/6703/translations" \
-H "Authorization: Bearer $OP_TOKEN" \
-H "X-OnPress-Blog-Id: 240" \
-H 'Content-Type: application/json' \
-d '{"translations": {"en": 6703, "ru": "/ru/about/"}}'
{
"success": true,
"blog_id": 240,
"page": { "id": 6703 },
"translations": { "en": 6703, "ru": 6711 },
"pages": { "en": { "id": 6703, "…": "…" }, "ru": { "id": 6711, "…": "…" } },
"changed": true,
"group": { "term_id": 912, "created": true },
"unlinked": [],
"deleted_groups": [],
"content_saved": [6703, 6711]
}
Карта — это и есть группа, а не добавка к ней: страница, которая была в группе и которой нет в карте, выходит из неё и попадает в unlinked. Страница из адреса всегда участник под своим языком, в карте её можно не указывать. deleted_groups — термы групп, которые опустели при переносе и удалены, как это делает Polylang. content_saved — страницы, которым ушло событие.
Та же карта повторно — ничего не делает: changed: false, group.term_id: null, ничего не записано, событий нет. Миграция может класть все группы на каждом прогоне.
| HTTP | Код | Когда |
|---|---|---|
| 400 | bad_translations | translations — не объект |
| 409 | translations_rejected | одна или несколько записей неверны; rejected[] называет каждую с lang, ref и reason: неизвестный язык, страница не найдена, язык страницы не совпадает с ключом, одна страница дважды, чужая страница под языком страницы из адреса |
| 409 | page_in_other_group | указанная страница состоит в другой группе с другими участниками, и связывание слило бы две группы; возвращаются its_group и target_group. Сначала выведите её из группы DELETE |
| 409 | page_has_no_language | у страницы из адреса нет языка; поставьте его через POST /pages/language |
| 404, 409 | page_not_found, page_language_mismatch, ambiguous_page | {ref} в адресе не разрешается в одну страницу |
POST /pages/{ref}/translations/{lang}
| Поле | Тип | Описание |
|---|---|---|
page | id, слаг или путь | перевод, который встаёт под {lang} |
curl -s -X POST "$BASE/pages/6703/translations/ru" \
-H "Authorization: Bearer $OP_TOKEN" \
-H "X-OnPress-Blog-Id: 240" \
-H 'Content-Type: application/json' \
-d '{"page": 6711}'
Остальная группа сохраняется. Если в группе уже была другая страница на {lang}, она выходит и попадает в unlinked. Ответ — как у PUT, плюс linked: {lang, id}.
Ошибки: 400 page_required, 422 unknown_language, 409 same_language ({lang} — язык самой страницы из адреса), 409 page_language_mismatch (page на другом языке, возвращается actual_lang), 409 page_in_other_group, 409 page_has_no_language.
DELETE /pages/{ref}/translations
{
"success": true,
"blog_id": 240,
"page": { "id": 6711 },
"was_in": { "en": 6703, "ru": 6711 },
"remaining": { "en": 6703 },
"unlinked": true,
"changed": true,
"group_deleted": false,
"content_saved": [6703, 6711]
}
Страница остаётся опубликованной и сохраняет язык; уходит только связь. Группа, в которой не осталось страниц, удаляется (group_deleted: true); группа с одной страницей остаётся, как в Polylang. Страница, которая ни в какой группе не была, отвечает unlinked: false, changed: false.
translation_of при публикации
Новый перевод обычно связывают прямо при публикации: translation_of во frontmatter POST /pages/markdown вводит страницу в группу оригинала той же транзакцией, что и запись, — страница либо сохранена и связана, либо не сохранено ничего.
---
name: "Страницы и markdown"
slug: "stranicy"
url: "/docs-ru/stranicy/"
lang: "ru"
translation_of: 6187
---
translation_of — id, слаг или путь оригинала, путь может быть с языковым префиксом. Нужен явный lang, отличный от языка оригинала. Группа сливается, а не заменяется, поэтому третий язык присоединяется к первым двум; если в группе уже была страница на lang, она выходит, и об этом говорит предупреждение. В ответе — translations и translation_group:
{
"id": 6353,
"lang": "ru",
"translations": { "ru": 6353, "en": 6187 },
"translation_group": { "origin": { "id": 6187, "lang": "en" }, "term_id": 126, "created": false, "changed": true, "unlinked": [6234] }
}
Это настоящий ответ на публикацию русской версии страницы Страницы и markdown: в группе уже стояла русская страница-заглушка #6234, и она из группы вышла. Повторная публикация того же перевода даёт changed: false. Ссылка, которая не держится, — 422 translation_link_failed до всякой записи; страница, найденная по слагу и состоящая в другой группе, — 409 translation_group_conflict, переносить её нужно PUT выше.
Язык, группы и события
Страница входит в группу только под своим языком, поэтому страницу без языка ручки отклоняют (page_has_no_language). Терм языка ставится публикацией с lang или чинится через POST /pages/language; сама эта ручка групп не касается.
После каждой записи, изменившей группу, onpress/content_saved уходит каждой странице, которая в группе есть или была, — в том числе только что вышедшей, у которой hreflang должен исчезнуть. PHP сбрасывает у неё кэши связей и термов Polylang и запись в страничном кэше и пересобирает её индекс Yoast. Событие асинхронное; ответ перечисляет страницы в content_saved, не дожидаясь.
Весь путь — опубликовать оригинал, опубликовать перевод, проверить группу — в рецепте Перевод страницы.