Published on Sep 01, 2025
Sphinx Translation Tutorial: Localize Your Docs Like a Pro#
Localizing documentation, manual, or help is a challenging task. But it’s also an area where Sphinx documentation generator really shines. Sphinx gives you strong tooling for the entire lifecycle from initial translation to keeping translation(s) up to date with constantly changing original. In this tutorial, I will guide you through the full translation workflow using a minimal yet realistic Sphinx documentation example.
If you are in a hurry or want to see the result, check out my sphinx-doc-i18n-example GitHub repo.
Goals#
In this tutorial, I will demonstrate how to
Create a minimal Sphinx project
Build the docs languages set.
Extract strings and translate strings with gettext.
Keep translation and original in sync
Automate the process with Nox.
At the end of the tutorial, you will have
Minimal but real-world Sphinx documentation.
Primary language is English (a language of .rst documents)
Translations to Czech and Ukrainian.
Project using modern Python tooling like uv and nox.
Create a Sphinx project#
The bare minimum Sphinx project needs only two files: index.rst
and conf.py
. Moreover, the latter one, conf.py
, can be completely blank because all options have reasonable defaults.
Create a new folder for the Sphinx documentation. E.g.,
sphinx-translation-example
.Create a new subfolder
source
. It will contain Sphinx source files, such as .rst documents, images, and conf.py. Folder is sometimes called indir.Within
source/
createThe blank
conf.py
file. E.g., with thetouch conf.py
command in Linux-like environments)The
index.rst
with this content:
Welcome ======= Hello Sphinx! .. note:: This is a note.
Build docs languages set#
Let’s build the documentation for English, Czech, and Ukrainian. Translations will be identical to the English original at this moment.
Sphinx requires Python to be installed; however, no prior knowledge of Python is necessary to use it. The recommended way to install the Sphinx is the uv. The uv can even install Python if you don’t already have it.
Test building docs. Open and execute in the terminal the following command.
with Sphinx already installed by other means:
sphinx-build -b html source build
Read more about sphinx-build, if you are interested.
install and run Sphinx using uvx:
uvx --from sphinx sphinx-build -b html source build
It says to the uv to temporarily install the Sphinx package (in the latest version) and run the
sphinx-build
command from it. The command takes sources fromsource/
and outputs HTML tobuild/
. Read more about uvx, if you are interested.Important
From now on, I will omit the
uvx --from sphinx
part.
The previous
sphinx-build
invocation sets English because if not specified on the commandline or inconf.py
, Sphinx assumes English (en
).Build languages set with
-D language=<code>
argument tosphinx-build
. It overrideslanguage
from conf.py, if any. Even if English is the default, it’s better to be explicit.To build English (en), Czech (cs), and Ukrainian (uk) docs in respective
build/<code>/
subfolders run:$ sphinx-build -b html -D language=en source build/en $ sphinx-build -b html -D language=cs source build/cs $ sphinx-build -b html -D language=uk source build/uk
Hint
Always consult the language codes in Sphinx docs. Despite, e.g.,
uk
for Ukrainian works, the official code isuk_UA
.Open
build/<code>/index.html
in the browser to check everything looks okay. The pages are almost the same. They all contain original English strings, but Sphinx translates some parts of the website out of the box.For example, admonitions like “Note”, labels like “Navigation”, or the search “Go” button are automatically translated into the target language.
Gettext in Sphinx overview#
Sphinx supports localization using Gettext, the industry standard for software translation. There are many articles about this Gettext, including on our blog:
However, in essence, to know about gettext localization in Sphinx:
Gettext calls strings to be translated as messages, groups them into domains, and uses files with
.pot
,.po
, and.mo
extensions to get localization works.Textual .pot contains message templates (therefore “T” in .pot). When the gettext extract tool searches for strings, it initially stores them into .pot files.
In Sphinx docs, domains are documents. E.g., all strings found in intro.rst will be in the intro domain and intro.pot file. (Footnote: The text domain of individual documents depends on gettext_compact.)
There is always one POT per domain.
Textual .po file is for translated messages. They exist for non-primary languages only. In Sphinx, the files are stored in the
locales/
folder by default. E.g., for Czech and “intro” domain inlocales/cs/LC_MESSAGES/intro.po
.PO files are identical to POT, but have empty translations.
Binary .mo files are compiled .po files. The gettext-powered software, such as Sphinx, doesn’t look up messages in .po files, but instead reads an optimized binary version with the .mo extension. For each .po, a corresponding .mo exists. Sphinx compiles MO from PO automatically as needed at sphinx-build time.
Only PO files are versioned. Ignore POT because they are generated from source documents, and MO because they are compiled from PO files.
Extract strings#
Extracking is searching documents for a string to be translated. For extracting strings in Sphinx documents, such as index.rst
, we first need to run the Sphinx gettext builder. So far, we have been building in HTML. Gettext is another built-in Sphinx builder that produces .pot files instead of .html, .css, and .js.
Extract strings to
build/gettext/
:$ sphinx-build -b gettext source build/gettext
Examine
build/gettext/index.pot
. The POT/PO is a simple text format with couples ofmsgid
andmsgstr
. The former is the original string, and the latter is the translation (which is always empty in POT).The first pair of blank
msgid
andmsgstr
is a header with metadata.# SOME DESCRIPTIVE TITLE. # Copyright (C) # This file is distributed under the same license as the Project name not set package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: Project name not set \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-08-28 22:53+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n"
After the header, the actual strings follow. Our little
index.rst
has only three strings.#: ../../source/index.rst:2 msgid "Welcome" msgstr "" #: ../../source/index.rst:4 msgid "Hello Sphinx!" msgstr "" #: ../../source/index.rst:6 msgid "This is a note." msgstr ""
The
#: ../../source/index.rst:4
means the path to the document and the line number where the original string is located.
Translate strings#
It’s time to translate! From the POT templates, I will generate a PO for each target language. Translations in POT are always blank. That’s the only difference with PO. A translator’s job is to enter the translation into msgstr.
Create
locales/<code>/LC_MESSAGES/
folders for each localized (non-primary) language. For our case, you will end up withlocales/cs/LC_MESSAGES/
locales/uk/LC_MESSAGES/
(
locales/
is the default value of Sphinx conf.py locale_dirs. Using a different name is not recommended.)Copy all
.pot
files frombuild/gettext/
tolocales/<code>/LC_MESSAGES/
as.po
. You will end up with:locales/cs/LC_MESSAGES/index.po
locales/uk/LC_MESSAGES/index.po
Translate msgid into msgstr.
(For brevity, I am omitting the headers.) The Czech PO:
#: ../../source/index.rst:2 msgid "Welcome" msgstr "Vítejte" #: ../../source/index.rst:4 msgid "Hello Sphinx!" msgstr "Ahoj Sphinxi!" #: ../../source/index.rst:6 msgid "This is a note." msgstr "Toto je poznámka."
The Ukrainian PO:
#: ../../source/index.rst:2 msgid "Welcome" msgstr "Ласкаво просимо" #: ../../source/index.rst:4 msgid "Hello Sphinx!" msgstr "Привіт, Sphinx!" #: ../../source/index.rst:6 msgid "This is a note." msgstr "Це примітка."
Now rebuild the docs languages set and check out the files in the browser.
Sync translations with original#
Initial translation is a big task, but not as tricky as keeping translation in sync with the original in the long term. The source text is constantly changing, and it’s too easy to get out of sync with the original.
To simulate real-world scenarios, I will
Change “Hello Sphinx” -> “Hi Sphinx”. Hello and Hi are synonymous, so that it will have no impact on translations.
Delete “This is a note.” admonition.
Add a new “How are you?” paragraph.
Note
Strictly speaking, there’s only an add/delete modification because a change is actually a deletion and an addition.
To help, we will start using the sphinx-intl tool. Its command update
will update messages in PO according to new, fresh POTs without losing existing translations.
You can install and run sphinx-intl with uvx, too. Just run uvx sphinx-intl
and the command name. E.g., uvx sphin-intl stat
to display translation stats.
Update the contents of the index.rst to:
Welcome ======= Hi Sphinx! How are you?
Re-generate a new fresh POTs (
sphinx-build -b gettext ...
).Update message in PO according to POT without losing existing translations with the
sphinx-intl update
command.sphinx-intl update
Notice the output
Update: source/locales/uk/LC_MESSAGES/index.po +2, -2 Update: source/locales/cs/LC_MESSAGES/index.po +2, -2
The output informs us about the changes made and the number of changes. E.g., we made two additions and two removals.
With arguments to sphinx-intl update, you can you can specify locales folder, pot folder, language, etc. Since we are using default values (build/gettext/ for POTs, locales/ for POs), there is no need to override them. But from practice, I highly recommend being
explicit about paths
disable line wrapping (lines in POs are, by default, limited to 76) to reduce later Git conflicts (
-w 0
)
I.e.,
sphinx-intl update -d source/locales -p build/gettext -w 0
Examine the POs. For example, a Czech PO now looks like:
#: ../../source/index.rst:2 msgid "Welcome" msgstr "Vítejte" #: ../../source/index.rst:4 #, fuzzy msgid "Hi Sphinx!" msgstr "Ahoj Spjinxi!" #: ../../source/index.rst:6 msgid "How are you?" msgstr "" #~ msgid "This is a note." #~ msgstr "Toto je poznámka."
Please notice
The modification, the message “Hi Sphinx!”, has been marked as fuzzy (
#, fuzzy
). It means gettext thinks the translation might stay the same because the original has changed only a little (it was “Hello Sphinx!”). But gettext is unsure. By marking it with a fuzzy flag, it’s raising a translator’s attention.The removal, the message “This is a note.”, has been prefixed with
#~
, which means in Gettext “obsolete” message. It’s no longer used in sources, but it’s retained for historical purposes.(sphinx-intl update can with
--no-obsolete
delete these messages, but I usually prefer to keep them.)
Rebuild the docs language set and check out in the browser.
$ sphinx-build -b html -D language=en source build/en $ sphinx-build -b html -D language=cs source build/cs $ sphinx-build -b html -D language=uk source build/uk
Note the fuzzy (“Hi Sphinx”) and missing (“How are you?”) messages fall back to the original language.
Fix translations. Remove fuzzy and fill-in translation. E.g., Ukrainian:
#: ../../source/index.rst:2 msgid "Welcome" msgstr "Ласкаво просимо" #: ../../source/index.rst:4 msgid "Hi Sphinx!" msgstr "Привіт, Sphinx!" #: ../../source/index.rst:6 msgid "How are you?" msgstr "Як справи?" #~ msgid "This is a note." #~ msgstr "Це примітка."
Rebuild the docs language set and check out in the browser. No change to English, but the remaining locales are fixed now.
Automate with Nox#
Writing the above commands in the terminal again and again is difficult and error-prone. In the Python world, there are tools like Nox which centralize all “tasks” you might need to do with the project. Nox is like Make for C programmers, or package.json scripts for Node.js programmers.
I am not going to explain Nox here, so only in a hurry:
In Nox, tasks are written as a Python script called
noxfile.py
in the root.The tasks in Nox are properly called sessions, but I will call them tasks.
We will need, actually, only two tasks: update translation and build the docs language set.
The resulting noxfile.py:
import nox
# Speed up builds by uv and reusing virtualenvs
nox.options.reuse_existing_virtualenvs = True
nox.options.default_venv_backend = "uv"
@nox.session
@nox.parametrize("language", ["cs", "uk"])
def gettext(session, language):
"""Generate .pot files and update .po files."""
session.install("sphinx==8.1.3", "sphinx-intl==2.3.2")
# Generate .pot files from Sphinx
session.run("sphinx-build", "-b", "gettext", "source", "build/gettext")
# Update .po from .pot templates
session.run(
"sphinx-intl",
"update", # update .po files
"-p", # from .pot files at
"build/gettext",
"-l", # for language
language,
# No line wrapping
"-w",
"0",
)
@nox.session
@nox.parametrize("language", ["en", "cs", "uk"])
def build(session, language):
"""Build documentation for a language."""
session.install("sphinx==8.1.3")
session.run(
"sphinx-build",
"-b",
"html",
"-D",
f"language={language}",
"source",
f"build/{language}",
)
For Pythonistas, the script should be self-explanatory. For everybody, here are some comments:
To massive performance improvement, I again use uv. If you don’t have it installed, delete the
nox.options.default_venv_backend = "uv"
line.The task
gettext
will install Sphinx and sphinx-intl, extract POTs, and create/update PO for Czech and Ukrainian (["cs", "uk"]
)The task
build
builds the docs for all languages (["en", "cs", "uk"]
).To call a task, run
nox -s <name>
, e.g.,nox -s gettext
. To run Nox without installing using the uvx, typeuvx nox -s gettext
.To call multiple tasks, run
nox -s <name1> <name2>
.To call all tasks from top to bottom, just run
nox
. In our case, it means togettext
followed bybuild
.
Summary and workflow cheatsheet#
With nox, a simple no-brainer cheatsheet is:
On every text change, run
nox -s gettext
to update POs and commit.Translate missing or fuzzy messages before publishing the docs.
To build the docs language set, run
nox -s build
and add some language selection.In other words, your everyday workflow is gettext -> build, which is launched by just running
nox
.
I hope you enjoyed the tutorial and it convinced you that localizing Sphinx documentation projects is just more reason why Sphinx is the best documentation generator.