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.
- Keep
.pofiles in Git; treat.mofiles as build output (add*.moto.gitignore), or commit them deliberately, but not both. - Install GNU gettext where the build runs (
apt-get install gettext). - Run
python manage.py compilemessages --ignore=.venv --ignore=venv --ignore=node_modulesin CI or the Docker build. - Validate catalogues with
msgfmt --checkso syntax errors and format mismatches fail the build. - Restart Gunicorn and workers after deploying new catalogues.
- 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).
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.
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
- Add the compile step to your image with How to Dockerize a Django App for Production.
- Run the validation in CI with Deploy Django with GitHub Actions CI/CD.
- If translations still look old after deploy, see Fix Django Code Changes Not Showing After Deploy.
- Review the whole release process with the Django Deployment Checklist for Production.
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.