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.

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.
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.
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}}}}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.
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}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.
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 thingDrop 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.
01{02 "unread": {03 "one": "# непрочитанное сообщение",04 "few": "# непрочитанных сообщения",05 "many": "# непрочитанных сообщений",06 "other": "# непрочитанного сообщения"07 }08}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.
{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 {# сообщения}} correctICU 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.
{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 branchHTML, 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.
<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 compileMarkdown 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.
# 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"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.
| Approach | Optimization for | Control syntax treatment | General fit |
|---|---|---|---|
| Instruction-tuned LLM | Natural language: register, idiom, context | As text, unless told otherwise. Protection is a request the model can decline. | Prose, marketing copy, anything read rather than parsed |
| Neural MT engine | Semantic fidelity and throughput on plain text | Preserves the simple inline tags it was trained on; has no mechanism for schema-level rules | Long-form documentation and high-volume plain text |
| LLM under constrained decoding | Structural validity of the output | Enforced by a grammar at inference time. Invalid tokens are unreachable, not discouraged. | Generating or editing structured catalogues directly |
| Generate first, then extract | Both, at the cost of a second call | Ignored in the first pass, enforced in the second | Pipelines that need fluent output in a strict format |
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.
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.
- 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.