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:

  1. Marking translatable strings in Python code.
  2. Extracting strings into a .pot (Portable Object Template) file.
  3. Translating messages within language-specific .po files.
  4. Compiling .po files into binary .mo files for fast lookup.
  5. Loading and applying translations at runtime using the gettext API.

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

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.py

Translators 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.mo

Step 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 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.