Join early access

Probabilistic models alongside deterministic parsers: the case for constrained localization using LLMs

A translation can be one hundred percent correct as language and one hundred percent wrong as software. Fluency is necessary and not sufficient; the German reads beautifully, and the build fails.

A figure holding up a single puzzle piece, drawn in white line on a slate ground

The localization paradox

Machine translation has gotten very good at language. In high-resource pairs, a current model holds tone, carries register and handles idiom well enough that a reader does not notice. Dropping one into a continuous localization pipeline has nonetheless exposed a structural problem that no amount of linguistic skill addresses, and it is best stated as a paradox: a translation can be one hundred percent correct as language and one hundred percent wrong in a software.

The friction comes from a difference in what the two sides tolerate. Human language is forgiving. We as humans recover from a missing comma, an unusual clause order or a word used slightly off-centre. Software parsers are deterministic and brittle and are not forgiving. When a model translates a user interface string it is not only translating text, it is editing structured data that something downstream will execute. Translate an interpolation placeholder, drop a brace, render an HTML attribute into Spanish, shift a YAML line by one space, and the application fails to build, renders a broken variable at a customer, or opens a security hole.

BLEU and COMET score the text. Every failure below leaves text that scores well and a syntax tree that is broken, which is exactly why these reach production: the quality gate that would have caught them is measuring the wrong thing.

When a string stops being text

Modern internationalization has moved a long way past flat key-value pairs. To absorb the structural variation between languages, frameworks embed a small formatting language inside the string itself. FormatJS, i18next and Apple’s String Catalogs do not fetch static text; they execute localized logic at render time against real user data.

So a model working on a JSON, YAML, XLIFF or .xcstrings file is operating in a hybrid domain. It has to be a linguist for the natural-language parts and a compiler for the control syntax, in the same token stream, with no marker separating them. ICU MessageFormat is where this bites hardest: braces open executable blocks, reserved keywords invoke the locale’s arithmetic, and the inner names are dictionary keys that a backend will look up by exact string.

text
01{gender, select,02  female {{count, plural,03    one {She has # unread message}04    other {She has # unread messages}}}05  male {{count, plural,06    one {He has # unread message}07    other {He has # unread messages}}}08  other {{count, plural,09    one {They have # unread message}10    other {They have # unread messages}}}}
A plural rule nested inside a gender selector. Every brace, keyword and inner name here is structure, and none of it is prose.

To a parser those boundaries are absolute. To a model without a hard constraint they look like English prose awaiting translation, wrapped in punctuation. Nest one rule inside another and the grammar’s complexity scales faster than the model’s odds of reproducing it exactly.

Placeholder corruption and interpolation syntax

Placeholders inject runtime data into the interface, and every framework spells them differently. Rails uses %{variable}, Symfony uses %variable%, i18next uses {{variable}}, and C-style formatting uses %s or %1$s. Corrupting one of these is the single most common failure in LLM localization.

Models work on subword tokens and probability distributions. {user_name} is a word to a tokenizer. Unless the token is explicitly protected, the model will often localize the variable name to fit the target language’s semantics, because that is what the surrounding context suggests.

json
01{02  "source":     "Welcome back, {user_name}",0304  "localised":  "Bienvenido de nuevo, {nombre_de_usuario}",05  "spaced":     "Bienvenido de nuevo, { user_name }",06  "correct":    "Bienvenido de nuevo, {user_name}"07}
Three outputs for one source string. Only the last one renders.

The backend passes a variable named exactly user_name, so the first line renders empty, undefined, or NaN. The second is subtler and just as fatal: a human reviewer reads { user_name } as the same token with a stylistic space around it, and react-intl throws a syntax error that can take down the client render or fail the build outright.

Variable ordering, or the bug that reads perfectly

Ordering sits exactly on the seam between grammar and program logic. If the source reads Moved %1$s to %2$s, the application guarantees that the first argument is the thing being moved and the second is where it went. German frequently needs the destination first to read naturally.

text
en   Moved %1$s to %2$sde   %2$s: %1$s wurde verschoben     positions kept, renders correctlyde   Verschoben %s nach %s           compiles, and says the wrong thing
Positional arguments kept, and positional arguments dropped. Both compile.

Drop the positional identifiers and the runtime injects the arguments in their original order anyway. Nothing errors. The user is told a folder was moved into a file. This is the purest form of the paradox: a linguistically accurate translation operating as a critical bug, and the only signal that anything is wrong is a customer who reads German.

The plurality cliff and CLDR non-compliance

English has two plural categories, one and other, which is why English source files so often carry a ternary or a hardcoded pair. The Unicode CLDR defines far more. Arabic uses six: zero, one, two, few, many and other. Russian and Polish need one, few, many and other. Japanese needs only other, because nouns do not inflect for quantity at all.

Hand a model an English catalogue to translate into Russian and it will usually mirror the shape of its input, returning one and other in Russian and nothing else. It has not been asked to change the schema, so it does not. When the application renders “3 items” it looks for the few key, finds nothing, and either throws or falls back to a form that is grammatically wrong.

jsonru.json
01{02  "unread": {03    "one":   "# непрочитанное сообщение",04    "few":   "# непрочитанных сообщения",05    "many":  "# непрочитанных сообщений",06    "other": "# непрочитанного сообщения"07  }08}
The four categories Russian requires. The two highlighted rows are the ones an English-shaped catalogue never asks for.

Instructing the model to emit every CLDR form is necessary and not sufficient. Two failures survive the instruction, and both are hallucinations that look like helpfulness.

text
{count, plural, one {# сообщение} другие {# сообщений}}      the fallback key was translated; the parser has no branch to fall back to{count, plural, one {одно сообщение} other {много сообщений}}      # is gone, so the count never renders{count, plural, one {# сообщение} few {# сообщения} many {# сообщений} other {# сообщения}}      correct
The two failures that survive an explicit instruction to emit every CLDR form.

ICU requires the literal English string other as the fallback node in the tree. Translating it, which is the obvious thing to do if you believe you are translating, produces a fatal parse error. The # token is the second: it is where ICU substitutes the number, and models routinely delete it or replace it with the number spelled out, which kills the dynamic rendering while leaving a sentence that reads fine.

Gender rules and select formats

Gender selection has the same shape as pluralization and breaks in three ways. The control keys get translated, so nothing the backend sends matches a branch. The verbs and adjectives inside the nested blocks are not re-conjugated for a heavily inflected target, so the branches are grammatically inconsistent with each other. Or the model invents categories the schema has no column for, or collapses the whole structure into one neutral string and quietly discards a personalization the product was built around.

text
{gender, select, male {He responded.} female {She responded.} other {They responded.}}{gender, select, masculino {Respondió.} femenino {Respondió.} otro {Respondió.}}      keys translated, so nothing the backend sends will ever match a branch
A select block before and after translation. The branches read correctly and none of them can ever be reached.

HTML, Markdown tags and code preservation

Documentation, marketing pages and rich UI components interleave HTML and Markdown inside localized strings, which asks the model to decide continuously what is prose and what is syntax. It gets that wrong in consistent ways.

text
<a href="/about">Learn more about us</a><a href="/acerca-de">Conozca más sobre nosotros</a>      the visible text is right; the href now points at a page that does not existRun `npm install -g acme-cli` to beginEjecute `npm instalar -g acme-cli` para comenzar      the command inside the code span was translatedLine one<br />line twoLínea uno<br></br>línea dos      a self-closing tag became a pair, which JSX refuses to compile
Three ways markup gets localized along with the text around it.

Markdown tables lose their pipes or their alignment. A CLI command inside an inline code span gets translated, which leaves documentation that is factually wrong and operationally useless to the developers it was translated for. An href gets localized alongside the link text and points at a page that was never created. And a self-closing tag comes back as an open-and-close pair, which JSX refuses outright.

JSON structure, YAML indentation and escape sequences

The file format adds a layer of fragility of its own. JSON at least fails loudly: a missing brace or an unescaped quote invalidates the document and the build stops. Models produce unescaped quotes regularly, because plenty of target languages want quotation marks inside a string where the English had none.

YAML is worse precisely because it fails quietly. Its hierarchy is whitespace, so a key indented one space too far still parses. The parser simply files it under a different parent. Nothing errors, the build is green, and a screen in production is missing its text because the lookup path no longer exists.

yamlfr.yml
# sourcetagline: "Your comfort, guaranteed"# what came backtagline: Votre confort: garanti# the parser now sees a mapping where a string was meant, and either# throws or silently reshapes the tree under "tagline"
A colon inside an unquoted scalar. The file still parses, which is the problem.

Certain characters force the whole scalar to be quoted: a colon followed by a space, an ampersand, a hash. A model translating Your comfort, guaranteed into French can reasonably produce a phrase containing a colon, and if it does not carry the quotes across, the colon becomes structure instead of punctuation.

Fluency and structure are different skills

Everything above asks one system for two capabilities at once, and they are not the same capability. Producing natural, register-correct language is a generative task: the model is rewarded for finding the phrasing a native speaker would have chosen. Preserving a syntax tree is a constraint-satisfaction task: the model is rewarded for changing nothing it was not asked to change. Nothing about being good at the first makes a system good at the second, and the mechanisms that deliver each one are different.

ApproachOptimization forControl syntax treatmentGeneral fit
Instruction-tuned LLMNatural language: register, idiom, contextAs text, unless told otherwise. Protection is a request the model can decline.Prose, marketing copy, anything read rather than parsed
Neural MT engineSemantic fidelity and throughput on plain textPreserves the simple inline tags it was trained on; has no mechanism for schema-level rulesLong-form documentation and high-volume plain text
LLM under constrained decodingStructural validity of the outputEnforced by a grammar at inference time. Invalid tokens are unreachable, not discouraged.Generating or editing structured catalogues directly
Generate first, then extractBoth, at the cost of a second callIgnored in the first pass, enforced in the secondPipelines that need fluent output in a strict format
Four ways of getting a translated string out of a machine, and what each one is architecturally able to promise about the syntax around it.

The distinction that matters is in the third column. A prompt naming the placeholders is a request, and a request has a failure rate. A grammar that masks invalid tokens is a constraint, and a constraint has none, because the token that would break the file is not available to be sampled. Everything else is a question of which properties you are willing to trade for that.

It is also why a neural MT engine and an instruction-tuned model are not competitors on this problem so much as different instruments. An MT API takes plain text or simple markup and returns a translation. There is no channel through which to say “translate the values in this file, preserve the CLDR plural categories for Polish, expand the key set to include few and many, and leave every ICU keyword alone”, because the interface was never designed to carry that kind of instruction. For code-adjacent localization that is a question of fit, not of quality.

Constrained decoding and the format tax

The obvious fix is to stop asking. Constrained decoding frameworks such as XGrammar, Outlines and DOMINO work at inference time by masking the model’s logits so that output which would violate a schema becomes mathematically unreachable. Compile the target format into a finite-state machine or a context-free grammar, and at every step any token that would produce invalid JSON has its probability forced to zero. Structural validity stops being likely and starts being guaranteed.

It costs something, and the cost has a name. Warping the model’s natural distribution degrades its reasoning, and strict schema-mode decoding can cost accuracy on multi-step analytical tasks. Attention spent on braces, quotes and escape sequences is attention not spent on register and nuance, and the translations come back stilted.

Token misalignment makes it worse. Models emit subword tokens, not characters, and a single token can straddle the boundary between syntax and prose. A character-level grammar will mask out an otherwise optimal token because a fraction of it violates an intermediate parser state, pushing generation down a worse path. The output is valid and reads badly.

Translate unconstrained, in free-form prose, so the model uses its best decoding paths. Then hand that output to a second, smaller, cheaper model whose only job is extraction: put this text into this schema. The second pass carries the grammar; the first keeps the language. It costs one extra hop and it recovers what the format tax takes.

Security: XSS and localization poisoning

The gap is not only a compilation problem. Localized strings routinely carry markup for bold text, links and spans, and rendering that in React, Vue or Angular means reaching for dangerouslySetInnerHTML or v-html. Those directives are considered acceptable on the assumption that the strings are authored by trusted people and reviewed before they land.

Machine-generated translations flowing straight into that path through CI break the assumption. A model that hallucinates an unclosed <img> with an onerror attribute, or “improves” a Markdown link into a script tag, has written a payload. If the pipeline syncs to the production branch without server-side sanitization, every client rendering that string executes it.

This is not hypothetical: stored XSS has been achieved by poisoning translation payloads. Anyone with access to the localization platform, or a successful prompt injection against the model doing the translating, can plant a payload that fires in an administrator’s browser the moment they view the product in that language. Expecting the model to emit safe HTML is not a control. Sanitize with something like DOMPurify before a localized string reaches the DOM, and keep a Content Security Policy behind it.

Treating a translation as a code change

All of it points one way: a translation is a code change, and it needs the same gate a code change gets. Not a review of the language, a check of the structure, and it has to run in the pipeline rather than in someone’s memory.

Parse both sides, compare the structures, fail the build on a mismatch. Placeholder sets must be identical. CLDR categories must be complete for the target locale. ICU must still parse. Markup must balance and its attributes must be untouched.

locale checks in CI
npx i18n-verify locales/ --against en.json
Build failure reason
  • es.json · account.welcome — placeholder renamed: {user_name} → {nombre_de_usuario}
  • ru.json · inbox.unread — CLDR incomplete: missing few, many
  • de.json · files.moved — positional arguments dropped: %1$s %2$s → %s %s
  • fr.yml · marketing.tagline — unquoted colon reshapes the tree
  • 4 of 1,204 strings failed, exit 1

Above that gate, two things are worth building. Route by string rather than picking one engine: send marketing copy to the model that writes best and a dense ICU catalogue to the model that holds structure best, and divert anything a quality estimator flags to a human instead of to production. And keep the model away from the file format where you can. A protocol layer that owns the writing, validating format specifiers and generating the CLDR variants itself, leaves the model doing the one thing it is genuinely better at than any tool that came before.

Models are very good at language. That much is settled, and it is not a small thing to be able to buy. What they cannot be is trusted on their own, because a probability distribution is the wrong shape for a deterministic parser, and no amount of prompting changes that: a prompt is a request, and a parser is not negotiating.

So build the cage. A harness around the model, one that states the checks explicitly and enforces the constraints itself, is not a way of distrusting the model; it is the only way to use it at full strength. Give it the whole of the language and none of the file format, put a gate between what it writes and what ships, and the thing that was unreliable in isolation becomes the best translator you have ever had in the pipeline. The reliability was never going to come from the model. It comes from what you build around it.