- Home
- Case studies
- Round-trip translation of structured documents without breaking them
Round-trip translation of structured documents without breaking them
Translating complex documents without breaking layout by extracting thousands of text segments, preserving their XML positions, and reinserting them losslessly.
Published Jun 30, 2026Reviewed Jul 2, 2026
The short version
- Problem
- Packaged document formats scatter translatable text through XML runs where careless extraction corrupts formatting or loses the path back.
- Constraint
- The system had to translate large documents while preserving segment order, inline formatting, layout metadata, and editor compatibility.
- System built
- An XML/ZIP pipeline that records element indexes and formatting context for every segment before exporting to a translation interchange.
- Result
- Translated files open like human-edited originals: same structure, same formatting, and no silent reflow or document corruption.
- Where this applies
- Useful for AI translation and document automation projects where model output must round-trip through hostile file formats safely.
- Stack
- Python XML/ZIP tooling · multi-format CAT interchange
Why this matters
If your team is planning a system with the same failure modes, this is the level of thinking a first call starts at.


Overview
The constraint is the entire brief: translate a packaged document — a nested ZIP of XML parts holding thousands of translatable segments — while preserving segment order, inline formatting, layout metadata, and compatibility with the editor a human would open the result in. Nothing about the visible page is allowed to shift: no paragraph reflows, no bold turns plain, no table cell grows past its column. The only thing permitted to change is the language of the text itself. That constraint rules out every shortcut that treats the document as a string, because a packaged document isn't a string — it's a tree, and the tree is the layout.
Walking the XML tree for extraction
Packaged document formats — the word-processing, presentation, and spreadsheet families built on this model, and their open-standard equivalents — are ZIP archives holding a tree of XML parts: one part for body content, others for styles, headers, footers, embedded media, and the relationships between them. Inside the content part, the text a reader sees as one continuous sentence is rarely one continuous string in the markup. It's split across multiple text elements nested inside formatting runs, and a single sentence can span several runs because bold starts partway through a word, because a hyperlink wraps three words in the middle, or because the authoring tool's spellchecker or autocorrect silently inserted a run boundary years before anyone thought about translation. A process that reads the document by concatenating visible text has already thrown away the one thing it needs to write a translation back: which XML element, and which run inside it, each fragment came from.
Layout metadata lives one level up from the runs — page breaks, section properties, list numbering, table grid definitions — and none of it is translatable content, so extraction is deliberately scoped to leaf text nodes only. The walk visits every run looking for text to lift out, but it never touches the structural elements around those runs, which is what keeps a translated document's pagination, numbering, and table layout identical to the source without any separate step to preserve them: they were simply never disturbed.
The extraction step here never treats the document as text. It walks the XML tree part by part, in document order, visiting every run that carries translatable content, and treats each run — not each sentence, not each paragraph — as the unit of extraction. That's the only granularity at which "record exactly where this came from" and "the visible layout is untouched" can both hold at once. In a large document that walk produces well over three thousand segments, each one a small, addressable unit rather than a fragment of an undifferentiated blob of text.
Why not extract raw text and reinsert by string match
The obvious shortcut is to skip the bookkeeping: pull all the visible text out, translate it, and reinsert each translated string wherever its original matched in the document. It fails for three separate reasons, and any one of them is enough to rule it out. Documents repeat strings — the same button label, the same disclaimer, the same section heading — appearing dozens of times with no way to tell one occurrence from another by content alone, so a string match either translates every occurrence identically (wrong, when they sit in different contexts) or matches the wrong occurrence outright. Translation itself reorders: a source sentence's word order rarely survives translation intact, so matching "the text that used to be here" against "the text that's here now" stops being reliable the moment the source has already changed. And formatting runs split strings in the source document in ways that have nothing to do with meaning — a word broken across two runs because of a mid-word style change is not text a naive extractor can even isolate as a coherent unit to match against, let alone reinsert correctly. Position — the element's place in the tree — doesn't have any of these failure modes, because it doesn't depend on the content staying recognizable. It only depends on the tree still existing in the same shape it was walked in.
Recording element index and formatting context
Extraction on its own only solves half the problem — pulling text out is worthless without a reliable way to put a translation back in the exact same place with the exact same formatting. So every segment is recorded with more than its text: which XML part it lives in, its index within that part's document order, and the formatting context of the run it came from — bold, italics, font, language tag, and whatever other run-level properties would otherwise be lost the moment the text is lifted out into a flat translation file.
{
"segment_id": 1842,
"part": "word/document.xml",
"element_index": 5391,
"run_props": { "bold": true, "lang": "en-US" },
"text": "Continue to checkout"
}
One extracted segment record — position and formatting travel with the text, not just the text itself
This record is the entire contract between export and reimport. Over three thousand of these in a large document means over three thousand small opportunities to get the position wrong, so the bookkeeping has to be exact rather than approximate — a segment placed one element off doesn't fail loudly, it silently attaches translated text to the wrong run, and the corruption doesn't surface until someone opens the file and finds a sentence sitting in the wrong paragraph with the wrong style.
Exporting to a translation interchange format
Once every segment is extracted and indexed, it's written out to a flat translation interchange rather than handed to a translator as raw XML. Raw XML carries far more than translatable text — namespaces, style definitions, section breaks, embedded object references — and putting that in front of a human translator or a CAT tool invites exactly the kind of accidental edit the whole pipeline exists to prevent: a translator scrolling through markup can nudge a tag, break a namespace prefix, or "fix" something that wasn't broken. A flat interchange strips all of that away and leaves only what a translator or a translation-memory system actually needs: a segment identifier, the source text, and a slot for the target text, paired one-to-one with the records captured during extraction.
That format boundary also does useful work beyond safety. Translation-memory matching, terminology checks, and quality-assurance passes in CAT tooling all operate on flat segment lists — that's the interchange contract most translation tooling already expects, so exporting to it rather than inventing something bespoke means the translation side of the workflow needs no special handling for documents that started life as packaged XML. It also means a segment can be reused: the same disclaimer or button label extracted from three different places in the document arrives at the translator as three entries with three segment IDs, each one free to be translated identically or differently depending on context, instead of being silently collapsed into one because the extraction step happened to notice they matched. The interchange is also a natural checkpoint: every segment that goes out has a known segment ID, and every segment that's expected to come back has the same ID, so a segment lost or duplicated in translation is a diffable, catchable error rather than a silent one.
Lossless reinsertion
Reimport reverses the walk. For each translated segment, the pipeline reads its recorded element index and formatting context, locates that exact position in a fresh copy of the original document's XML tree, and writes the translated text into it — reconstituting the run's formatting properties rather than assuming the translated text can simply replace the old string in place. Where a run boundary fell mid-phrase in the source (a bold sub-span, a hyperlinked fragment), the reinsertion logic has to decide how the translated text maps onto that same set of runs, since a straight one-to-one character replacement won't hold once the word order and word lengths of the translated text differ from the source.
Diagram: the XML round trip — export walks the document's XML tree, extracts each translatable segment while recording its element index and formatting-run context, writes a flat translation interchange file; after translation, reimport parses the interchange, locates each segment's original element index, and reinserts the translated text with its formatting runs preserved, producing a document that opens exactly as the source structure intended.
The last step before a translated file is considered done is structural, not linguistic: the reassembled XML has to still be well-formed, the ZIP container has to repack cleanly, and the document has to open in the same editor a human author would use — not a viewer, not a converted export, the actual authoring application — without a repair prompt or a corruption warning. That's the bar the whole pipeline is built around, because a translation that's linguistically perfect but structurally broken is not a deliverable; it's a support ticket.
Tradeoffs and limitations
- The index-based approach assumes the original document's XML structure is unchanged between export and reimport. If the source file is edited while translation is in flight, the recorded element indexes can point at the wrong element, or at nothing, when reinsertion runs.
- Formatting runs that split mid-word in the source need careful merge and re-split logic on the way back in; a translated phrase with a different word count than the source doesn't map cleanly onto the same run boundaries without that handling.
- Bookkeeping scales with segment count, not document complexity in the abstract — a document with three thousand segments has three thousand places a position or formatting record can be wrong, each one a narrow, silent failure rather than a loud one.
- Formatting a translator introduces inside the interchange itself — emphasis added for a target-language convention, for instance — still has to map back onto the run model on reimport; the interchange format doesn't guarantee that translation-side formatting round-trips as cleanly as the source formatting does.
- This pipeline guarantees structural integrity, not translation quality. A fluent, well-formed, entirely wrong translation round-trips just as cleanly as a correct one — that's a separate problem, solved by separate tooling.
- Editor compatibility is verified empirically against the target authoring application. A file that opens cleanly in one version of that application isn't automatically guaranteed to open cleanly in every version.
References and further reading
Related case studies
Systems that share a constraint with this one — retries, isolation, cost, recovery.
Frame-accurate video localization
Long clips stay locked to source timestamps while the original non-vocal audio survives beneath the synthesized dub.
WhisperX · XTTS voice cloning · Demucs · FFmpeg · CTranslate2A local-first dictation pipeline for macOS
Speech stays local, latency stays low, and the pasted result can reflect on-screen context without collapsing when one OS subsystem changes state.
Swift / SwiftUI · whisper.cpp · Accessibility & CoreGraphics APIs