Scale & Reach

Project: Multilingual Translation Workflow

⏱ 15 min

This project builds an ability that takes a post ID and a target language, generates a translated draft with the AI Client SDK, and links the new draft back to its original through post meta, so an editor reviewing translations can always trace one back to the other.

What you'll learn in this lesson
How to generate a translated draft with the AI Client SDK
Producing a full translated post, not just a snippet.
How to link a translation to its source post
Using post meta as a simple, plugin-agnostic relationship.
Why translations are created as drafts, same as generated content
Machine translation needs a fluent-speaker review before it's public, every time.
Where this fits alongside real WordPress i18n plugins
What this project does and doesn't replace.
Prerequisites

Course 5 for wp_ai_client_prompt(). A published post to translate, and clarity that this project builds a standalone translation workflow, not an integration with a specific existing i18n plugin.

Step 1: Register the ability

Input is a post ID and a target language. Output is the new draft’s ID, same as the Blog Post Generator project, this ability never publishes anything either.

// File: translation-workflow/translation-workflow.php
add_action( 'wp_abilities_api_init', function () {
	wp_register_ability(
		'translation-workflow/translate-post',
		array(
			'label'               => __( 'Translate post', 'translation-workflow' ),
			'description'         => __( 'Generates a translated draft of a published post in a target language, linked to the original for review. Always creates a draft, never publishes.', 'translation-workflow' ),
			'category'            => 'content',
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array(
					'post_id'         => array( 'type' => 'integer' ),
					'target_language' => array( 'type' => 'string' ),
				),
				'required'   => array( 'post_id', 'target_language' ),
			),
			'output_schema'       => array(
				'type'       => 'object',
				'properties' => array(
					'translated_post_id' => array( 'type' => 'integer' ),
					'edit_url'           => array( 'type' => 'string' ),
				),
			),
			'permission_callback' => function ( $input ) {
				return current_user_can( 'edit_post', $input['post_id'] );
			},
			'execute_callback'    => 'translation_workflow_translate',
			'meta' => array(
				'annotations' => array( 'destructive' => false ),
				'mcp'         => array( 'public' => true ),
			),
		)
	);
} );

Step 2: Translate, then insert as a linked draft

// File: translation-workflow/translation-workflow.php
function translation_workflow_translate( $input ) {
	$source = get_post( $input['post_id'] );
	if ( ! $source ) {
		return new WP_Error( 'not_found', 'Post not found.' );
	}

	$target_language = sanitize_text_field( $input['target_language'] );

	$prompt = <<<PROMPT
Translate the following blog post into {$target_language}. Preserve the meaning,
tone, and paragraph structure. Do not summarize or shorten it. Return the translated
title on the first line prefixed with "Title: ", then the translated body after a
blank line.

Title: {$source->post_title}
Content: {$source->post_content}
PROMPT;

	$response = wp_ai_client_prompt( $prompt )
		->using_model_preference( 'anthropic/claude', 'openai/gpt', 'google/gemini' )
		->generate_text();

	if ( empty( $response ) ) {
		return new WP_Error( 'empty_response', 'Translation failed, no draft was created.' );
	}

	list( $title, $body ) = translation_workflow_split_title_body( $response, $source->post_title );

	$translated_id = wp_insert_post( array(
		'post_title'   => sanitize_text_field( $title ),
		'post_content' => wp_kses_post( $body ),
		'post_status'  => 'draft', // Machine translation always needs human review before publishing.
		'post_type'    => $source->post_type,
	), true );

	if ( is_wp_error( $translated_id ) ) {
		return $translated_id;
	}

	// Link both directions so either post can be traced to the other.
	update_post_meta( $translated_id, '_translation_of', $source->ID );
	update_post_meta( $translated_id, '_translation_language', $target_language );
	update_post_meta( $source->ID, '_translation_' . sanitize_key( $target_language ), $translated_id );

	return array(
		'translated_post_id' => $translated_id,
		'edit_url'            => get_edit_post_link( $translated_id, 'raw' ),
	);
}

function translation_workflow_split_title_body( $response, $fallback_title ) {
	if ( preg_match( '/^Title:\s*(.+)$/mi', $response, $m ) ) {
		return array( trim( $m[1] ), trim( str_replace( $m[0], '', $response ) ) );
	}
	return array( $fallback_title, $response );
}

_translation_of and _translation_language on the new post, plus a language-specific key back on the source, means a query for “does post 42 already have a French translation” is a single get_post_meta() call in either direction.

Step 3: Where this fits next to real i18n plugins

WordPress’s established multilingual ecosystem, plugins like WPML and Polylang, solve problems this simple meta-based linking doesn’t: language switchers, translated permalinks, taxonomy translation, and syncing menus or widgets across languages. If your site already runs one of those, this ability’s job is narrower and complementary, it generates the first-draft translation text, and the actual cross-language post relationship your i18n plugin manages should be set up through that plugin’s own mechanism rather than duplicated here. The _translation_of meta key in this project is a stand-in, minimal enough to work standalone, but check your specific i18n plugin’s documentation for its real linking mechanism before treating this meta key as a substitute for it in a production multilingual site.

Test it

Translate a real post and confirm the link is set in both directions:

wp-env run cli wp eval '
$result = wp_get_ability( "translation-workflow/translate-post" )->execute( array(
	"post_id"         => 42,
	"target_language" => "Spanish",
) );
print_r( $result );
'
wp-env run cli wp post meta get 42 _translation_spanish

Confirm the second command returns the same post ID as translated_post_id from the first, and open the edit_url to read the translated draft for fluency and accuracy.

Publishing a machine translation without a fluent-speaker review

Even a strong model can mistranslate idioms, get formal/informal register wrong, or mishandle terms that shouldn’t be translated at all (product names, code samples, proper nouns). Because this ability always creates a draft, that review step is structurally required before anything reaches a public, non-English (or non-source- language) audience, don’t build a follow-up automation that auto-publishes these drafts without a real review step in between.

Recap

This ability translates a post’s title and body with wp_ai_client_prompt(), inserts the result as a linked draft using wp_insert_post(), and stores the relationship in post meta in both directions. It’s a starting point for a translation review queue, not a replacement for the taxonomy, routing, and UI features a dedicated i18n plugin provides, use both together on a real multilingual site.

Resources & further reading

← Project: Internal-Linking Automation Project: Analytics Reporter →