Quality

Preserving Tone and Terminology Across Languages

⏱ 15 min

Lessons 2 and 3 each send one prompt per post to wp_ai_client_prompt(), and that prompt is self-contained, it knows nothing about how any other post was translated. Run either ability against ten posts that all mention your product name or a recurring technical term, and there’s no guarantee the model renders that term the same way twice. One draft might translate a brand name that should stay untouched, another might use two different words for the same technical concept. This lesson fixes that with a glossary fed into every translation prompt through using_system_instruction(), the builder method Course 12 covered in depth as the highest- leverage way to shape a model’s standing behavior.

What you'll learn in this lesson
Why terminology drift happens even with a good translation prompt
Each AI call is independent, nothing carries consistency between them unless you supply it.
Storing a translation glossary as structured data
Per-language terms, both "do not translate" entries and "always translate this way" entries.
Feeding the glossary in through using_system_instruction(), not the prompt text
Keeping standing rules separate from the specific content being translated, exactly Course 12's pattern.
Wiring the glossary into Lesson 2 or 3's translation ability
One small change to the existing generation step, nothing else about the ability changes.
Prerequisites

Course 12’s lesson on using_system_instruction(), this lesson treats that method as already understood and applies it to a specific problem. Either Lesson 2’s WPML ability or Lesson 3’s Polylang ability, since this lesson modifies that generation step rather than building a new ability from scratch.

Step 1: Why one good prompt per post isn’t enough

The translation prompt in Lessons 2 and 3 already asks the model to preserve meaning and tone, and a capable model will generally do that well within a single call. What it can’t do on its own is remember that a different post, translated an hour earlier in a separate request, rendered a particular term a specific way. Every call to wp_ai_client_prompt() starts fresh. Left alone, this produces small, inconsistent variations across a site’s translated content, exactly the kind of thing a bilingual reader notices even when each individual sentence reads fine.

This isn't a model quality problem

A stronger model doesn’t fix this by itself, since the issue isn’t translation quality within one call, it’s the absence of any shared state between calls. The fix has to be supplying that shared state yourself, a glossary, on every call.

Step 2: Store the glossary as structured data, not a paragraph of prose

A glossary works best as data your code can query and grow, not as a string manually re-edited by hand. Store it as a wp_option, an array keyed by source term, each entry recording whether the term should ever be translated at all and, if so, its exact rendering per target language.

// File: translation-glossary/translation-glossary.php
function translation_glossary_get() {
	return get_option( 'translation_glossary', array(
		'Acme Sync' => array(
			'do_not_translate' => true, // A product name, never translate this string, in any language.
		),
		'webhook' => array(
			'do_not_translate' => false,
			'es' => 'webhook', // Commonly left untranslated in Spanish technical writing too.
			'fr' => 'webhook',
		),
		'dashboard' => array(
			'do_not_translate' => false,
			'es' => 'panel de control',
			'fr' => 'tableau de bord',
		),
	) );
}

function translation_glossary_build_instruction_block( $target_language ) {
	$lines = array();

	foreach ( translation_glossary_get() as $term => $rule ) {
		if ( ! empty( $rule['do_not_translate'] ) ) {
			$lines[] = "- Never translate \"{$term}\", keep it exactly as written in every language.";
			continue;
		}

		if ( isset( $rule[ $target_language ] ) ) {
			$lines[] = "- Always translate \"{$term}\" as \"{$rule[ $target_language ]}\" in this language, never a synonym.";
		}
	}

	return implode( "\n", $lines );
}

Keeping the glossary in a wp_option means an editor can grow it over time (a new product name, a newly recurring technical term) without touching any PHP, and every translation call from this point forward reads the same, current list.

Step 3: Feed the glossary through using_system_instruction(), not the prompt text

The glossary is standing behavior, it applies to this translation and every other one, which is exactly the distinction Course 12 draws between using_system_instruction() and with_text(). The post content being translated belongs in with_text(), the glossary and tone rules belong in using_system_instruction().

// File: translation-glossary/translation-glossary.php
function translation_glossary_translate_with_terminology( $source, $target_language ) {
	$glossary_block = translation_glossary_build_instruction_block( $target_language );

	$system_instruction =
		// Role
		"You are translating WordPress content into the language identified by the code " .
		"\"{$target_language}\", preserving meaning, tone, and paragraph structure. " .
		// Constraints, including the glossary
		"Follow these terminology rules exactly:\n{$glossary_block}\n" .
		"Do not summarize or shorten the content. " .
		// Output shape
		'Return the translated title on the first line prefixed with "Title: ", then the ' .
		'translated body after a blank line.';

	$response = wp_ai_client_prompt()
		->with_text( "Title: {$source->post_title}\nContent: {$source->post_content}" )
		->using_system_instruction( $system_instruction )
		->using_model_preference( 'anthropic/claude', 'openai/gpt', 'google/gemini' )
		->generate_text();

	return $response;
}

Notice the glossary rules live inside using_system_instruction() alongside the role and output shape instructions, following the same role, constraints, output-shape ordering Course 12 recommends, while the actual post content stays in with_text(). Swap this function in for the prompt-building step inside Lesson 2 or Lesson 3’s _create_translation() helper, everything else in either ability, the duplicate guard, the wp_insert_post() call, the WPML or Polylang linking, stays exactly the same.

Step 4: Keep the glossary short and specific

Only add terms that have actually drifted, or are likely to
Brand names, product names, and recurring technical terms, not every noun in your content.
Prefer "do not translate" entries for anything with legal or trademark weight
Product names and registered terms should read identically across every language version of your site.
Review the glossary per language, not once for the whole site
A term correctly left untranslated in French might have a well-established native equivalent in another language.
Let this feed Lesson 7's review stage too
A bilingual reviewer checking a translation against this same glossary catches drift a prompt alone can't guarantee against.

Test it

Translate two different posts that both mention the same glossary term, and confirm it renders identically in both:

wp-env run cli wp eval '
$post_a = get_post( 10 );
$post_b = get_post( 55 );
echo translation_glossary_translate_with_terminology( $post_a, "es" );
echo "\n---\n";
echo translation_glossary_translate_with_terminology( $post_b, "es" );
'

Read both outputs specifically for how each one renders the glossary term, not just for overall translation quality, that’s the specific thing this lesson’s change is meant to fix.

Recap

Terminology drift happens because each wp_ai_client_prompt() call starts with no memory of any other call, and the fix is supplying that shared context yourself: a glossary stored as structured data, built into a plain-language instruction block per target language, and passed into using_system_instruction() alongside the role and output-shape rules, while the actual content stays in with_text(). This is a direct application of Course 12’s system instruction pattern to one specific, recurring translation problem, not a new mechanism of its own.

Resources & further reading

← AI Translation Pipelines With Polylang Locale-Aware SEO and hreflang →