Multi-Language API Docs: Translating Content Without Forking the Spec
Quick answer: The OpenAPI contract is language-neutral; the prose around it is not. Translate the human-facing descriptions, not the structural schema, and keep every locale pulling from the same source so a field change cannot reach one language and miss another. Forking the spec per language is the path that guarantees drift; a single contract plus separate translation layers keeps the reference accurate in every locale.
I inherited a project that had done exactly the thing to avoid: three copied spec files, one per language, each edited independently. They started identical. Within a quarter, the English version had two new fields and a renamed parameter, while the other two lagged behind, and nobody noticed because the localized docs rendered without errors. The technical reference in those languages was quietly wrong. Consolidating to one source spec with external translations fixed the structural problem rather than chasing each mismatch.
What actually needs translating
It is worth separating the two kinds of content in a spec, because only one of them is locale-specific.
- Do not translate: field names, schema keys, enum values, operation paths, and the structural shape, which code and clients depend on.
- Translate: operation summaries and descriptions, parameter and property descriptions, doc titles, and any explanatory prose.
- Handle carefully: enum values that are machine tokens stay fixed; if users need a readable label, provide a separate display label per locale rather than changing the token.
Keeping that boundary clear means a generated client stays identical regardless of language, while the reading experience is fully localized.
Forked specs versus a translation layer
The two approaches look similar on the first day and diverge sharply over time. The table below is the comparison I use when deciding.
| Factor | One spec per language | Single spec plus translations |
|---|---|---|
| Schema change | Repeated in every file, easy to miss | Made once and reflected everywhere |
| Structural drift | Almost guaranteed over time | Impossible; the contract is shared |
| Translation review | Mixed with structural edits | Isolated in locale files |
| Generated clients | Risk of differing shapes | One identical client for all locales |
| Missing-key detection | Silent until a user reports | Can be checked in the build |
Structuring the translation files
A common pattern keeps the source spec in a base language and maps stable keys to per-locale strings, often keyed by JSON Pointer so a translated description is tied to an exact location.
// locales/fr.json
{
"paths./orders.post.summary": "Créer une commande",
"paths./orders.post.description": "Ouvre une nouvelle commande...",
"components.schemas.Order.properties.email.description":
"Adresse e-mail associée à la commande"
}
The build walks the source spec, applies the matching locale strings, and produces a localized view for rendering without ever mutating the base contract. Because the keys reference fixed locations, a moved or renamed field is visible immediately rather than silently orphaned.
Treat missing translations as a build signal
Decide explicitly how to handle a new English string that has no translation yet. The safe default is to fall back to the base language and report the gap, rather than rendering an empty description or blocking the entire release. Then run a coverage check that lists every missing key per locale so the localization work is a visible queue. This is the same discipline as i18n in an application: keys must align, and placeholders and interpolations must be preserved rather than machine-filled.
Keep technical terms and code samples consistent
Some terms should stay in English even in translated prose because they are recognized identifiers or library names, and a style guide should state which ones. Code samples stay unchanged in structure; only surrounding comments and prose are localized, and any placeholders inside a sample must remain intact. That prevents a well-meaning translation from turning a working snippet into code that does not run.
Locale formatting goes beyond words
Localization also covers how numbers, dates, and currency are presented, and those should be rendered with locale-aware formatting rather than baked into the translated strings. Keep the underlying values in the schema unchanged, then format them at render time so the same date reads correctly in each locale and a right-to-left language gets the correct direction and layout without changing the data. For right-to-left locales, check navigation and tables as well as prose, since a translated sentence that is correct can still sit inside a layout that was never built for reversed reading order.
Wire the check into the pipeline
Add a validation step that confirms every locale resolves against the current spec and that no translation points to a location that no longer exists. Run it on every change so a schema edit and its translation impact are surfaced together. The result is a single source of truth that stays structurally identical in every language, which is exactly what the forked-spec approach could not deliver.
FAQ
Why not just copy the spec for each language?
It is fast at first, but structural changes then have to be repeated in every file, and missed edits make the localized reference wrong. A shared contract keeps the schema identical while only the prose is translated.
Do field names and enum values get translated?
No. They are part of the machine contract and must stay fixed. If users need a readable label for an enum, provide a separate localized label rather than changing the underlying token.
What happens when a new string has no translation yet?
Fall back to the base language and flag the missing key in the build so localization is a tracked queue. Avoid empty descriptions and do not block the whole release on a single pending string.
How do I know a translation still points to a valid field?
Key translations by stable JSON Pointer and validate them in the pipeline. A translation referencing a removed or renamed location then fails the check instead of being silently dropped.
This is part of the Powerduck vs Mintlify hub — start there for the full comparison of API documentation tools in the AI era.