PO File Translation¶
TranslateBot Django automatically translates your Django .po (gettext) files, which are the standard way Django handles internationalization.
How It Works¶
- Scans your project for
.pofiles (django.poanddjangojs.po) in:LOCALE_PATHSdirectories- App-specific
locale/directories (apps installed insite-packagesare skipped) - The
locale/directory inBASE_DIR, only whenLOCALE_PATHSis empty
- Identifies entries to translate: empty and fuzzy ones
- Sends messages to the AI model with instructions to preserve formatting, along with their
msgctxtand translator comments (LLM providers only) - Updates the
.pofile with translations, saving after every batch - Skips obsolete entries and third-party packages automatically
Basic Usage¶
Translate to a Specific Language¶
Translate to All Languages¶
If you have LANGUAGES defined in settings:
This translates to every language in LANGUAGES except the source language (LANGUAGE_CODE).
Preview Mode¶
See what would be translated without making API calls or changes:
Dry Run Output
ℹ️ Found 15 untranslated entries
🔍 Dry run mode: skipping translation
Processing: /path/to/locale/nl/LC_MESSAGES/django.po
✓ Would translate 'Welcome to our site'
✓ Would translate 'Hello, %(name)s!'
...
Dry run: 15 entries would be updated in /path/to/locale/nl/LC_MESSAGES/django.po
============================================================
Dry run complete: 15 entries would be translated across 1 file(s)
Dry run mode skips the translation API entirely, so there are no costs. It lists which strings need translation, not what they would be translated to.
Re-translate Existing Entries¶
By default, only empty and fuzzy entries are translated. Translated fuzzy entries lose their fuzzy flag. To re-translate everything:
Warning
This will replace all existing translations. Consider using --dry-run first.
Plural Forms¶
TranslateBot handles Django's plural forms (msgid_plural / msgstr[n]) automatically. Already-translated plural entries are skipped, just like regular entries.
With an LLM provider, every plural form of the target language gets its own translation. TranslateBot reads the Plural-Forms header of the .po file and tells the model which counts use which form. makemessages writes this header for most languages; if it's missing or still the nplurals=INTEGER; plural=EXPRESSION; placeholder, a warning is logged and that file's plurals are translated like with DeepL (below). Polish, for example, has three:
| Form | Used for | Example |
|---|---|---|
msgstr[0] |
1 | %(count)d plik |
msgstr[1] |
2, 3, 4, 22, 23, … | %(count)d pliki |
msgstr[2] |
0, 5, 6, 7, 8, … | %(count)d plików |
DeepL
DeepL can't translate into specific plural forms. With DeepL, the singular and plural are translated as two strings, and the plural translation is used for every form after the first. Review plural entries for languages with more than two forms (Polish, Russian, Czech, Arabic, …).
Message Context¶
Entries with the same msgid but a different msgctxt (from pgettext() or {% translate "May" context "month" %}) are translated separately, and the context is passed to the model to tell them apart:
Extracted translator comments (#. lines, from # Translators: comments in your code) are passed along in the same way.
DeepL
DeepL doesn't receive the context or comments, so with DeepL entries that differ only in msgctxt are translated identically.
A message that appears in several .po files for the same language, such as django.po and djangojs.po, is translated once and written to every file where it still needs a translation. Files that use a different app-level TRANSLATING.md get their own translation.
Placeholder Preservation¶
TranslateBot preserves all Django placeholders:
| Type | Example | Preserved |
|---|---|---|
| Named | %(name)s, %(count)d |
Yes |
| Positional | %s, %d |
Yes |
| Format strings | {0}, {name} |
Yes |
| HTML tags | <strong>, <a href="..."> |
Yes |
| Line breaks | \n |
Yes |
Example¶
Source:
Translated (Dutch):
Translation Validation¶
Every translation is checked before it's written, using the rules msgfmt --check-format (run by compilemessages) applies to python-format and python-brace-format entries:
- Named placeholders and
{fields}must match the source and keep their type:%(n)dcan't become%(n)s. - Positional
%s/%dmust match in number and order. - A literal percent sign must stay
%%. - A
{field}'s format spec must stay as in the source:{a:.2f}can't become{a:,.2f}or{a}. - Plural forms are checked against
msgid_pluraland may leave named placeholders out ("one file"), except in languages with a single form (Japanese, Chinese, …).
Empty translations and index markers (#1, #2, …) are rejected too.
Rejected translations are retried once, on their own. If they fail again, they don't break compilemessages: an untranslated entry gets the translation as a draft marked #, fuzzy. compilemessages skips fuzzy entries (unless run with --use-fuzzy), PO editors flag them for review, and the next translate run translates them again. A draft never replaces an existing translation, not even with --overwrite.
The run ends with a list of every rejected translation, per file:
⚠️ 1 translation(s) failed validation, also on retry:
locale/nl/LC_MESSAGES/django.po
'Welcome back, %(name)s!' (marked fuzzy): missing placeholders: %(name)s
To fix one by hand, correct the placeholders in your PO editor and remove the fuzzy flag. Drafts are fuzzy entries, so check_translations fails until they're reviewed.
Intelligent Batching¶
TranslateBot groups strings into batches based on the AI model's token limits. This:
- Maximizes efficiency by reducing API calls
- Handles large projects with thousands of strings
- Automatically adjusts batch size based on content length
Each batch's expected output is kept under 8,000 tokens, even when the model allows more, because long JSON responses get less reliable. If LiteLLM doesn't recognize the model name, TranslateBot logs a warning and falls back to small batches (8,192 input / 4,096 output tokens). The run still works, but it makes more API calls.
Translations are saved after every batch, so if a later batch fails, the earlier ones are kept. Rerunning the command picks up where it stopped (unless you use --overwrite, which starts over).
Workflow Example¶
# 1. Mark strings in your code
from django.utils.translation import gettext as _
message = _("Welcome to %(site_name)s!")
# 2. Generate .po files
python manage.py makemessages -l de -l fr -l nl
# 3. Preview translations
python manage.py translate --dry-run
# 4. Apply translations
python manage.py translate
# 5. Compile for use
python manage.py compilemessages
Best Practices¶
Use Dry Run First
Always preview translations with --dry-run before applying them to catch any issues.
Commit Before Translating
Commit your .po files to version control before running translations, so you can easily revert if needed.
Provide Context
Create a TRANSLATING.md file in your project root to provide domain-specific context, terminology guidelines, and tone preferences. See Translation Context for details.
Review Translations
While AI translations are generally good, consider having a native speaker review critical user-facing text.
Next Steps¶
- Learn about Model Translation for database content
- See all Command Options