Tutorial¶
This page goes from an empty directory to a program that greets in Japanese. Five steps, no gettext experience assumed, and every command is shown with the output it actually produces — so at each step you know whether you are on track.
You need Python 3.14 or newer, because t-strings are new syntax in 3.14.
Japanese is this page's example target, but nothing depends on that choice. To
use another language, replace ja in step 4 — that locale code is the only
thing that names it.
1. Install¶
The [babel] extra brings in Babel, the tool that collects your messages
into catalog files in step 3. It is a development-time tool: production code
renders with the standard library alone.
2. Mark a message in your code¶
Create app.py:
t"Hello {name}" looks like an f-string, but the t prefix keeps the text
and the value separate instead of merging them on the spot. That separation is
what lets tr() look up a translation for the whole sentence Hello {name}
and insert the value afterwards.
Run it now:
No translations are installed yet, so the source text renders as-is. A program using this library never requires a catalog to run — English (or whatever your source language is) is the built-in fallback.
3. Extract the messages¶
Translators usually work from catalogs rather than from source code, so a small file called a catalog travels between you and them. The first step toward one is collecting every marked message out of the code.
Tell Babel how to find your messages by creating babel.cfg:
Then extract into a template file (.pot):
$ mkdir -p locales
$ pybabel extract -F babel.cfg -c "Translators:" -o locales/messages.pot .
extracting messages from app.py (encoding="utf-8")
writing PO template file to locales/messages.pot
locales/messages.pot now contains one entry per message:
msgid is the key your code will look up. The empty msgstr is where a
translation goes — but not in this file: a .pot is a template, and the
next step copies it once per language.
4. Translate and compile¶
Create the Japanese catalog from the template:
$ pybabel init -i locales/messages.pot -d locales -l ja
creating catalog locales/ja/LC_MESSAGES/messages.po based on locales/messages.pot
Open locales/ja/LC_MESSAGES/messages.po and fill in the msgstr:
Keep {name} exactly as it is — the placeholder is how the value finds its
place inside the translated sentence, and the translation is free to move it
wherever the target language needs it. On a real project this .po file is
what you hand to a translator or upload to a translation platform; the format
is the same either way.
Catalogs are edited as text but loaded in a binary form (.mo), so compile:
$ pybabel compile -d locales
compiling catalog locales/ja/LC_MESSAGES/messages.po to locales/ja/LC_MESSAGES/messages.mo
This command is also a safety net. Had the translation damaged the
placeholder — {nome} instead of {name}, say — it would refuse to pass:
$ pybabel compile -d locales
error: locales/ja/LC_MESSAGES/messages.po:24: translation does not match the
source placeholders: {name} is missing; {nome} is not in the source message
1 errors encountered.
One caveat worth knowing now: it reports the error and exits non-zero, but
writes the .mo anyway. On a real project it is CI that has to stop on that
exit status — In production sets that up.
5. Run it¶
Steps 2–4 used tr(), which looks for a catalog and finds none. Now that one
exists, load it and bind it once: Translator holds a catalog so the call
sites do not have to name it, and _ is the conventional gettext name for the
result.
Point app.py at the compiled catalog. Click the markers to see what each
line is doing:
import gettext
from gettext_tstrings import Translator
_ = Translator(gettext.translation("messages", localedir="locales", languages=["ja"])) # (1)!
name = "Ada"
print(_(t"Hello {name}")) # (2)!
- The standard library loads the compiled
.mo, andTranslatorbinds it to a callable._is the conventional gettext name for "translate this" — short because it appears on every user-facing string. It performs the same translation astr, bound to one catalog. - At the call: the t-string's text becomes the lookup key
Hello {name}, the catalog answersこんにちは {name}, the answer is checked against the source placeholders, and only then is the value put in.
That is the whole loop, and it is worth seeing as one picture:
flowchart LR
mark["1–2 mark<br>t-strings in code"] --> extract["3 extract<br>messages.pot"]
extract --> translate["4 translate<br>ja/…/messages.po"]
translate --> compile["4 compile<br>ja/…/messages.mo"]
compile --> run["5 run<br>こんにちは Ada"]
Mark → extract → translate → compile → run. Everything else on this site is a refinement of one of those five steps.
Where next¶
- Why t-strings — what this design protects you from,
compared to
%(name)s,.format(), and$-strings. - Guide — plurals, per-request languages, deferred strings, and what happens at runtime when a catalog is wrong anyway.
- In production — this same loop as a team runs it, week after week: updating catalogs, CI gates, and translation platforms.
- Extraction — the full
pybabelreference: custom function names, strict CI mode, and the checks that guard your catalogs. - Migration — if the project you actually want to do this in already has gettext catalogs.
- For translators — the one page to hand to whoever fills in
those
msgstrlines.