Skip to content

Translation Prompts With Glossary Enforcement

13 min read · updated August 4, 2026

Three things go wrong in machine translation that a human translator would never do: a locked term gets a synonym, a placeholder disappears, and a grammatical choice the source never made gets made silently. The prompt below addresses all three, and the gate after it catches what the prompt misses.

The prompt

Translate <source> from {{source_language}} into {{target_language}}.

<glossary> is binding. Each row is: source_term -> required_target_term.
Where a source term from <glossary> appears in <source>, the required target
term must appear in your translation. You may inflect it for grammar. You may
not replace it with a synonym, a more natural phrasing, or a gloss. If a
required term cannot be inflected into a grammatical sentence, restructure the
sentence around the term rather than changing the term.

<do_not_translate> lists strings to reproduce exactly: product names, UI
labels in quotes, code identifiers, URLs, and placeholders such as {count} and
%{name} and %s. Every placeholder in <source> must appear in the translation,
unchanged, exactly once, in a position that is grammatical in
{{target_language}}.

Register: {{register}}
  (for example: "formal, address the reader with Sie"; "informal tu";
   "no contractions"; "no idioms"; "second person plural")

Rules:
- Translate meaning, not word order. A translation that preserves the source
  syntax and reads as foreign has failed.
- Add nothing the source does not carry: no clarifying apposition, no
  explanatory relative clause, no politeness the source does not have.
- Where {{target_language}} forces a distinction {{source_language}} does not
  make - grammatical gender, formality, singular they, inclusive we - choose
  according to Register and list the choice in "forced_choices".
- Do not localise numbers, dates, units or currencies unless <localise> says
  to. A number that changes format is a bug in most pipelines.
- Where a sentence has two readings that translate differently, translate the
  more likely one and record both in "ambiguities".

Return JSON:
{"translation": "...",
 "glossary_used": [{"source": "...", "target": "...", "inflected_as": "..."}],
 "forced_choices": [{"what": "...", "chose": "...", "because": "..."}],
 "ambiguities": [{"span": "...", "readings": ["...", "..."], "chose": "..."}]}

<glossary>
{{glossary_rows}}
</glossary>

<do_not_translate>
{{protected_strings}}
</do_not_translate>

<source>
{{source}}
</source>

Term locking, and its one hard case

A glossary in a prompt is a soft constraint. The model will honour it most of the time and will quietly substitute a more idiomatic word when the sentence resists — which is exactly when the glossary matters, since terminology exists to be consistent rather than idiomatic.

Two lines in the prompt do the work. “You may inflect it for grammar. You may not replace it with a synonym” separates the permitted transformation from the forbidden one, which a bare “use this term” does not. And the restructuring instruction gives the model an escape that is not substitution: when the term will not fit, change the sentence around it. Without that sentence the model faces a choice between an ungrammatical sentence and a substitution, and it will pick grammar every time.

The hard case is inflecting languages where the required target term must change form — German case, Slavic declension, Finnish, Arabic broken plurals. Your glossary contains the citation form; the translation contains an inflected form; and a naive presence check on the citation form fails. The inflected_as field is how you survive this: the model reports the surface form it actually used, your check verifies that form is present, and a human reviewing the glossary can see whether the inflection is right. It does not prove the inflection is correct — nothing in this pipeline does — but it makes the claim explicit rather than buried in the prose.

Forced choices and ambiguity

English is under-specified relative to most target languages. “You can configure this” does not say whether “you” is one person or many, formal or familiar. “The user opened their account” does not say a gender that German, Spanish, Hebrew and Polish all require.

The translation has to choose. The only question is whether the choice is recorded. forced_choices makes it a listed decision rather than an invisible one, and the list is short enough to review — usually two or three entries per page of source. That review is the entire value: a formality choice that is wrong in every string of your product is a serious problem, and it is invisible in a translation nobody in your team reads.

ambiguities serves the same purpose for meaning rather than grammar. The instruction says translate the more likely reading — not ask, not hedge, not translate both — because a translation pipeline that stops for questions is a translation pipeline nobody uses. The record is what makes that acceptable.

The gates that need no model

Run these before any judged check. They are free, they never disagree with themselves, and between them they catch the failures that break software rather than merely reading badly.

import re
from collections import Counter

PLACEHOLDER = re.compile(r"\{[A-Za-z0-9_]+\}|%\{[A-Za-z0-9_]+\}|%[sd]|:[a-z_]+")
NUMBER = re.compile(r"\d[\d.,]*")

def placeholder_gate(source: str, translation: str) -> dict:
    a, b = Counter(PLACEHOLDER.findall(source)), Counter(PLACEHOLDER.findall(translation))
    return {"missing": sorted((a - b).elements()),
            "extra":   sorted((b - a).elements())}

def number_gate(source: str, translation: str) -> dict:
    "Only valid when <localise> did not ask for number reformatting."
    a, b = Counter(NUMBER.findall(source)), Counter(NUMBER.findall(translation))
    return {"missing": sorted((a - b).elements()),
            "extra":   sorted((b - a).elements())}

def glossary_gate(translation: str, glossary_used: list[dict]) -> list[str]:
    "Every term the model says it used must actually be in the output."
    return [g["source"] for g in glossary_used
            if g.get("inflected_as", g["target"]) not in translation]

def protected_gate(translation: str, protected: list[str]) -> list[str]:
    return [p for p in protected if p not in translation]

Using a Counter rather than a set is deliberate. A source string with {count} twice and a translation with it once is a real and common failure — the model merges two clauses — and a set-based check passes it happily.

The placeholder gate is the one to wire as a hard block. A missing placeholder is not a quality issue; it is a string that will throw at format time or render a literal brace to a user. Fail the string, keep the source, log it.

The back-translation gate

Back-translation is the standard check and it is usually implemented in a way that makes it meaningless. The rule: the back-translating call must not see the original. If the source is anywhere in its context — in a system prompt, in an earlier turn, in a “for reference” block — it will reconstruct the original rather than translate what is in front of it, and the comparison will always pass. That is not a subtle effect: it is the difference between a test and a formality.

  1. Call two, fresh context. Nothing but the target-language text.
    Translate <text> from {{target_language}} into {{source_language}}.
    
    Translate literally. Preserve the structure, the hedging and any awkwardness.
    Do not improve the text, do not make it idiomatic, and do not restore anything
    you believe was lost. If a sentence is odd in <text>, it must be odd in your
    output.
    
    Return only the translation, with no commentary.
    
    <text>{{translation}}</text>
  2. Call three, comparison. Give it the original and the back-translation, both in the source language, and nothing about how either was produced. Reuse the entailment prompt from the rewriting recipe with source as the original and rewrite as the back-translation — it is the same question, and it already has the quote requirement.
  3. Read the result correctly. A difference between the original and the back-translation is a signal, not a verdict. Two round trips through a model will produce differences on any text longer than a sentence. What you are looking for are the ones that change meaning: a negation, a number, a modal verb, a scope, a named party.

Because of that last point, run the gate on a sample rather than on every string, and reserve it for content where a meaning error is expensive: legal text, safety instructions, dosage, terms. For UI strings, the deterministic gates plus a glossary check catch more per unit of cost. The wider question of whether an LLM or a dedicated MT system is the right engine for a given language pair is covered in AI in translation and localisation.

When it stops working

  • glossary_used shortens. The model has started reporting fewer terms than it applied, or applying fewer than it should. Cross-check by searching the source for glossary terms yourself; if the source contains six and the model reports two, the binding instruction is losing.
  • forced_choices empties out on a language that requires them. Always a regression. German or Spanish output with no reported formality or gender choice means the model stopped noticing it was choosing.
  • The back-translation is suspiciously good. If it matches the original almost word for word, check that the original is genuinely absent from that call’s context. This breaks most often when somebody adds a shared system prompt to every call in the pipeline.
  • Placeholder failures cluster in one language. Usually a word-order problem: the placeholder must move in that language and the model drops it rather than moving it. Give an example in that language in the prompt.