MT glossary not applied to ICU branches

The termbase says “account” must be “Konto” in German. The machine-translation glossary is configured, the pre-fill job runs, and the result has “Konto” in the singular branch and “Account” in the plural one. Same message, same run, same glossary.

The message was not translated once. It was decomposed into branches, each branch was sent as an independent request, and consistency between independent requests is not something a glossary guarantees.

Branch-by-branch translation of an ICU message A plural message with three branches is decomposed by the pre-fill job, each branch is sent to the engine as its own short segment, three independent translations come back, and reassembly produces a message whose branches may use different words for the same bound term. How an ICU message reaches the engine Catalogue Pre-fill job MT engine Result plural with 3 branches each branch sent separately three independent translations reassembled — terms may differ
Each branch is a separate request with separate context — consistency between them is not automatic.

Root cause: a branch is not a sentence

A pre-fill job cannot hand an ICU message to a translation engine as-is, because the engine would translate the syntax along with the text — the failure described in pseudo-locale strings breaking ICU placeholders, with a different cause and the same shape. So the job walks the message, extracts the literal text from each branch, and sends those.

That decomposition has two consequences. Each branch arrives as a short fragment with no surrounding context, so the engine has less information to work with than the full sentence would provide. And each branch is a separate request, so any engine behaviour that depends on the request — including glossary application — applies to each independently.

Glossaries are usually reliable per request. What fails is the assumption that three requests carrying the same term produce the same output: a short fragment where the term appears in an inflected form may not match the glossary entry, while the branch where it appears in the base form does.

Four reasons a bound term is ignored A term inside an ICU branch reaches the engine as a fragment, so only some branches obey. A glossary listing a single form fails on inflected variants. Masking that splits a phrase can separate a term from its context. And a request that omits the glossary identifier applies none of it anywhere. Why a glossary term is not applied Cause Tell Term inside an ICU branch the engine sees fragments some branches obey, some do not Inflected form in the target the glossary lists one form nominative correct, others wrong Term is a placeholder neighbour masking split the phrase only masked segments fail Glossary not attached the request omitted the id nothing obeys, anywhere
The tell distinguishes them: which branches obeyed is the fastest question to ask.

Minimal reproducible example

{count, plural,
  one {You have # account}
  other {You have # accounts}}

The singular branch contains “account” and matches the glossary entry exactly. The plural branch contains “accounts”, which — if the glossary lists only the singular — does not match, and the engine translates it freely. The result mixes “Konto” and whatever the engine preferred for the plural.

The fix, at three levels

Attach the glossary to every request. The commonest configuration error is attaching it once at job level while the individual calls omit the identifier. Every request must name it.

const translated = await deepl.translateText(fragment, 'en', 'de', {
  glossary: glossaryId,             // per call, not per job
  tagHandling: 'xml',
  formality: 'less',
});

Send whole messages where the engine supports inline tags. Several engines accept markup and preserve it, which means the ICU structure can be encoded as tags and the message translated as one unit. That restores the context and makes the glossary apply once to a coherent sentence rather than three times to fragments.

Include the forms that actually appear. A glossary listing “account → Konto” does not help with “accounts”. Adding the plural, and any other inflected form your source uses, closes most of the remaining gap. Some engines support lemma-level matching; where they do not, enumerating the forms is the practical answer, and the list is short because source-language inflection is limited.

Five steps for glossary compliance on ICU messages The glossary is attached to every request rather than configured once. Whole messages are sent where the engine supports inline tags, avoiding decomposition. Inflected forms are included so the term matches in context. Verification runs on the reassembled message rather than on individual branches. And a message with any non-compliant branch is rejected as a whole. Making terminology hold across branches 1 Attach the glossary to every request per language pair, per call 2 Send whole messages where the engine supports it tag handling instead of decomposition 3 Include inflected forms in the glossary or use a lemma-aware entry 4 Check the terms after reassembly not on each branch 5 Reject the whole message if any branch fails partial compliance is worse than none
The last step matters: a message where two branches say Konto and one says Account is worse than an untranslated one.

Verify after reassembly, not per branch

The check that matters runs on the finished message. A per-branch check passes in the case above — each branch is individually plausible — and the defect only exists in the relationship between branches.

function glossaryHolds(message: string, terms: Map<string, string>): boolean {
  const branches = extractLiterals(parse(message));     // ICU-aware, as in the transform
  for (const [source, target] of terms) {
    const relevant = branches.filter((b) => sourceMentions(b, source));
    if (!relevant.length) continue;
    // Every branch that should use the term must use the same one.
    if (!relevant.every((b) => b.includes(target))) return false;
  }
  return true;
}

A message failing this check should be rejected in full rather than partially accepted. A message where two branches say “Konto” and one says “Account” is worse for a reader than one that is untranslated, because the inconsistency looks like a product distinction rather than a mistake — and it will be reported as a bug against the product rather than against the pipeline.

Where this fits in the quality gate

Terminology is one check among the several a pre-fill gate should apply, and it interacts with the others in a specific way.

Placeholder parity and ICU structure are structural checks: they can be evaluated per branch, because a placeholder either survived or did not. Terminology is a consistency check across the message, and length plausibility sits somewhere between — a single branch can be implausibly long, and so can the assembled message.

The ordering that works is structural checks first, on branches, because they are cheap and their failures are unambiguous. Consistency checks then run on messages that passed, and their failures return the whole message for human translation rather than for another machine attempt. Sending a failed message back to the engine with the same glossary produces the same result, which is a loop worth avoiding explicitly.

That ordering also keeps the state model honest. A message that fails terminology is not a machine suggestion needing review; it is a message the machine could not produce acceptably, and marking it as needing translation rather than as needing review is the difference between a translator writing one sentence and a reviewer reading three.

Keeping the glossary small enough to be right

A glossary that grows without discipline starts producing wrong translations rather than consistent ones, and the failure is harder to spot than a missing term because the output looks deliberate.

The mechanism is substitution without context. A glossary entry applies wherever its source appears, so binding a common word — “open”, “share”, “post” — forces one target everywhere, including the places where the word carried a different sense. In English those words are verbs and nouns and adjectives depending on context; in the target language they are different words entirely, and the glossary has just insisted on one of them.

Three rules keep a termbase useful. Bind product nouns, not general vocabulary: your feature names, your object types, the words a reader would recognise as belonging to your product. Bind terms with a compliance or legal meaning, where a synonym is not acceptable. And do not bind anything whose English form has more than one part of speech unless the entry can be scoped to a context the engine understands.

The size that results is smaller than teams expect — usually a few hundred entries for a large product, not several thousand. A glossary of five thousand entries is nearly always a dictionary that has absorbed general vocabulary, and it will produce stilted output that translators then have to undo, which is the opposite of the intended effect.

The review habit that keeps it small is to require a reason on every addition, recorded with the entry. “Product noun”, “regulatory wording” and “brand term” are reasons; “translators kept getting it wrong” usually means the string needed context rather than the term needed binding.

Verification

# Sample the pre-filled catalogue for messages containing a bound term
npx tsx scripts/check-glossary.ts locales/de/*.json --terms termbase.csv

# Expected
#   2841 messages scanned, 312 contain a bound term
#   ✓ 309 consistent across all branches
#   ✗ 3 inconsistent: checkout.summary.accounts, billing.card.expiry, nav.account.menu

Reporting the failing keys rather than a count is what makes this actionable — three named messages are a ten-minute fix, and a number is a ticket nobody picks up.

When to escalate

If terminology holds in the pre-fill and drifts later, the glossary and the termbase have diverged. They should be generated from one source, as described in translation memory and glossary management, and regenerating on every termbase change is the only arrangement that stays correct.

If a term is applied correctly but reads wrongly in context, the entry may be too broad. A term bound unconditionally will be substituted everywhere, including places where the general-language meaning was intended, and the fix is a narrower entry rather than a stronger enforcement.

If the engine ignores the glossary entirely for one language pair, check that a glossary exists for that exact pair. Glossaries are directional and per pair, and a missing one usually fails silently rather than erroring.

FAQ

Should the glossary be enforced or only checked?

Both, at different points. Enforcing during translation produces a correct draft, which is strictly cheaper than detecting an incorrect one. Checking afterwards catches what enforcement missed — inflected forms, engine limitations — and is what should gate the write into the catalogue.

Does this affect human translation too?

The consistency risk does, though for a different reason: a translator working branch by branch in a tool that shows fragments has the same fragmented view the engine had. Showing the whole message in the editing interface is the equivalent fix.

What about terms that must not be translated?

Those belong in the glossary as identity entries — source and target identical — rather than being masked. Masking removes them from the engine’s view entirely, which occasionally changes how the surrounding sentence is translated.

Is it worth pre-filling ICU messages at all?

For short, factual messages, yes. For messages with many branches and heavy inflection, the review cost often exceeds the saving, and marking them for human translation from the start is the better policy — the selectivity argument in machine translation pre-fill workflows.

Part of Machine Translation Pre-Fill Workflows.