Skip to content

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: …, or Max retries exceeded. Last error: Rejected: ….
  • The plugin log records Translation rejected by latwaitr_translated_strings with the field and
    the error message, and Retrying failed response for each new attempt.