Your product team drops a brochure export request into Slack. The source is an .indd file. You need translated variants, you want the strings in Git, and you don't want a workflow that depends on someone manually opening Adobe InDesign every time copy changes.
That's where developers usually hit the wall. An INDD file is a bad handoff if your next step is extraction, diffing, automated translation, or review in CI. You can't treat it like source content. You mostly end up asking a designer to export something more usable.
If you're searching what is an idml file, the useful answer isn't “an InDesign file type.” The useful answer is: it's the version of an InDesign document you can inspect, parse, and transform with code.
Meta description: Got handed an INDD file for localization? Learn what an IDML file is, how its XML structure works, and how to extract text for translation in code.
The Designer Hands You an INDD File Now What
A designer drops an .indd file into Slack at 4:30 PM. The ask sounds simple: extract the copy, send it for translation, and give design something they can place back into the layout without redoing the document by hand.
For engineering, the problem starts with the file itself. .indd is a native authoring format, which means it works well inside InDesign and poorly inside an automated content pipeline. You cannot inspect it with normal XML tools, you cannot review text changes cleanly in Git, and you cannot wire it into a repeatable extraction step without adding a manual Adobe-dependent hop.

Why INDD is the wrong handoff for engineering
An INDD-only handoff usually pushes a team into one of three failure modes:
- Manual text scraping: Someone copies strings out of the layout, strips context, and introduces errors before translation even starts.
- Designer-in-the-loop for every revision: Each copy update has to be reopened, replaced, and exported in Adobe InDesign.
- No auditable content flow: The source text, translated text, and final placed text live outside version control and outside the same review path you use for code.
That trade-off might be acceptable for a one-off brochure. It breaks down fast once the asset needs multiple languages, legal review, or regular copy updates.
The practical question is not whether the design can be localized. It is whether the content inside that design can be treated as structured input. If the answer is no, every translation round becomes a small production incident.
The file format that fixes the handoff
The useful handoff for developers is the one you can parse, inspect, and rebuild from a script. For InDesign documents, that handoff is IDML.
Once the document is in IDML form, the workflow changes. Text stops being trapped inside a binary file and becomes part of a package you can open from the command line, walk with an XML parser, extract into translation resources, and validate before it goes back to design.
That is the difference that matters in practice. You stop treating the file as a design artifact that engineering has to work around, and start treating it as document data that can move through the same automation discipline as the rest of your localization stack.
What an IDML File Is and Why It Matters for Code
A designer sends over brochure.indd five minutes before string freeze. You cannot diff it, cannot parse it, and cannot feed it into the same review path you use for templates, locale files, or API content. The practical fix is to ask for IDML.
An IDML file is InDesign Markup Language, Adobe's interchange format for InDesign documents. For engineering work, the useful property is simple: it turns document content into XML-based package data that scripts can inspect and modify. Adobe documents IDML as a format intended for creating, modifying, and extracting InDesign content with standard XML tools in its InDesign Markup Language overview.
That changes how the file fits into a localization system.
Instead of treating the handoff as a design artifact that has to be opened in a desktop app, you can treat it as a structured source format. Text, inline formatting, story order, and document relationships become machine-readable enough to extract, validate, and map into translation resources. If your pipeline already handles XML and you understand UTF-8 text encoding details that affect multilingual content, IDML is much closer to code-friendly input than an .indd binary.

Why developers should care
For translation and content automation, IDML gives engineering teams a few concrete advantages:
- You can inspect it outside InDesign. A script can open the package, read XML files, and identify where text lives.
- You can extract text with structure intact. Paragraph boundaries, inline tags, and style references stay available during parsing.
- You can put the content under normal engineering controls. Extraction rules, validation, and rebuild steps can live in version control and CI.
There are trade-offs. IDML is still a document format, not a clean message catalog. Text may be split across multiple XML nodes, inline formatting can interrupt what looks like a single sentence, and layout-driven content often needs post-processing before it is safe to send for translation. But those are scripting problems. A binary .indd file gives you no such entry point.
The compatibility benefit
IDML also reduces one common production issue. Native InDesign files are tied more tightly to the version that wrote them. Adobe's own support guidance for InDesign exchange formats points teams to IDML when they need to move documents between versions of the application, as described in Adobe's InDesign file compatibility documentation.
For developers, that matters less as a desktop publishing feature and more as an operational boundary. If design authors in one version and another team validates output in a different version, IDML is the format that keeps the document portable while your extraction and reinsertion scripts stay unchanged.
Automate against IDML. Let designers keep authoring in INDD.
Anatomy of an IDML Package
A designer sends brochure.indd, translation is due tomorrow, and your automation has nothing to read. The workable handoff is the .idml export, because it gives you a file you can inspect with ordinary ZIP and XML tooling from the shell.
Rename an .idml file to .zip and extract it. What comes out is a package of XML documents plus supporting folders that describe stories, styles, spreads, resources, and package metadata. For a developer, that matters because you can trace where text lives, follow references, and script around the parts that affect translation quality.

What you'll see after unzip
A typical extracted package looks roughly like this:
brochure.idml/
├── designmap.xml
├── Stories/
│ ├── Story_u1.xml
│ ├── Story_u2.xml
│ └── ...
├── Resources/
├── Spreads/
├── MasterSpreads/
├── XML/
├── Styles/
└── META-INF/
Folder names and file counts vary by document, but the shape is consistent enough to automate against.
The files that matter most
| Path | What it does | Why you care |
|---|---|---|
designmap.xml |
Main package manifest | Helps map document parts and relationships before you parse individual files |
Stories/Story_*.xml |
Text content and inline structure | Contains most translatable copy, plus tags that affect sentence segmentation |
Styles/ |
Paragraph and character style definitions | Useful when translated output must preserve formatting intent or trigger QA rules |
Resources/ |
Shared assets and metadata | Good for context checks, usually not the first place to extract user-facing text |
For translation work, Stories/ is the center of gravity. In practice, that is where I start parsing, because walking every XML file in the archive adds noise before it adds value.
designmap.xml is still worth understanding. It is the package index, and it helps you answer annoying but common questions such as which stories belong to which spreads, whether a document has alternate layouts, and what other XML parts might need to travel with your rebuilt archive.
What makes IDML tricky in code
The text is structured, but it is not clean in the way a message catalog is clean. A visible sentence may be split across multiple XML nodes because of character styling, hyperlinks, footnotes, tables, or special characters. If you concatenate everything too early, you lose boundaries you may need when you write translations back.
That trade-off matters in automated localization, including teams experimenting with AI in content creation and translation. Fast extraction is easy. Reliable round-trip handling is the harder part.
Encoding mistakes also show up here. If your parser reads or writes XML with the wrong charset assumptions, you can damage accented text, CJK content, or RTL strings before anyone opens the file in InDesign. Keep your pipeline strict about XML serialization and review the basics of UTF-8 text encoding in localization workflows if that part has caused trouble before.
A visual walkthrough helps here:
How to Programmatically Extract Text for Translation
You don't need a vendor SDK to get started. Python's standard library is enough to read an IDML package and pull user-facing text from the story XML files.
That's one reason IDML is useful in automation. It's also smaller to move around. Technical benchmarks reported that IDML files are typically 50–70% smaller than equivalent INDD files, and a 2017 benchmark found sample documents that were 85–110 MB as INDD shrank to 28–40 MB as IDML, according to Markzware's IDML size discussion.
A minimal extractor in Python
The script below opens the archive, finds Stories/*.xml, parses them, and extracts text from Content nodes.
from __future__ import annotations
import sys
import zipfile
import xml.etree.ElementTree as ET
from pathlib import Path
def strip_namespace(tag: str) -> str:
if "}" in tag:
return tag.split("}", 1)[1]
return tag
def extract_idml_text(idml_path: Path) -> list[tuple[str, list[str]]]:
results: list[tuple[str, list[str]]] = []
with zipfile.ZipFile(idml_path, "r") as zf:
story_files = sorted(
name for name in zf.namelist()
if name.startswith("Stories/") and name.endswith(".xml")
)
for story_file in story_files:
with zf.open(story_file) as fp:
tree = ET.parse(fp)
root = tree.getroot()
strings: list[str] = []
for elem in root.iter():
if strip_namespace(elem.tag) == "Content":
text = elem.text or ""
if text.strip():
strings.append(text)
results.append((story_file, strings))
return results
def main() -> int:
if len(sys.argv) != 2:
print(f"Usage: python {Path(sys.argv[0]).name} path/to/file.idml")
return 1
idml_path = Path(sys.argv[1])
if not idml_path.is_file():
print(f"File not found: {idml_path}")
return 1
extracted = extract_idml_text(idml_path)
for story_file, strings in extracted:
print(f"\n[{story_file}]")
for text in strings:
print(text)
return 0
if __name__ == "__main__":
raise SystemExit(main())
Run it like this:
python extract_idml.py brochure.idml
What this gets right, and what it misses
It's a good base layer, but not a full production extractor.
What works
- No extra dependencies:
zipfileandxml.etree.ElementTreeare built in. - Predictable scope: It only reads story XML files.
- Easy to adapt: You can map extracted strings into gettext entries, JSON, or a custom review format.
What doesn't
- No text reassembly: Adjacent
Contentnodes may belong to one visible sentence. - No placeholder logic: Variables, inline markers, and special characters may need preservation rules.
- No context modeling: Headline text and footnote text can look identical if you ignore styles or story placement.
If you're building a larger automation stack, it helps to think about the extraction step alongside your broader content pipeline. The piece on AI in content creation and translation gives a useful high-level view of where scripted extraction fits in a modern publishing workflow.
For teams that already automate localization in Python, you can wrap IDML extraction into the same app code that handles .po files and model text. If you prefer a Python-first interface for translation tasks, the TranslateBot Python API docs show the kind of call pattern that fits well once your text is already normalized.
Reinserting Translations and Rebuilding the IDML
Extraction is only half the job. You also need a clean round trip back into a valid package that InDesign can open.
That means writing translated text back into the correct XML nodes, preserving every non-text element and attribute, and rebuilding the archive without changing the package structure in ways InDesign doesn't like.

The safe way to write translations back
At a high level, the loop looks like this:
- Parse the target
Story_*.xml. - Match extracted source segments to their original nodes.
- Replace only text payloads, not structure.
- Serialize the XML back to bytes.
- Repack the entire directory tree as
.idml.
You want node-level replacement, not regex replacement against raw XML strings. Regex will eventually eat markup you needed to keep.
from __future__ import annotations
import shutil
import tempfile
import zipfile
import xml.etree.ElementTree as ET
from pathlib import Path
def strip_namespace(tag: str) -> str:
return tag.split("}", 1)[1] if "}" in tag else tag
def replace_story_content(story_path: Path, replacements: dict[str, str]) -> None:
tree = ET.parse(story_path)
root = tree.getroot()
for elem in root.iter():
if strip_namespace(elem.tag) == "Content" and elem.text:
source = elem.text
if source in replacements:
elem.text = replacements[source]
tree.write(story_path, encoding="utf-8", xml_declaration=True)
def rebuild_idml(extracted_dir: Path, output_idml: Path) -> None:
with zipfile.ZipFile(output_idml, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for path in sorted(extracted_dir.rglob("*")):
if path.is_file():
zf.write(path, arcname=path.relative_to(extracted_dir))
def translate_idml_file(input_idml: Path, output_idml: Path, replacements: dict[str, str]) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
workdir = Path(tmpdir)
with zipfile.ZipFile(input_idml, "r") as zf:
zf.extractall(workdir)
for story_path in (workdir / "Stories").glob("*.xml"):
replace_story_content(story_path, replacements)
rebuild_idml(workdir, output_idml)
Where people usually break the file
Most corrupted IDML files come from one of these mistakes:
- Dropped XML structure: A serializer removes or rewrites something InDesign expects.
- Bad escaping: Raw
&,<, or>in translated text produces invalid XML. - Segment mismatch: One source string appears multiple times, but only one instance should change.
- Formatting loss: Inline style boundaries get merged because replacement happened at the wrong level.
Never treat translated XML as a string templating problem. Treat it as a tree mutation problem.
If you need to map extracted strings into gettext before writing them back, keep a dedicated .po domain for design assets so they don't get mixed with app UI. A refresher on how gettext PO files work helps when you need to preserve message identity across repeated exports.
A Practical IDML Workflow for Django Teams
A designer drops brochure.indd into Slack two days before a locale launch. The part that blocks automation is not the copy. It is the file format. INDD keeps you inside InDesign. IDML gives you something your Django stack can inspect, diff, and rebuild.
For a development team, the cleanest setup is to treat design copy as versioned source data. Store the exported .idml package in the repo, extract translatable text into its own gettext domain, and generate localized IDML outputs in CI. That keeps design assets on the same review path as templates, app strings, and content changes.
A workflow that holds up in production
Use a flow like this:
Commit the source asset
Put the exported
.idmlfile in your repo, for example:design/brochure/source/brochure.idmlExtract story text into a dedicated PO file
Generate something like:
locale/fr/LC_MESSAGES/django_marketing.po locale/de/LC_MESSAGES/django_marketing.po locale/ja/LC_MESSAGES/django_marketing.poYour extractor should create stable
msgidvalues from the source text and add enough context to distinguish repeated phrases when needed.Translate in the same pipeline as the app
Keep the command flow familiar:
django-admin makemessages --locale=fr django-admin compilemessagesIf you use an automated translation command in your project, run that against the dedicated marketing domain too.
Reinject
msgstrvalues into cloned IDML packagesWrite out localized deliverables such as:
design/brochure/build/brochure_fr.idml design/brochure/build/brochure_de.idml design/brochure/build/brochure_ja.idmlOpen only the output in InDesign
Let design do final copyfit and visual QA on the generated localized files, not on ad hoc manual edits.
This workflow works because each tool does one job. Django and gettext manage message identity, review, and locale state. Your IDML script handles XML extraction and reinsertion. InDesign stays at the end of the pipeline, where it belongs, for layout validation rather than string handling.
There are trade-offs. A dedicated django_marketing domain adds one more artifact to maintain, and repeated exports from design can invalidate message context if the source text moves between XML nodes. That is still easier to control than manual copy spreadsheets. Git history shows what changed, translators work from stable files, and rebuilds become repeatable.
If your team still passes brochure copy around as PDFs, comments, and hand-edited layouts, fix that first. Converting the asset flow to IDML plus PO files removes a lot of avoidable translation debt.
If you want that Django-side translation step to happen with one command instead of a portal, TranslateBot is built for exactly that workflow. It translates .po files in place, preserves placeholders and HTML, and fits into the same makemessages and compilemessages loop your team already uses.