Checking Translations Before They Are Saved
On this page
#What it does
By default a translation is saved as soon as the engine returns a well-formed answer. The plugin
checks that the answer is valid JSON and that its fields match the ones that were sent — it does not
read the text. If the model mistranslates a term, the mistranslation goes live.
The latwaitp_translated_strings filter is the point where your site can look at the finished
translation before it is written, and either correct it or reject it. It is part of the free
plugin.
Typical uses:
- terminology your glossary cannot express — the glossary says “use X”, not “never use Y”;
- integrity checks, e.g. every measurement in the source must survive into the translation;
- a search-and-replace your editors keep applying by hand after every translation.
What it cannot do. These are text checks, not comprehension. A translation that reverses the
meaning of a sentence while keeping every number, tag and glossary term intact will pass every
deterministic rule you write here. Catching that needs a second AI call, which this filter lets you
make, but does not make for you.
#Where it runs
engine answers → JSON parsed → latwaitp_translated_strings → post, term or strings saved
It runs for post translations, taxonomy term translations and interface string batches, with every
engine and in every processing mode, in the same background process that saves the translation.
In delta mode (“Translate only changed strings”, PRO) the filter only ever sees the fields that
were actually sent to the AI. The unchanged parts of the post keep their existing translation and
never reach your code — so a check that needs to see the whole post should load the translated post
itself rather than assume $strings is complete. An update that turned out to need no translation
at all does not call the filter.
#The filter
add_filter( 'latwaitp_translated_strings', function ( $strings, $translation ) {
return $strings;
}, 10, 2 );
$strings is an array of field name => translated text. The field names are the plugin’s own:
post_title, post_excerpt, content_1, meta_1, builder_1 for posts (pll_1 and so on on
Polylang Pro), name and description for terms, string_… for interface strings.
$translation is the job’s row in the plugin’s queue:
| Key | Meaning |
|---|---|
id |
Row id, as shown on the History screen. |
element_type |
pll_post_job for posts, term for taxonomy terms, pll_string_job for interface strings. |
source_post_id |
The original post, or the original term for term. 0 for interface strings. |
source_lang |
Language slug translated from, e.g. en. |
target_lang |
Language slug translated into, e.g. de. |
model, provider |
The model and engine this job was actually sent to. |
translation_type |
on-demand or auto, with -delta or -delta-context for an update that sent only what changed. |
source_strings_json |
JSON of the fields that make up the job, each with the source text in data. Not set for terms. |
The source text of a field:
$source = json_decode( (string) $translation['source_strings_json'], true );
$text = isset( $source[ $field ]['data'] ) ? $source[ $field ]['data'] : '';
#Returning a corrected translation
Return the array with whatever changes you want saved.
// Editors kept fixing this one word by hand after every translation.
add_filter( 'latwaitp_translated_strings', function ( $strings, $translation ) {
if ( $translation['target_lang'] !== 'de' ) {
return $strings;
}
foreach ( $strings as $field => $translated ) {
$strings[ $field ] = str_replace( 'Bodensatz', 'Sediment', $translated );
}
return $strings;
}, 10, 2 );
Do not remove a field to skip it. The translated post is rebuilt from the original, so a title,
excerpt, paragraph or Elementor text missing from the array is written back in the source
language. Fields you add are ignored — there is no place in the post to put a field that was never
part of the job.
#Rejecting a translation
Return a WP_Error — or an empty array — and nothing is saved. The job is marked failed, the
post keeps whatever translation it had before, and the reason is stored on the job and written to
the log.
Anything other than an array is treated as a rejection too, so always return $strings from a
branch that does not apply.
// Words that must never appear, per language.
add_filter( 'latwaitp_translated_strings', function ( $strings, $translation ) {
$banned = array(
'de' => array( '/bBodensatzb/u' ),
'it' => array( '/bspremiagrumib/iu' ),
);
if ( empty( $banned[ $translation['target_lang'] ] ) ) {
return $strings;
}
foreach ( $strings as $field => $translated ) {
foreach ( $banned[ $translation['target_lang'] ] as $pattern ) {
if ( preg_match( $pattern, $translated ) ) {
return new WP_Error(
'mysite_banned_term',
sprintf( 'Banned term in field "%s" (%s)', $field, $pattern )
);
}
}
}
return $strings;
}, 10, 2 );
// Every measurement in the source must appear in the translation.
add_filter( 'latwaitp_translated_strings', function ( $strings, $translation ) {
$source = json_decode( (string) $translation['source_strings_json'], true );
$measurement = '/d+(?:[.,]d+)?s?(?:mm|cm|m|ml|l|g|kg|%)b/u';
foreach ( $strings as $field => $translated ) {
$text = isset( $source[ $field ]['data'] ) ? $source[ $field ]['data'] : '';
preg_match_all( $measurement, wp_strip_all_tags( $text ), $in_source );
preg_match_all( $measurement, wp_strip_all_tags( $translated ), $in_translation );
if ( count( $in_source[0] ) !== count( $in_translation[0] ) ) {
return new WP_Error(
'mysite_measurements_lost',
sprintf(
'Field "%s": %d measurements in the source, %d in the translation.',
$field,
count( $in_source[0] ),
count( $in_translation[0] )
)
);
}
}
return $strings;
}, 10, 2 );
A rejected job is not retried. It stays failed until someone translates the post again, and
that sends the same prompt again: nothing tells the model what you objected to, so a model that made
a deliberate word choice will usually make it again — at your provider’s expense. Rejection is best
used for mistakes a model makes occasionally (a dropped number, a truncated field). For a word the
model keeps choosing, correct it in the filter, add the preferred term to the glossary, or say so in
the translation prompt.
#Where to check on it
- AI Translation → History shows the job as
failed, with your message in the Error column. - The plugin log (AI Translation → Logs) records
Response processing failedwith the error
message.