Multi-Language¶
Sphinx ships a full internationalisation pipeline based on GNU
gettext, the same toolchain that translates the Python interpreter,
the GNU coreutils, and most of the Linux desktop. It is not opt-in
per theme; you get it for free the moment you install Sphinx, and
sphinx-hextra inherits it without needing any theme-specific
configuration. This page walks through the workflow.
A caveat up front: sphinx-hextra’s own UI chrome — the “On this
page” heading above the right-hand TOC, the light/dark toggle button
label, the search box placeholder — is currently hard-coded in
English. The content you author gets translated end-to-end; the
surrounding theme chrome does not. If this is a blocker, please open
an issue or a pull request at
github.com/patdhlk/sphinx-hextra
— translating the theme is a finite amount of work we are happy to
accept contributions for.
The gettext workflow¶
Translation has three phases: extraction, translation, and
building. Extraction scans your Sphinx source for translatable
strings and writes them into .pot template files. Translators
create a .po file per target language from each .pot. At build
time, Sphinx reads the .po files and emits a fully translated HTML
tree.
Step 1 — Extract¶
Tell Sphinx to extract strings from your source:
sphinx-build -b gettext docs docs/_build/gettext
This produces one .pot file per .md or .rst source file under
docs/_build/gettext/. A .pot is the untranslated template — think
of it as the schema for a translation.
Step 2 — Initialise or update translations¶
Install sphinx-intl once (uv add sphinx-intl or pip install sphinx-intl) and use it to create .po files per locale:
sphinx-intl update -p docs/_build/gettext -l de -l fr -l ja
This creates docs/locale/de/LC_MESSAGES/, docs/locale/fr/…, and
docs/locale/ja/…, each containing one .po per source file.
Re-running the same command after a content update merges new strings
into existing .po files without discarding already-translated
entries.
Step 3 — Translate¶
A .po file is plain text with blocks of this shape:
#: ../../getting-started.md:15
msgid "Install"
msgstr ""
Fill in msgstr with the translation. Translators usually use an
editor like Poedit or
Lokalize which wraps the same files
in a friendlier UI, but a plain text editor works too.
Step 4 — Build per language¶
Point sphinx-build at a locale by passing the language config:
sphinx-build -b html -D language=de docs docs/_build/de
sphinx-build -b html -D language=fr docs docs/_build/fr
sphinx-build -b html -D language=ja docs docs/_build/ja
Each build produces a fully translated HTML tree. The usual deployment
pattern is to publish each locale under its own path prefix:
/de/…, /fr/…, /ja/…, with the default English build at the
root.
conf.py settings¶
The two settings you want in conf.py:
language = "en"
locale_dirs = ["locale/"]
gettext_compact = False
languageis the source language of the.mdfiles. Sphinx uses this to pick the right date formats and to select the.podirectory when building translated output. You override it per build with-D language=….locale_dirslists directories to search for.pofiles.locale/is the convention and matches whatsphinx-intl updateproduces.gettext_compact = Falsetells Sphinx to produce one.potper source file instead of collapsing them into per-directory bundles — which makes diffs clearer and lets you split translation work per file.
Localising URLs and file names¶
By default, translated pages keep the original file names — an
English getting-started.md is still getting-started.html in
German. If you want the URLs themselves translated (premiers-pas.html
for French), look at
sphinx-polyglot or build
a post-processing step. Sphinx itself does not rename files based on
language, because doing so breaks cross-references and search
indexing.
Theme chrome translation¶
The strings sphinx-hextra puts into the rendered HTML — “On this
page”, “Search”, “Edit this page” (reserved), the theme toggle aria
labels — live inside layout.html and its partials as plain text. In
v0.1 they are not wrapped in {% trans %} blocks, which means even if
you have translated all your content, the surrounding chrome stays in
English. The roadmap for v0.2 is to wrap every chrome string in a
{% trans %} block and ship a default en.po, at which point
translating the chrome becomes a matter of dropping a de.po next to
it. Until then, the workaround is to override layout.html via
_templates/layout.html (see the template-overriding section in
Customization) and hard-code
the strings in your target language.