Languages
Translate your documentation by adding a languages.json file and one folder per language. Each language gets its own URL prefix, its own sidebar, a switcher in the sidebar, hreflang tags for search engines, and search and AI chat that stay inside the language the reader is looking at.
Languages are entirely opt-in. Without a languages.json file nothing changes: your docs live at the root of the docs folder and publish exactly as before.
Configuration
Create languages.json in your project root, next to doccupine.json:
[
{ "code": "en", "label": "English", "default": true },
{ "code": "de", "label": "Deutsch" }
]- code: a lowercase URL segment (letters, digits, and hyphens, such as
en,de, orpt-br). It is the folder name, the URL prefix, and the value of the page'slangandhreflangattributes. - label: the name shown in the language switcher.
- default: exactly one entry must be the default. Its pages live at the docs root and keep unprefixed URLs.
- strings (optional): overrides for the site's own interface text in that language. See Interface strings.
The order of the entries is the order of the switcher.
Folder layout
The default language lives at the root of the docs folder. Every other language lives in a folder named after its code, mirroring the default layout:
docs/
index.mdx -> /
guides/
intro.mdx -> /guides/intro
de/
index.mdx -> /de
guides/
intro.mdx -> /de/guides/introA page is the translation of another when it sits at the same path inside its language folder. Only translated pages exist in a language: a page you have not translated yet returns a 404 under that language's prefix, and the switcher takes readers to that language's home page instead.
A language folder without an index.mdx gets a generated home page that redirects to its first page.
What the generated site does
- Switcher: the sidebar footer row, between the focus-mode and theme toggles, shows a language dropdown when at least two languages are configured: a 30px pill carrying the current language that opens a menu of every language above it (the same dropdown the Doccupine platform uses), with the arrow keys, Escape, and a click outside to close it. When a version switcher shares the row, the two pills grow from their labels' widths to fill the sidebar's free width, and on the narrow desktop rail each shows its short form: the language's code and the version's own label. Switching goes to the same page in the chosen language when it exists, otherwise to that language's home.
- Sidebar and sections: the sidebar lists only the pages of the current language. Sections work inside every language; a section tab is hidden in a language that has no pages in it.
- Search and AI chat: results and answers come from the current language only. The MCP server's
search_docsandlist_docstools accept alanguageparameter and default to the default language. - SEO: every translated page carries
alternates.languages(hreflang) links to its other languages, anx-defaultpointing at the default language, and an Open Graph locale. The sitemap lists the same alternates. The<main>element carries the page'slangattribute; the<html lang>attribute is the default language server-side and is corrected from the URL before the first paint. - Agents: each language has its own
llms.txtandllms-full.txtunder its prefix (/de/llms.txt), and the root files link to them.
Navigation
Frontmatter navigation is built per language automatically. To define a language's sidebar by hand, key navigation.json by URL prefix: "de" for the German root, "de/api" for the German API section. See Navigation.
Interface strings
The generated site's own labels (the search box, "On this page", the previous and next links, the action bar, the footer) are English by default. Override any of them per language with a strings object:
[
{ "code": "en", "label": "English", "default": true },
{
"code": "de",
"label": "Deutsch",
"strings": {
"searchPlaceholder": "Dokumentation durchsuchen...",
"onThisPage": "Auf dieser Seite",
"previous": "Zurück",
"next": "Weiter"
}
}
]Keys you leave out keep their default. The known keys are searchButton, searchDialog, searchPlaceholder, searchResults, searching, noResults, resultsAvailable (with a {count} placeholder), resultAvailable, askAi, askAiAssistant, closeSearch, onThisPage, previous, next, openNavigation, closeNavigation, expandGroup and collapseGroup (both with a {title} placeholder), enterFocusMode, exitFocusMode, focusMode, copyContent, copied, copyCode, codeVariants, rss, rssFeed, viewAsMarkdown, toggleView, poweredBy, githubLink, aiAssistant, resetChat, chatGreeting, chatPlaceholder, chatInputLabel, chatAnswering, chatError, language, and version. An unknown key is reported when the site is generated and ignored.
Rules
- A language code must not be the slug of a section, the first folder of a section's
directory, a version slug, or one of the reserved segmentsapi,gate,mcp, andingest. The build stops with a message naming the file when it is. - The default language has no folder: a
docs/en/folder on a site whose default isenstops the build. - When versions are configured too, the language folder comes first:
docs/de/v1/. - The API reference generated from an OpenAPI spec is published in the default language only.