Polylang REST and Headless
Use this skill when a client, plugin, or importer talks to WordPress REST and expects language-aware content.
Core Polylang and Polylang Pro behave differently:
| Stack | REST behavior |
|---|---|
| Polylang core | Defines current language from lang; exposes pll/v1/languages and settings controllers. |
| Polylang Pro | Adds lang and translations REST fields to translated posts/terms; filters collections by lang; adds translation endpoints. |
| Polylang for WooCommerce | Replaces generic Pro handling for products/orders/product taxonomies with Woo-aware handlers. |
Current language in REST
Polylang core reads a lang request parameter during REST dispatch:
GET /wp-json/wp/v2/posts?lang=fr
If lang is valid, pll_current_language() returns that language during the request. If an invalid lang is sent and a default language exists, Polylang falls back to the default language.
Do not rely on URL prefixes alone in custom REST routes. Read the language through Polylang:
register_rest_route( 'myplugin/v1', '/cards', array(
'methods' => WP_REST_Server::READABLE,
'permission_callback' => '__return_true',
'callback' => static function ( WP_REST_Request $request ) {
$lang = function_exists( 'pll_current_language' ) ? pll_current_language() : false;
if ( ! $lang && function_exists( 'pll_default_language' ) ) {
$lang = pll_default_language();
}
return rest_ensure_response( array(
'lang' => $lang ?: get_locale(),
) );
},
) );
For state-changing custom routes, still use normal REST permissions and nonces/auth. Language is not authorization.
Languages endpoint
Polylang core registers:
GET /wp-json/pll/v1/languagesPOST /wp-json/pll/v1/languagesGET|PUT|PATCH|DELETE /wp-json/pll/v1/languages/<term_id>GET /wp-json/pll/v1/languages/<slug>
The response schema includes fields such as term_id, name, slug, locale, w3c, is_rtl, flag_url, is_default, active, home_url, search_url, page_on_front, page_for_posts, fallbacks, and term_props.
Use this endpoint for language selector bootstrapping in headless clients. Do not hardcode language lists in JS.
Filterable REST routes
Core Polylang detects REST routes for translated show_in_rest post types and taxonomies, plus wp/v2/search. It exposes the filter list to editor scripts and can add query parameters to preload paths.
The filter is:
add_filter( 'pll_filtered_rest_routes', static function ( array $routes ): array {
$routes['myplugin_item'] = 'myplugin/v1/items';
return $routes;
} );
Use this only for collection routes that accept a lang parameter and return language-filterable objects. Do not mark single-object routes ending with an ID as collection filters.
Polylang Pro fields
Polylang Pro 3.8 registers REST fields on translated REST-enabled post types and taxonomies:
{
"lang": "en",
"translations": {
"en": 123,
"fr": 456
}
}
The lang field is a string enum of language slugs. The translations field is an object whose keys are slugs and values are IDs.
Write pattern:
PUT /wp-json/wp/v2/book/456
Content-Type: application/json
X-WP-Nonce: ...
{
"lang": "fr",
"translations": {
"en": 123
}
}
The Pro update callback checks language validity and translation capabilities. Invalid language returns rest_invalid_language_code; insufficient translation permission returns rest_cannot_set_language.
REST type filters in 3.8
Polylang Pro filters the REST-enabled object types it manages:
add_filter( 'pll_rest_api_post_types', static function ( array $post_types ): array {
$post_types[] = 'book';
return array_values( array_unique( $post_types ) );
} );
add_filter( 'pll_rest_api_taxonomies', static function ( array $taxonomies ): array {
$taxonomies[] = 'genre';
return array_values( array_unique( $taxonomies ) );
} );
In 3.8 the legacy format with options arrays is deprecated. Return a simple indexed array of post type or taxonomy names. The Pro source sanitizes legacy formats but calls _deprecated_argument().
Custom REST controllers
Polylang's request helper can infer object type from WP_REST_Posts_Controller or WP_REST_Terms_Controller. For a custom controller that does not extend those, identify the object type:
add_filter(
'pll_rest_request_object_type',
static function ( $type, array $handler, WP_REST_Request $request ) {
if ( str_starts_with( $request->get_route(), '/myplugin/v1/books' ) ) {
return 'post';
}
return $type;
},
10,
3
);
Accepted return values are post, term, or null.
Pro translation endpoints
Polylang Pro 3.8 adds:
POST /wp-json/pll/v1/translation
Required body:
{
"from_post": 123,
"lang": "fr",
"action": "duplicate"
}
It duplicates a translatable post to the target language through Pro's sync model. Permissions require edit_posts, read_post on the source, and the ability to translate into the requested language.
Untranslated posts endpoint:
GET /wp-json/pll/v1/untranslated-posts?type=book&lang=en&untranslated_in=fr&search=foo
Required query params are type, lang, and untranslated_in. This endpoint is useful for editor UIs and headless translation management.
Its permission check allows view-context reads and only enforces the post type edit capability for context=edit. Do not proxy it as a private management endpoint without your own permission layer.
WooCommerce REST
Polylang for WooCommerce 2.2.2 requires Polylang Pro 3.8+ for its REST module.
It removes generic Pro handling for:
productproduct_variationshop_orderproduct_catproduct_tagproduct_brandproduct_attribute_term
Then it registers Woo-aware handlers. For products:
langandtranslationsare exposed on Woo REST product objects.- SKU and global unique ID checks are language-aware during REST writes.
- Batch product create reads
langfrom eachcreateitem through an internal FIFO queue.
Example:
POST /wp-json/wc/v3/products
Content-Type: application/json
Authorization: Basic ...
{
"name": "Blue shirt",
"type": "simple",
"lang": "en",
"translations": {
"fr": 456
}
}
For orders, Polylang WC adds lang to Woo order REST object queries:
GET /wp-json/wc/v3/orders?lang=fr
When HPOS is enabled, its HPOS query filter adds language JOIN/WHERE clauses for translated order types.
Headless rules
- Always pass
langexplicitly in REST collection reads. - Treat empty
langdifferently from missinglang: Polylang WC restores explicitlang => ''in Woo queries to mean all languages. - Use language endpoint data for switchers and route generation.
- For writes, send both
langandtranslationsonly when the user has translation capability. - Do not use
langto bypass object permissions. Keep normal REST permission checks. - Do not return cached REST HTML/JSON without varying by language, user, and auth state where applicable.
Common mistakes
- Expecting core Polylang alone to add
langandtranslationsfields to posts. That is Pro behavior. - Returning legacy option arrays from
pll_rest_api_post_typesorpll_rest_api_taxonomieson 3.8+. - Filtering single-object routes by language as if they were collections.
- Creating Woo products through REST without
lang, then trying to repair language from SKU later. - Assuming Woo batch update/delete need language queue. Polylang WC's queue is for batch create items because those objects do not exist yet.
Cross-references
- Use
wp-rest-apifor generic route security, nonce, schema, and permission rules. - Use
polylang-object-translationsfor PHP-side linking after imports. - Use
polylang-wc-compatibilityfor product/order language data store behavior.
Verification
Local source checked against:
- REST language definition:
wp-content/plugins/polylang/src/rest-request.php - REST request helper and
pll_rest_request_object_type:src/modules/REST/Request.php - Languages endpoint schema/routes:
src/modules/REST/V1/Languages.php - Pro REST fields and filters:
polylang-pro/src/modules/rest/* - Woo REST module and batch queue:
polylang-wc/src/modules/REST/*
References
- Official documentation: https://polylang.pro/doc/rest-api/
- Official documentation: https://polylang.pro/doc/function-reference/
- Verified source paths:
wp-content/plugins/polylang/src/rest-request.phpwp-content/plugins/polylang/src/modules/REST/Request.phpwp-content/plugins/polylang/src/modules/REST/API.phpwp-content/plugins/polylang/src/modules/REST/V1/Languages.phpwp-content/plugins/polylang/src/filter-rest-routes.phpwp-content/plugins/polylang-pro/src/modules/rest/rest-api.phpwp-content/plugins/polylang-pro/src/modules/rest/Translatable/Abstract_object.phpwp-content/plugins/polylang-pro/src/modules/rest/Translated/Abstract_Object.phpwp-content/plugins/polylang-pro/src/modules/rest/V1/Translation.phpwp-content/plugins/polylang-pro/src/modules/rest/V1/Untranslated_Posts.phpwp-content/plugins/polylang-wc/src/modules/REST/Module.phpwp-content/plugins/polylang-wc/src/modules/REST/Translated/Product.phpwp-content/plugins/polylang-wc/src/modules/REST/Translated/Batch.phpwp-content/plugins/polylang-wc/src/modules/REST/Filtered/Order.php