The Content Translation experiment adds AI-powered block translation to the WordPress post editor. It provides a "Generate Translation" button in the post status panel, lets users choose a target language, and translates eligible blocks in the post one batch at a time. The experiment registers a WordPress Ability (ai/content-translation) that can be called from the editor UI or directly through the REST API.
When enabled, the Content Translation experiment adds a "Generate Translation" button to the post status panel in the WordPress post editor. Clicking the button opens a modal where users choose the target language and can optionally translate the post title. The experiment then translates supported text blocks and applies the translated content back to each block.
Key Features:
- One-click access from the post status panel
- Language picker for supported target languages
- Optional post title translation
- Block-by-block translation for
core/paragraphandcore/heading - Batch processing with progress shown in the button label
- Partial success handling: failed blocks are counted and reported without discarding successful translations
- User-initiated retries for failed title and block translations; only failed translations are retried
- Blocks below the minimum content length are skipped before a request is made, and reported separately from failures
The experiment consists of three main components:
- Experiment Class (
WordPress\AI\Experiments\Content_Translation\Content_Translation): handles registration, asset enqueuing, localized editor settings, and ability registration. - Ability Class (
WordPress\AI\Abilities\Content_Translation\Content_Translation): implements the translation logic through the WordPress Abilities API. - Languages Class (
WordPress\AI\Experiments\Content_Translation\Languages): defines the supported target language list and exposes it to both PHP and JavaScript.
The ability is block-agnostic: it translates any content string sent to it. The shipping editor UI limits translation to paragraph and heading blocks.
array(
'type' => 'object',
'properties' => array(
'post_id' => array(
'type' => 'integer',
'sanitize_callback' => 'absint',
'description' => 'The ID of the post to translate content for.',
),
'content' => array(
'type' => 'string',
'description' => 'The block content to translate.',
),
'target_language' => array(
'type' => 'string',
'enum' => Languages::get_codes(),
'default' => Languages::get_default_target_language(),
'sanitize_callback' => 'sanitize_key',
'description' => 'The target language for translation.',
),
),
)The ability returns a string with translated content:
array(
'type' => 'string',
'description' => 'The translated content.',
)The default target language is en-us (English US). The supported language list is:
ar- Arabicbn- Bengalizh-cn- Chinese (Simplified)zh-tw- Chinese (Traditional)nl-nl- Dutchen-gb- English (UK)en-us- English (US)fr-fr- Frenchde-de- Germanhi- Hindiid- Indonesianit-it- Italianja- Japaneseko- Koreanpl-pl- Polishpt-br- Portuguese (Brazil)pt-pt- Portuguese (Portugal)ru-ru- Russianes-es- Spanishsv-se- Swedishtr-tr- Turkishuk- Ukrainianvi- Vietnamese
The list is filterable with wpai_content_translation_languages.
The ability checks permissions based on the input:
-
If
post_idis provided:- Verifies the post exists; returns
post_not_foundotherwise. - Checks
current_user_can( 'edit_post', $post_id ). - Requires the post type to have
show_in_restenabled.
- Verifies the post exists; returns
-
If
post_idis not provided:- Checks
current_user_can( 'edit_posts' ).
- Checks
POST /wp-json/wp-abilities/v1/abilities/ai/content-translation/run
You can authenticate using either:
- Application Password (Recommended)
- Cookie Authentication with Nonce
See TESTING_REST_API.md for detailed authentication instructions.
curl -X POST "https://yoursite.com/wp-json/wp-abilities/v1/abilities/ai/content-translation/run" \
-u "username:application-password" \
-H "Content-Type: application/json" \
-d '{
"input": {
"content": "Our new platform helps teams collaborate more effectively, share files securely, and track progress in real time.",
"target_language": "fr-fr",
"post_id": 123
}
}'curl -X POST "https://yoursite.com/wp-json/wp-abilities/v1/abilities/ai/content-translation/run" \
-u "username:application-password" \
-H "Content-Type: application/json" \
-d '{
"input": {
"content": "Read the <a href=\"https://example.com\">launch notes</a> before publishing.",
"target_language": "es-es"
}
}'The system instruction tells the model to preserve inline HTML such as links, emphasis, and code.
curl -X POST "https://yoursite.com/wp-json/wp-abilities/v1/abilities/ai/content-translation/run" \
-u "username:application-password" \
-H "Content-Type: application/json" \
-d '{
"input": {
"content": "Translate this sentence using the default target language."
}
}'When target_language is omitted, the ability uses en-us.
const response = await fetch(
'/wp-json/wp-abilities/v1/abilities/ai/content-translation/run',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': wpApiSettings.nonce, // If using cookie auth
},
credentials: 'include', // Include cookies for authentication
body: JSON.stringify( {
input: {
content: blockContent,
target_language: 'pt-br',
post_id: postId,
},
} ),
}
);import apiFetch from '@wordpress/api-fetch';
const translated = await apiFetch({
path: '/wp-abilities/v1/abilities/ai/content-translation/run',
method: 'POST',
data: {
input: {
content: blockContent,
target_language: 'pt-br',
post_id: postId,
},
},
});The ability may return the following error codes:
content_not_provided-contentwas missing or empty.content_too_short- Content has fewer than the minimum number of characters, counted excluding whitespace and after stripping HTML. Defaults to 5 characters; see Adjusting the Minimum Content Length.invalid_target_language-target_languageis not in the supported language list.post_not_found- Apost_idwas supplied but the post does not exist.insufficient_permissions- Caller lacksedit_post(withpost_id) oredit_posts(without).no_results- The AI client did not return translated text.- A WP_Error from
ensure_text_generation_supported()if no connected provider supports text generation.
Use the wpai_content_translation_languages filter to replace or extend the target language list:
add_filter( 'wpai_content_translation_languages', function ( array $languages ): array {
$languages['sv'] = __( 'Swedish', 'my-plugin' );
return $languages;
} );The filtered list is used for the ability schema, PHP validation, and the editor language picker.
Language codes are normalized with sanitize_key(), and entries whose label is not a non-empty string are discarded. A filter that returns something other than an array is ignored.
The ability rejects content below a minimum character count (excluding whitespace, after stripping HTML). The default is 5 characters. Because the shared wpai_min_content_length filter applies to every feature, branch on the feature ID to change it for translation alone:
add_filter( 'wpai_min_content_length', function ( int $length, string $feature_id ): int {
return 'content-translation' === $feature_id ? 20 : $length;
}, 10, 2 );The same value is passed to the editor, which uses it to decide whether the Generate Translation button is available and which blocks to send. Raising it means more blocks are skipped as too short.
The ability uses the standard prompt filters:
wpai_content_translation_prompt— filters the assembled user prompt. Receives the prompt and the resolved target language name.wpai_content_translation_prompt_builder— filters the configured prompt builder before generation support is verified. Receives the builder, the prompt, and the resolved target language name.
add_filter( 'wpai_content_translation_prompt_builder', function ( $builder, $prompt, $language ) {
return $builder->using_temperature( 0.2 );
}, 10, 3 );See PROMPT_CUSTOMIZATION.md for details.
Note that this ability deliberately does not inject the site's editorial guidelines. Translation must reproduce the source faithfully, and copy guidelines would instruct the model to restyle the text rather than translate it.
-
Enable the experiment:
- Go to
Settings -> AI - Toggle Content Translation to enabled
- Ensure you have valid AI credentials configured
- Go to
-
Test in the editor:
- Create or edit a post with enough post content to meet the minimum length
- Open the post sidebar and click Generate Translation
- Choose a target language
- Toggle Also translate the title and click Translate
- Verify the title updates when the toggle is enabled
- Verify paragraph and heading blocks are replaced with translated text
- Verify the button shows progress while blocks are translating
-
Test disabled states:
- Disable all experiments and verify the translation UI is hidden
- Disable only Content Translation and verify the translation UI is hidden
- Use content shorter than the minimum length and verify the button is disabled
-
Test short and unsupported content:
- Add a heading shorter than the minimum length (for example
FAQ) alongside a long paragraph, then translate; verify the heading is left untouched and reported as skipped rather than failed - Translate a post whose only content is an unsupported block type (a Code block, say); verify the "No translatable content found in the post." notice
- Open the modal with an empty or short title; verify the informational notice appears and "Also translate the title" is disabled. Close the modal, lengthen the title, reopen it, and verify the notice is absent and the option is enabled.
- Add a heading shorter than the minimum length (for example
-
Test REST API:
- Use curl or Postman to test the REST endpoint
- Test each supported language code
- Verify
invalid_target_languagefor an unsupported code - Verify
post_not_foundand permission errors when using invalid or inaccessible posts
- The experiment requires a configured AI connector/provider that supports text generation.
- Users must have
edit_postwhen invoking with apost_id, oredit_postswhen invoking without one.
- The content sent to the model is wrapped in
<content>tags. - The result is sanitized with
wp_kses_post(). - Blocks below the minimum content length are filtered out in the editor before any request is made, so a short heading does not cost a request or get reported as a failure. The post title is checked the same way when title translation is enabled.
- The post-level check that enables the Generate Translation button measures the whole post, while the minimum applies per block. A long post can therefore contain individual blocks that are skipped.
The system instruction guides the AI to:
- Translate into the selected target language.
- Return only the translated text.
- Avoid preamble, explanation, or commentary.
- Preserve inline HTML and the original format where possible.
- Maintain the original perspective and voice.
- The editor UI only translates paragraph and heading blocks.
- There is no batch REST endpoint; the editor performs multiple ability calls in batches of 4.
- Translations are generated in real time and are not cached.
- Failed block translations are skipped; successful blocks remain applied. Failures and length skips are reported together in a single warning notice, and the progress counter reflects blocks actually translated.
- The UI replaces the current block content directly, so users should review changes before saving.
- Each block update is its own undo step, so reverting a whole translation takes multiple undos.