Internationalization in Python Using the gettext Module
Python implements software internationalization (i18n) and
localization (l10n) through its built-in gettext module,
which interfaces with the standard GNU gettext translation framework.
The module works by decoupling source text from language-specific
translations: developers mark translatable strings in source code using
an alias (conventionally _()), extract them into Portable
Object (.po) template files for translators, compile the
translated catalogs into binary Machine Object (.mo) files,
and use the gettext runtime API to dynamically resolve
localized strings according to user locale settings.
The gettext Workflow
Implementing translations via gettext follows a
five-step lifecycle:
- Marking translatable strings in Python code.
- Extracting strings into a
.pot(Portable Object Template) file. - Translating messages within language-specific
.pofiles. - Compiling
.pofiles into binary.mofiles for fast lookup. - Loading and applying translations at runtime using
the
gettextAPI.
Step 1: Marking Strings in Python
To flag a string for translation, wrap it with a function named
_(). Python does not define _() by default, so
it must be mapped to a gettext function:
import gettext
# Basic string marking
_ = gettext.gettext
print(_("Hello, World!"))For plural forms, use ngettext, which selects the
appropriate singular or plural translation based on a numeric count:
count = 3
message = gettext.ngettext(
"You have %d new notification.",
"You have %d new notifications.",
count
) % count
print(message)Step 2: Directory Structure for Catalogs
The gettext module expects binary translation files to
follow a standard directory layout:
project/
│
├── main.py
└── locales/
├── es/
│ └── LC_MESSAGES/
│ ├── base.po
│ └── base.mo
└── fr/
└── LC_MESSAGES/
├── base.po
└── base.mo
locales/: The base directory containing all translations.es/,fr/: Language or locale codes (e.g., ISO 639-1 language codes, optionally with ISO 3166 territory codes likeen_US).LC_MESSAGES/: Standard gettext subdirectory.base: The "domain" name of the application, which groups related messages.
Step 3: Extracting and Compiling Translations
Extraction
Translatable strings are extracted into a .pot template
using GNU xgettext or Python's pygettext.py
tool:
xgettext -d base -o locales/base.pot main.pyTranslators copy base.pot to
locales/es/LC_MESSAGES/base.po and fill in the
msgstr entries:
msgid "Hello, World!"
msgstr "¡Hola, Mundo!"
msgid "You have %d new notification."
msgid_plural "You have %d new notifications."
msgstr[0] "Tienes %d nueva notificación."
msgstr[1] "Tienes %d nuevas notificaciones."
Compilation
Before Python can read the .po file, it must be compiled
to binary .mo format using GNU msgfmt:
msgfmt locales/es/LC_MESSAGES/base.po -o locales/es/LC_MESSAGES/base.moStep 4: Loading Translations at Runtime
Python's gettext module provides two distinct APIs for
loading .mo files: the Class-Based API and
the GNU gettext-Style Global API.
The Class-Based API (Recommended)
The class-based API encapsulates translation state within objects, avoiding global state modification. This is preferred for libraries, multi-threaded applications, and services handling multiple languages simultaneously:
import gettext
LOCALE_DIR = "locales"
DOMAIN = "base"
# Create a Translation object for Spanish, falling back to original strings if missing
lang_es = gettext.translation(DOMAIN, localedir=LOCALE_DIR, languages=["es"], fallback=True)
# Use the translation instance directly
_ = lang_es.gettext
print(_("Hello, World!")) # Output: ¡Hola, Mundo!To switch languages dynamically, instantiate a different
NullTranslations or GNUTranslations
object:
lang_fr = gettext.translation(DOMAIN, localedir=LOCALE_DIR, languages=["fr"], fallback=True)
print(lang_fr.gettext("Hello, World!")) # Output: Bonjour, le monde!To expose _() globally across builtins, call
install() on the translation instance:
lang_es.install()
# Now _() is available globally in the builtins namespace
print(_("Hello, World!"))The GNU gettext Global API
The global API mimics the C-language gettext interface by modifying the global environment. It is simpler for standalone CLI applications where only one locale is active during execution:
import gettext
gettext.bindtextdomain("base", "locales")
gettext.textdomain("base")
_ = gettext.gettext
print(_("Hello, World!"))Summary of Key
Functions in Python's gettext
| Function / Method | Description |
|---|---|
gettext.translation(...) |
Searches for, loads, and returns a GNUTranslations
object. |
gettext.gettext(message) |
Translates message according to the current global
domain. |
gettext.ngettext(singular, plural, n) |
Plural-aware translation in the global domain. |
GNUTranslations.gettext(message) |
Translates message using a specific translation
instance. |
GNUTranslations.ngettext(...) |
Plural-aware translation via a specific translation instance. |
GNUTranslations.install(...) |
Binds _() into Python's builtins namespace
globally. |