In Django, what do makemessages and compilemessages each do, and why does a project need both .po and .mo files?
answer
- extract, translate, compile
- text for people, binary for gettext
- -l, --all, -d djangojs
- fuzzy entries are skipped
basics
~20 smakemessages 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# 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 --allgo deeper
Recall the order: mark strings, run makemessages -l, translate msgstr, run compilemessages.
Explain what each command runs under the hood, .po versus .mo, obsolete and fuzzy entries, and the djangojs domain.
Build compilation into the deploy pipeline, fail builds on msgfmt format errors, and restart workers when catalogs change.
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.