Compile Django Translations During Deployment

Ship Django translations reliably: run compilemessages in CI or a Docker build stage with gettext, ignore virtualenvs, validate .po files, restart workers, and verify every language in production.

Problem statement

A Django site with translations works in development but shows English text in production, or shows new strings untranslated after a deploy, or crashes the build with CommandError: Can't find msgfmt. The cause is almost always the gap between .po files (the source translations in the repository) and .mo files (the compiled binary catalogues Django actually reads at runtime).

Django never compiles translations automatically. This guide adds compilemessages to the release process in a way that works with Docker, CI, and plain servers, and adds checks so broken translation files fail the build instead of production.

Translations in a release.po filesin the repositorycompilemessagesneeds GNU gettext.mo filesshipped with releaseRestart workersload new catalogsDjango reads compiled .mo files only; editing .po files changes nothing until they are compiled.
  1. Keep .po files in Git; treat .mo files as build output (add *.mo to .gitignore), or commit them deliberately, but not both.
  2. Install GNU gettext where the build runs (apt-get install gettext).
  3. Run python manage.py compilemessages --ignore=.venv --ignore=venv --ignore=node_modules in CI or the Docker build.
  4. Validate catalogues with msgfmt --check so syntax errors and format mismatches fail the build.
  5. Restart Gunicorn and workers after deploying new catalogues.
  6. Verify each language on the live site after the deploy.

Step-by-step solution

1. Confirm the i18n settings

USE_I18N = True
LANGUAGE_CODE = "en"
LANGUAGES = [("en", "English"), ("de", "Deutsch"), ("fr", "Français")]
LOCALE_PATHS = [BASE_DIR / "locale"]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.locale.LocaleMiddleware",
    "django.middleware.common.CommonMiddleware",
    # ...
]

LocaleMiddleware goes after SessionMiddleware and before CommonMiddleware. LOCALE_PATHS points at project-level translations; apps can also have their own locale/ directories.

2. Decide: build or commit .mo files

Build them (recommended): .mo files are generated from .po files, so committing them duplicates data and invites stale binaries. Add to .gitignore:

*.mo

Commit them only if your deploy target cannot run a build step. Then make CI fail when committed .mo files are out of date (compile and check git diff --exit-code).

where .mo files should come fromCompile in the buildCommit .mo filessource of truth✓.po files only.po and .mostale binaries✓impossible✕easy to forgetgettext neededbuild stage only✓nowhereCI checkmsgfmt --check-formatrecompile + git diffCompile during the build unless the deploy target cannot run a build stepdjango-deployment.com
Compiled catalogues belong to the release they were built with, like collected static files.

3. Compile in a Docker build stage

FROM python:3.12-slim AS build
RUN apt-get update && apt-get install -y --no-install-recommends gettext && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
COPY . .
RUN PYTHONPATH=/install/lib/python3.12/site-packages \
    DJANGO_SETTINGS_MODULE=config.settings.build \
    python manage.py compilemessages --ignore=.venv --ignore=venv --ignore=node_modules

FROM python:3.12-slim
WORKDIR /app
COPY --from=build /install /usr/local
COPY --from=build /app /app
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]

The runtime image does not need gettext: Python's gettext module reads .mo files natively. Only the msgfmt tool used by compilemessages requires the system package.

compilemessages searches the current directory tree and LOCALE_PATHS for .po files. Without --ignore, a virtual environment inside the project directory makes it recompile translations of installed packages (which already ship .mo files) and can fail on files it cannot write.

4. Compile on a plain server

In a release script, after installing dependencies and before switching the live release:

sudo apt-get install -y gettext   # once, during provisioning
cd "$RELEASE"
.venv/bin/python manage.py compilemessages --ignore=.venv
.venv/bin/python manage.py collectstatic --noinput

Building in CI and shipping the result is better still: every server receives identical catalogues.

5. Validate translations in CI

compilemessages fails on some errors but tolerates others. Run msgfmt --check explicitly, which also verifies that format placeholders in translations match the source (%(name)s in English must stay %(name)s in German):

find locale -name "*.po" -print0 | xargs -0 -n1 msgfmt --check-format -o /dev/null

A translation that drops or renames a placeholder raises KeyError or ValueError at runtime, only for users of that language. Catching it in CI prevents a class of production errors that tests in English never see.

Also check for missing translations so they do not ship silently:

msgfmt --statistics -o /dev/null locale/de/LC_MESSAGES/django.po

6. Restart processes after deploying

Django loads translation catalogues once per process and caches them. After new .mo files are deployed, restart Gunicorn and Celery workers (a restart or a Gunicorn HUP that replaces workers). If you deploy new releases into new directories and restart as part of the release, this happens automatically.

what the symptom points atSymptomCausePages stay in English.mo files not compiled or not deployedKeyError only in Germanplaceholder changed in atranslation; msgfmt --check-formatNew strings untranslatedfuzzy entries skipped by msgfmtOld text after a deployGunicorn and workers not restartedAll four are release-process problems, not Django bugsdjango-deployment.com
Validating format placeholders in CI catches the only one of these that raises errors for users.

Verification check:

find locale -name "*.mo" | head
curl -s -H "Accept-Language: de" https://example.com/ | grep -o "<html[^>]*lang=\"[a-z-]*\""
curl -s https://example.com/de/ | grep -c "Anmelden"

Use a string that you know is translated. If you use i18n_patterns, check the language-prefixed URLs; otherwise check with Accept-Language headers or the language cookie.

Explanation

.po files are human-editable text; .mo files are compact, indexed binaries that Python's gettext loads at runtime. compilemessages calls GNU msgfmt for each catalogue it finds. Since Django only reads .mo files, a release that ships updated .po files without compiling them looks exactly like a release with no translation changes. Treating compilation as a build step, like collectstatic, ties the compiled catalogues to the code release they belong to.

Edge cases / notes

Fuzzy entries

Entries marked #, fuzzy by makemessages are skipped by msgfmt unless compiled with --use-fuzzy, so they appear untranslated in production. Review and clear fuzzy flags before release; msgfmt --statistics reports them.

JavaScript translations

Strings in JavaScript use the djangojs domain and JavaScriptCatalog view. They compile the same way, but cached JavaScript catalogue responses may need cache busting after changes.

makemessages belongs in development

Do not run makemessages during deploy. It rewrites .po files from source code, and the translations for new strings do not exist yet. Run it in development, send files to translators, and commit the results.

When to automate this

Add the compile and msgfmt --check-format steps to CI so every pull request validates translations. Translation management platforms can open pull requests with updated .po files automatically, which then flow through the same checks.

Next steps

FAQ

Why are my Django translations not showing in production?

The .mo files were not compiled or deployed, LocaleMiddleware is missing or misplaced, or the processes were not restarted after new catalogues were deployed.

Should I commit .mo files to Git?

It is better to compile them during the build. Commit them only if the deploy environment cannot run compilemessages, and check in CI that they match the .po files.

Why does compilemessages say it can't find msgfmt?

GNU gettext is not installed where the command runs. Install the gettext package in the build environment.

Why does compilemessages fail on files in my virtualenv?

It searches the whole directory tree. Use --ignore for the virtual environment and other vendor directories.

Keep going