skip to content

Message Catalog Workflow

makemessages extracts marked strings into .po files, translators fill them, and compilemessages builds the .mo files Django loads. Interviewers probe fuzzy entries and LOCALE_PATHS lookup.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Django, what do makemessages and compilemessages each do, and why does a project need both .po and .mo files?

level: middleimportance: must knowfreq 52%

answer

  1. extract, translate, compile
  2. text for people, binary for gettext
  3. -l, --all, -d djangojs
  4. fuzzy entries are skipped

basics

~20 s

makemessages scans source files for marked strings and creates or updates a human-editable .po file per language. compilemessages turns each .po into the binary .mo file that Django actually loads at runtime. Translators edit .po; Django reads only .mo.

solid answer

~40 s

`manage.py makemessages -l de` runs GNU `xgettext` over `.py`, `.html` and `.txt` files and merges every marked string into `locale/de/LC_MESSAGES/django.po`. Existing translations are kept, new strings are added with an empty `msgstr`, and near-matches may be marked `fuzzy`. `--all` refreshes every language that already has a directory, and `-d djangojs` does the same for `.js` files into `djangojs.po`. Translators fill in the `msgstr` lines. `compilemessages` then runs `msgfmt --check-format` on each `.po` to produce a `.mo`, a compact binary catalog indexed for fast lookup. At runtime Django reads only the `.mo` files. An untranslated or never-compiled string falls back to the `LANGUAGE_CODE` translation and then to the source text, and fuzzy entries are left out unless you pass `--use-fuzzy`.

code

bash · 11 lines
bash
# From the project root (the directory with manage.py)
python manage.py makemessages -l de -l fr -l ja
python manage.py makemessages -d djangojs -l de -l fr -l ja

# ...translators fill in msgstr in locale/*/LC_MESSAGES/*.po...

python manage.py compilemessages

# Later, after new strings were marked in code:
python manage.py makemessages --all
python manage.py makemessages -d djangojs --all

go deeper

for a junior

Recall the order: mark strings, run makemessages -l, translate msgstr, run compilemessages.

for a middle

Explain what each command runs under the hood, .po versus .mo, obsolete and fuzzy entries, and the djangojs domain.

for a senior

Build compilation into the deploy pipeline, fail builds on msgfmt format errors, and restart workers when catalogs change.

for a principal

Decide whether .mo files are committed or built, and how translator handoffs fit the release cadence.

## The three-step workflow Django's translation system is built on **GNU gettext**. Getting a German version of a museum guide app takes three steps: 1. **Mark** strings in code and templates (`gettext`, `{% translate %}`, …). That is a separate topic. 2. **Extract** them into a message file per language with `makemessages`, and hand the file to translators. 3. **Compile** the translated file with `compilemessages` so Django can load it. Both commands shell out to the gettext tools (`xgettext`, `msgmerge`, `msguniq`, `msgattrib`, `msgfmt`), and Django requires version **0.19 or newer**. Without them, both commands stop with "Can't find xgettext" (or `msgfmt`) "… Make sure you have GNU gettext tools 0.19 or newer installed." ## `makemessages`: source → `.po` Run it from the project root or from an app's root: ```bash python manage.py makemessages -l de # one language python manage.py makemessages -l de -l fr # several python manage.py makemessages --all # every language that already has a directory python manage.py makemessages -d djangojs -l de # strings in .js files ``` What it does: - scans files with the default extensions `.html`, `.txt` and `.py` (`.js` for the `djangojs` domain; `-e` changes the list); - skips ignored paths: `CVS`, dot-directories, `*~`, `*.pyc`, anything matched by `-i`, and `STATIC_ROOT`/`MEDIA_ROOT` when settings are available; - merges the results into `locale/<locale>/LC_MESSAGES/django.po` (or `djangojs.po`). Existing translations are kept, and strings no longer found in the source become **obsolete** entries, which `--no-obsolete` removes; - refuses to run without a target: you must pass `-l`, `-x` or `--all`. Only the `django` and `djangojs` domains are supported. ## The `.po` file: for humans A `.po` file is **UTF-8 plain text** (no BOM allowed). Each entry pairs a source string with its translation: ```po #. Translators: label next to a painting's year #: exhibits/templates/exhibits/detail.html:14 msgid "Painted in %(year)s" msgstr "Gemalt %(year)s" ``` `msgid` is the source text and must not be edited. `msgstr` is the translation, empty until someone fills it in. Translators work in this file directly or in a gettext editor. Because it is plain text, it diffs cleanly and belongs in version control. ## `compilemessages`: `.po` → `.mo` `python manage.py compilemessages` finds every `.po` under `locale/` directories in the current tree, plus `LOCALE_PATHS` when settings are available. It runs `msgfmt --check-format` on each one to write a sibling `.mo` file. - `.mo` is a **binary, indexed catalog** that gettext can load and search quickly. Django never parses `.po` files at runtime. - `--check-format` makes compilation fail when a translation's placeholders don't match the source, for example a missing `%(year)s`. - **Fuzzy** entries are excluded unless you pass `--use-fuzzy` (`-f`). - Files whose `.mo` is at least as new as the `.po` are skipped as "already compiled and up to date". `-l` and `-x` limit the languages, and `-i` ignores directories. ## `.po` versus `.mo` | | `.po` | `.mo` | |---|---|---| | Format | plain UTF-8 text | binary | | Written by | `makemessages`, then translators | `compilemessages` (`msgfmt`) | | Read by | people and translation tools | gettext at runtime | | Contains comments, locations, fuzzy flags | yes | no | | Edit by hand | yes | never | ## A routine that keeps catalogs healthy 1. After strings change in code, run `makemessages --all`, plus `-d djangojs --all` if the front end has strings, and commit the updated `.po` files. 2. Send translators the `.po` files, or sync them with a translation platform. They fill in empty `msgstr` lines and clear `fuzzy` flags. 3. Run `compilemessages` in CI, so a placeholder mismatch fails the build instead of reaching production. 4. Ship the resulting `.mo` files with the release, and restart the application processes. Adding a new language is step 1 with `-l <locale>` instead of `--all`, since `--all` only refreshes languages that already have a directory. ## Where this bites in practice - Editing a `.po` and forgetting to compile it changes **nothing** at runtime. - Each process loads catalogs once and caches them. The development server's autoreloader clears that cache when a `.mo` changes, but production workers must be restarted after new `.mo` files are deployed. - Teams either commit `.mo` files or build them during deployment. Either works, as long as someone does it.

  • What does makemessages do to an existing translation when the msgid in the code changes slightly?
    It merges with `msgmerge`. The old entry no longer matches any source string, so it becomes obsolete, and a new entry is created for the new text. If the new text is close to the old one, gettext may pre-fill it with the old translation and flag it `fuzzy`. `compilemessages` skips fuzzy entries by default, so the new text shows in the source language until a translator reviews it and removes the flag.
  • Why must JavaScript strings be extracted with -d djangojs instead of -e js?
    Browser strings are served by the JavaScript catalog view, which reads the separate `djangojs` domain. `-d djangojs` scans `.js` files with a JavaScript-aware extractor and writes `djangojs.po`. `-e js` would push those strings into the Python `django` domain, where the JavaScript catalog never looks. Django's docs warn against it explicitly.

saying these in an interview costs you the question

  • Django reads the .po files directly, so compiling is optional.
  • Editing the .mo file is a quick way to fix a typo in a translation.
  • makemessages --all creates catalogs for every language in LANGUAGES.
  • compilemessages includes fuzzy entries by default.
  • JavaScript strings are extracted with -e js into django.po.
open as a page

In a Django-generated .po file, what do the msgid, msgstr, msgctxt, #., #: and #, lines of an entry mean?

level: juniorimportance: should knowfreq 30%

basics

~20 s

msgid is the source string and msgstr its translation; msgctxt, from pgettext, separates identical msgids. #. lines carry Translators: comments from the code, #: lines give source locations, and #, lines hold flags such as fuzzy or python-format.

open as a page

In a Django project, how do LOCALE_PATHS and each app's locale/ directory differ, and which wins when both translate the same string?

level: middleimportance: should knowfreq 34%

basics

~10 s

An app's locale/ directory ships translations with that app, while LOCALE_PATHS lists project-level directories. At runtime LOCALE_PATHS wins, earlier entries first, then apps' locale/ directories in INSTALLED_APPS order, then Django's own catalogs.

open as a page

A Django museum-guide site deploys an updated German .po file, yet several reworded exhibit texts still appear in English — what do you check, and in what order?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Check that compilemessages produced a fresh .mo and that it was deployed, that the changed entries are not marked fuzzy, that each msgid still matches the current source string, that the catalog sits in a searched locale directory, and that workers were restarted.

open as a page