このブログは、多言語化にPolylangの無料版を使っています。英語記事は日本語記事をもとにREST APIで入稿していて、そのたびに「無料版のREST APIでどこまでできるのか」が問題になりました。

結論から書くと、無料版でも、記事を作るときに?lang=enを付ければ英語の記事として作られます。できないのは、日本語記事と英語記事を翻訳として紐付けることと、レスポンスへの言語の出力、一覧の言語での絞り込みです。紐付けは、Polylangの関数pll_save_post_translations()を呼ぶだけの小さなプラグインでREST APIに出して補いました。

この記事では、無料版で試した結果と、紐付けのエンドポイントの作り方をまとめます。

Polylang無料版のREST APIでできること・できないこと

結論:作成時の言語指定はできます。翻訳の紐付け、レスポンスへの言語の出力、一覧の言語での絞り込みはできません。

ローカルに立てたWordPress 7.1にPolylang 3.8.9(無料版)を入れ、日本語(既定)と英語の2言語で試しました。本番のrinblog.orgでも、読み取りだけで同じ結果になるかを確かめています。

試したこと 無料版での結果
記事の作成時に?lang=enを付ける 英語の記事として作られる
記事の作成時に言語を指定しない 既定の言語(日本語)の記事になる
投稿のレスポンスにlangtranslationsが出るか 出ない(本番でも出ない)
一覧の取得で?lang=enを付ける 絞り込まれない(日本語の記事も返る)
作成時にtranslations[ja]=記事IDを付ける 無視され、紐付かない
pll/v1の名前空間 languagessettingsだけ(本番も同じ)

PolylangのREST APIのドキュメントは、langパラメータやtranslationsフィールドを含むREST APIの機能を「Polylang Proだけ」としています。表のうち、作成時の?lang=enだけは無料版でも効きました。

無料版でも作成時のlangが効く理由

結論:無料版のコードにも、新しく記事を作るときにリクエストのlangを読んで言語を決める処理が入っているからです。

Polylang 3.8.9のソースでは、src/Capabilities/Create/Post.phpがリクエストのlangを読んで、作成する記事の言語に使っています。

if ( ! isset( $this->pref_lang ) && ! empty( $_REQUEST['lang'] ) && $lang = $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ) ) {

この処理は作成時の言語を決めるためのもので、REST APIの機能として案内されているものではありません。ドキュメントに書かれていない動きに頼っているので、Polylangを更新したら、英語記事が英語として作られているかを必ず確かめるようにしています。

翻訳の紐付けをREST APIで行う方法

結論:pll_save_post_translations()を呼ぶエンドポイントを、register_rest_route()で1本追加します。

pll_save_post_translations()は、言語スラッグと記事IDの組を渡すと、それを1つの翻訳グループとして保存するPolylangの関数です。管理画面の投稿一覧で「+」から英語版を作ったときと同じ状態になります。

次のコードは、このブログで使っているプラグインから要点だけを抜き出したものです。

add_action( 'rest_api_init', function () {
	register_rest_route(
		'rinblog-poster/v1',
		'/translations',
		array(
			'methods'             => 'POST',
			'callback'            => 'rinblog_poster_bridge_save_translations',
			'permission_callback' => function () {
				return current_user_can( 'edit_posts' );
			},
		)
	);
} );

function rinblog_poster_bridge_save_translations( WP_REST_Request $request ) {
	$translations = $request->get_param( 'translations' ); // 例: { "ja": 123, "en": 456 }
	if ( ! is_array( $translations ) || count( $translations ) < 2 ) {
		return new WP_Error( 'invalid_translations', 'translations は2件以上必要です。', array( 'status' => 400 ) );
	}

	$clean = array();
	foreach ( $translations as $lang => $post_id ) {
		$lang    = sanitize_key( $lang );
		$post_id = absint( $post_id );
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return new WP_Error( 'forbidden', "記事 {$post_id} を編集する権限がありません。", array( 'status' => 403 ) );
		}
		// 記事に設定されている言語と、渡されたキーが一致しているかを確かめる
		$actual = pll_get_post_language( $post_id, 'slug' );
		if ( $actual !== $lang ) {
			return new WP_Error( 'language_mismatch', "記事 {$post_id} の言語は '{$actual}' ですが '{$lang}' として渡されました。", array( 'status' => 409 ) );
		}
		$clean[ $lang ] = $post_id;
	}

	pll_save_post_translations( $clean );

	// 保存できたかを読み直して返す
	$first = (int) reset( $clean );
	return rest_ensure_response(
		array(
			'id'           => $first,
			'lang'         => pll_get_post_language( $first, 'slug' ),
			'translations' => array_map( 'intval', pll_get_post_translations( $first ) ),
		)
	);
}

実際に使っているプラグインでは、Polylangの関数が存在するかの確認と、記事の存在チェックも入れています。紐付けの状態を読むためのGETのエンドポイントも、同じ形でもう1本用意しました。

言語の取り違えを止めるチェックを入れる

結論:記事の実際の言語と渡されたキーが違ったら、紐付けずにエラーで止めます。

pll_save_post_translations()は、渡された組をそのまま保存します。日本語の記事をenとして渡すと、投稿一覧の言語の表示がおかしくなり、管理画面から手で直すことになります。

ローカルで、日本語の記事と英語の記事のIDをわざと逆にして渡すと、409で止まりました。

409 language_mismatch 記事 6 の言語は 'en' ですが 'ja' として渡されました。

呼び出す側のコード

結論:WordPressのアプリケーションパスワードでBasic認証し、日本語と英語の記事IDの組をPOSTします。

import base64
import requests

token = base64.b64encode(f"{USERNAME}:{APP_PASSWORD}".encode()).decode()
headers = {"Authorization": f"Basic {token}"}

# 英語記事は ?lang=en を付けて作る(無料版でも英語の記事になる)
en = requests.post(f"{SITE}/wp-json/wp/v2/posts", params={"lang": "en"},
                   json={"title": "English post", "content": "...", "status": "draft"},
                   headers=headers, timeout=60).json()

# 日本語記事と紐付ける
r = requests.post(f"{SITE}/wp-json/rinblog-poster/v1/translations",
                  json={"translations": {"ja": ja_id, "en": en["id"]}},
                  headers=headers, timeout=30)
print(r.status_code, r.json())

ローカルで試したときの戻り値は{"id": 4, "lang": "ja", "translations": {"ja": 4, "en": 5}}でした。英語記事の側から読み直しても、{"en": 5, "ja": 4}と同じグループに入っていました。

英語記事を入稿するときの手順

結論:日本語記事を作る → 英語記事を?lang=enで作る → 紐付ける → 読み直して確かめる、の4段階です。

▼このブログの英語記事の入稿手順

① 日本語記事を作る(言語を指定しなければ既定の日本語になる)

② 英語記事を?lang=en付きで作る

③ 紐付けのエンドポイントに{"ja": 日本語のID, "en": 英語のID}を送る

④ GETのエンドポイントで両方の記事の言語と紐付けを読み直す

④を省かないのは、無料版のレスポンスに言語が出ないからです。作成のレスポンスだけでは、英語の記事として作られたかどうかが分かりません。

英語記事の公開状態は、日本語記事の状態に合わせています。REST APIで記事の状態を変えるときに気をつけることは、WordPressのREST API更新で記事が公開される2つの条件にまとめました。

Proを買わずに自作を選んだ理由

結論:必要だったのは翻訳の紐付けだけで、そのためのコードは数十行で済んだからです。

Proを入れれば、translationsフィールドで作成と同時に紐付けられます。一方、このブログで足りなかったのは紐付けの1操作だけでした。作成時の言語指定は無料版で動き、紐付けはpll_save_post_translations()を呼ぶだけで済みます。

観点 自作のエンドポイント Polylang Pro
紐付け 別のリクエストで行う 作成と同時に行える
レスポンスの言語 GETのエンドポイントで読む langtranslationsが出る
保守 Polylangの更新時に動作確認が要る 公式の機能として保守される

自作の弱点は保守です。関数名が変わったら動かなくなるので、プラグインは関数の存在を確かめ、無ければ500で理由を返すようにしています。記事の入稿を1人で回している規模なら、この形で困っていません。

英語版の記事をどういう方針で書いているかは、雑記ブログの方針と記事の考え方まとめの「英語版の記事について」に書いています。

 

まとめ

▼要点

① 無料版でも、作成時の?lang=enで英語の記事になる(ドキュメントには書かれていない)

② 無料版では、翻訳の紐付け・レスポンスの言語・一覧の絞り込みはできない

③ 紐付けはpll_save_post_translations()を呼ぶエンドポイントを1本足せば補える

言語の取り違えは409で止め、作ったあとは必ず読み直す

無料版のREST APIは、ドキュメントで想像するより少しだけ多くのことができます。ただ、その「少しだけ」は案内されていない動きなので、Polylangを更新したら入稿の結果を読み直して確かめています。

※WordPress 7.1とPolylang 3.8.9(無料版)をローカル環境(WordPress Playground CLI)で、本番のrinblog.orgは読み取りだけで、2026年9月12日に確認した内容です。

ABOUT ME
りん
このブログでは、Web開発やプログラミングに関する情報を中心に、私が日々感じたことや学んだことをシェアしています。技術と生活の両方を楽しめるブログを目指して、日常で触れた出来事や本、グルメの話題も取り入れています。気軽に覗いて、少しでも役立つ情報や楽しいひとときを見つけてもらえたら嬉しいです。