Checking Translations Before They Are Saved
On this page
#What it does
By default a translation is saved as soon as the AI 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 latwaitr_translated_strings filter is the point where your site can look at the finished
translation before WPML writes it, and either correct it or reject it.
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
AI answers → JSON parsed → fields matched to WPML tids → latwaitr_translated_strings → WPML saves
It runs for post translations and String Translation batches, in both background and synchronous
mode, and on the same cron process that saves the translation. Taxonomy term translations do not
go through it.
In delta mode (“Translate only changed strings”) 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 target post
itself rather than assume $strings is complete.
#The filter
add_filter( 'latwaitr_translated_strings', function ( $strings, $context ) {
return $strings;
}, 10, 2 );
$strings is an array of field name => translated text. The field names are WPML’s
(title, body, field-<something> for page builders, and so on).
$context describes the job:
| Key | Type | Meaning |
|---|---|---|
source |
array | The same field names, holding the source text that was sent. |
source_lang |
string | Language code translated from, e.g. en. |
target_lang |
string | Language code translated into, e.g. de. |
source_post_id |
int | The original post. 0 for String Translation batches. |
element_type |
string | ts_job for posts, ts_string_job for String Translation batches. |
model |
string | The model this job was actually sent to. |
wpml_job_id |
int | WPML translation job id. |
translation_id |
int | Row id in the plugin’s queue table, as shown on the Translations screen. |
#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( 'latwaitr_translated_strings', function ( $strings, $context ) {
if ( $context['target_lang'] !== 'de' ) {
return $strings;
}
foreach ( $strings as $field => $translated ) {
$strings[ $field ] = str_replace( 'Bodensatz', 'Sediment', $translated );
}
return $strings;
}, 10, 2 );
Fields you remove from the array are not written: the field keeps the translation it already had,
rather than being emptied. Fields you add are ignored — WPML has no place to put a field that was
never part of the job. A return value that is not an array is ignored, and the model’s translation
is saved unchanged.
#Rejecting a translation
Return a WP_Error and nothing is saved. The job is then treated exactly like a failed API call:
it is sent to the model again, up to three attempts in total, and parked as failed after that.
The post keeps whatever translation it had before, and the reason is stored on the queue row and
written to the log.
// Words that must never appear, per language.
add_filter( 'latwaitr_translated_strings', function ( $strings, $context ) {
$banned = array(
'de' => array( '/bBodensatzb/u' ),
'it' => array( '/bspremiagrumib/iu' ),
'hr' => array( '/bpresudab/iu' ),
);
if ( empty( $banned[ $context['target_lang'] ] ) ) {
return $strings;
}
foreach ( $strings as $field => $translated ) {
foreach ( $banned[ $context['target_lang'] ] as $pattern ) {
if ( preg_match( $pattern, $translated ) ) {
return new WP_Error(
'latwaitrp_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( 'latwaitr_translated_strings', function ( $strings, $context ) {
$measurement = '/d+(?:[.,]d+)?s?(?:mm|cm|m|ml|l|g|kg|%)b/u';
foreach ( $strings as $field => $translated ) {
$source = isset( $context['source'][ $field ] ) ? $context['source'][ $field ] : '';
preg_match_all( $measurement, wp_strip_all_tags( $source ), $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(
'latwaitrp_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 retry 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 and the job will end up failed after
three attempts — 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 Translator → Translations shows the row’s status and the stored reason:
Retrying (attempt 1): Rejected: …, orMax retries exceeded. Last error: Rejected: …. - The plugin log records
Translation rejected by latwaitr_translated_stringswith the field and
the error message, andRetrying failed responsefor each new attempt.