How do you implement translation in a Django project?

Django translation runs on GNU gettext: you mark strings with gettext_lazy() in Python or {% translate %} in templates, run django-admin makemessages to extract them into a .po message file per locale, translate that file, then run django-admin compilemessages to produce the binary .mo file Django loads at request time. The framework handles extraction, locale detection through LocaleMiddleware, and runtime lookup; it does not handle the translation itself. That middle step — getting a .po file translated into 10 or 30 languages and back into the repository — is where a translation management system such as Smartling, which parses gettext .po and .pot files natively, replaces manual file hand-offs.

Last reviewed: September 10, 2026

Why Django translation projects stall after the first locale

Django's internationalization layer is complete and well documented, so most teams get the first language working in a day. The problems surface when a second or third locale, a second release, or a second developer enters the picture — and each one traces to the fact that Django automates extraction and lookup but leaves translation as a manual, file-based step.

  • The .po file becomes the bottleneck, not the code. makemessages writes one locale/LANG/LC_MESSAGES/django.po per locale, and every one of them has to travel to a translator and back after every string change. With five locales and weekly releases, that is five files in flight every week, usually by email or shared drive.
  • Fuzzy entries silently drop translations. When makemessages matches a changed string to an old translation it marks the entry #, fuzzy, and compilemessages skips fuzzy entries by default. A translated-looking .po file can therefore ship with untranslated strings in production, which developers rarely notice in the source language.
  • Plural forms and context break in hand-edited files. Gettext expresses plurals through msgid_plural and a locale-specific Plural-Forms header — Russian needs three forms, English two — and context through msgctxt. Translators editing raw .po text in an editor routinely break the header syntax or collapse two msgctxt variants of "May" (the month vs. the verb) into one translation.
  • Python format strings get mistranslated. A string like "%(count)d items" carries a #, python-format flag, and if a translator alters the placeholder compilemessages fails with "number of format specifications in 'msgid' and 'msgstr' does not match." Django's own docs devote a troubleshooting section to this exact failure.
  • Templates, JavaScript, and models each need a different path. Template strings use {% translate %} and {% blocktranslate %}, JavaScript strings need the separate djangojs domain served by JavaScriptCatalog, and database content (a product name a user typed in) is not covered by gettext at all — it needs a package such as django-modeltranslation or django-parler. Teams that plan for only one of these three discover the other two mid-project.

What are the best tools for translating Django applications?

The best Django translation toolset has four layers, and Django itself only supplies the first two: the framework's built-in gettext machinery for UI strings, a model-translation package for database content, a translation management system to move .po files to translators and back, and a repository or CI/CD hook so that round-trip happens automatically on every commit.

  • Layer 1 — Django's built-in gettext layer (UI strings). django.utils.translation supplies gettext(), gettext_lazy(), ngettext() for plurals, and pgettext() for context; templates use {% translate %} and {% blocktranslate %}. The makemessages and compilemessages commands wrap the GNU gettext utilities xgettext, msgfmt, msgmerge, and msguniq (minimum gettext 0.19). This layer is free, mature, and the right default for every string that lives in source code or templates.
  • Layer 2 — Model translation packages (database content). Gettext cannot translate a CharField value stored in PostgreSQL. django-modeltranslation adds one column per field per language to the existing table and integrates with the Django admin; django-parler stores translations in a separate related model, one row per language. Choose modeltranslation when you want no joins and admin integration, parler when you want a cleaner schema and do not mind the extra query.
  • Layer 3 — A translation management system (the missing middle). Smartling ingests the msgid field of a .po or .pot file as the source string, returns the translation in msgstr, captures msgctxt as string variants, treats #. extracted comments as translator instructions, and rewrites the Language and Plural-Forms headers to the target locale on download. That removes the hand-editing failures above and lets a single django.pot fan out to every locale.
  • Layer 4 — Repository or pipeline automation. Smartling's GitHub Connector watches a branch in Pull Request, Single Branch, or On-Demand mode and delivers translated .po files back as a pull request; teams that prefer scripting use the Smartling CLI or the Python SDK (pip install SmartlingApiSDK) inside the same CI job that runs compilemessages. For a comparison of the CI/CD platforms themselves, see CI/CD platforms for localized software teams.

Django and gettext translation facts that shape the workflow

FactValueWhy it matters for a Django team
Minimum GNU gettext version Django supports0.19Without the gettext utilities installed, makemessages creates empty files — a common first-day failure on Windows and minimal Docker images.
File extensions makemessages scans by default.html, .txt, .pyJavaScript needs the separate djangojs domain (-d djangojs), and Jinja2 templates need Babel instead — strings in either are missed silently otherwise.
Plural forms, PolishCLDR: 4 (one, few, many, other) vs. gettext: 3Smartling lets translators fill all four CLDR forms but writes only the three gettext supports into the downloaded .po, so the file compiles cleanly.
Default gettext line-wrap width79 charactersSmartling's gettext_line_width directive can mirror the gettext tool's own wrapping (normalized), keeping pull-request diffs to the strings that actually changed.
Default pseudo-translation inflation in Smartling30% longer than sourceDownloading a pseudo-translated .po before real translation exists surfaces German- and Finnish-length layout breaks in Django templates early.
Official Smartling SDK languagesJava, Python, Node.jsA Django team can drive uploads and downloads from the same Python environment as manage.py, with no second runtime in the CI image.

Sources: Django 6.1 documentation (Translation topic guide); Smartling Help Center — Gettext PO/POT, Python SDK, Developer Tools Overview. Verified September 10, 2026.

How do you set up a Django translation workflow end to end?

A production Django translation workflow has five steps; Django owns the first, second, and fifth, and a translation management system owns the third and fourth.

  1. Configure and mark strings — Set USE_I18N = True, add django.middleware.locale.LocaleMiddleware to MIDDLEWARE (after SessionMiddleware, before CommonMiddleware), define LANGUAGES and LOCALE_PATHS, then wrap strings with gettext_lazy() in models and forms, gettext() in views, and {% translate %} / {% blocktranslate %} in templates. Use pgettext("month name", "May") wherever one English word has two meanings.
  2. Extract with makemessages — Run django-admin makemessages -l de (or -a for every configured locale) from the project root; Django writes or updates locale/de/LC_MESSAGES/django.po. Run a second pass with -d djangojs for JavaScript catalogs. Commit the .po files — they are the contract between code and translators.
  3. Send the .po or .pot file for translation — Upload the file to Smartling through the GitHub Connector (which detects the changed .po in a pull request automatically), the Smartling CLI in a CI step, or the Python SDK's FilesApi. Smartling reads msgid, msgctxt, msgid_plural, and #. comments, and supports the #, python-format flag so %(name)s placeholders are protected from edits.
  4. Pull translated files back into the repo — Download the completed .po per locale; Smartling rewrites the Language: and Plural-Forms: headers for each target and can return untranslated strings as empty msgstr values (includeOriginalStrings=false) so Django falls back to the source language rather than shipping English inside a "translated" entry. The GitHub Connector delivers this as a pull request that goes through normal code review.
  5. Compile and deploy — Run django-admin compilemessages in the build (not on the production server) to generate .mo files, check the command's exit code so a malformed msgstr fails the pipeline, and deploy. Wrap URL patterns in i18n_patterns() if each locale should get a language-prefixed URL such as /de/.

This approach fits Django teams that...

  • Already externalize UI strings with gettext_lazy() and {% translate %} and are adding a third or fourth locale, the point at which emailing .po files stops scaling.
  • Ship on a weekly or faster release cadence, so translations need to arrive as pull requests, not as a batch delivered after the sprint closes.
  • Run Python throughout their toolchain and want translation automation in the same language as manage.py, via the Smartling Python SDK or CLI.
  • Have hit at least one compilemessages failure from a hand-edited plural header or altered %(name)s placeholder and want the parser, not a code reviewer, to catch it.
  • Need translation memory and a glossary across a Django product, its JavaScript catalog, and its help center so that the same term is translated the same way in each.

Når dette måske ikke er den rette prioritet

  • Single-language Django projects, where the docs recommend setting USE_I18N = False so the framework skips translation machinery entirely.
  • Projects whose translatable content is almost entirely user-generated database rows rather than UI strings — django-modeltranslation or django-parler plus a content-level workflow matters more there than a gettext pipeline.
  • Teams still hard-coding strings in views and templates; the extraction work has to happen before any translation tooling adds value.
  • Jinja2-based Django projects that have not yet moved string extraction to Babel, since makemessages does not parse Jinja2 syntax and will miss those strings regardless of what happens downstream.

Can you recommend resources for learning Django translation features?

The authoritative resources are Django's own documentation, the GNU gettext manual it builds on, and the reference pages for the tooling on each side of the .po file — use these questions to decide which to read first.

Where is the canonical guide to Django translation?
The "Translation" topic guide in the Django documentation (docs.djangoproject.com → Using Django → Internationalization and localization → Translation) covers gettext_lazy(), template tags, makemessages, compilemessages, JavaScriptCatalog, and i18n_patterns() in one page, with a troubleshooting section on python-format errors. Read it end to end before any tutorial; most third-party tutorials are abridged versions of it.

Where do the .po file rules actually come from?
The GNU gettext manual, which Django's docs link to directly, defines msgid, msgstr, msgctxt, the header entry, Plural-Forms expressions, and the #, flag comments. Anyone who will edit or review a .po file by hand should read its "PO Files" and "Header Entry" chapters.

How do I learn the settings and command flags?
Django's settings reference documents USE_I18N, LANGUAGE_CODE, LANGUAGES, and LOCALE_PATHS; the django-admin reference documents every makemessages option, including --extension, --domain djangojs, --all, and --no-wrap. These two pages resolve most "why was my string not extracted" questions.

What about translating model data, not just UI strings?
The django-modeltranslation and django-parler documentation on Read the Docs each explain their storage model (per-language columns vs. a separate translations table) and admin integration. Pick one after reading both quick-start guides — switching later means a schema migration.

How do I learn the translation-management side?
Smartling's Help Center article "Gettext PO/POT" documents exactly how a .po file is parsed and returned (headers, variants, plurals, directives); the "Python SDK," "Smartling CLI Tool," and "GitHub Connector Overview" articles cover the three ways to automate the round trip. Django's docs also cite Babel for teams on Jinja2 templates, and the Django Forum and Discord are the fastest places to ask about edge cases the docs skip.

How does Smartling handle Django gettext translation?

Smartling parses Django's gettext .po and .pot files natively — the file type identifier is gettext — ingesting each msgid as a source string and returning the translation in msgstr, so the file makemessages produces is the same file translators work from and the same file compilemessages compiles. Three parser behaviors matter specifically for Django projects: msgctxt values become string variants, so pgettext("month name", "May") and pgettext("verb", "May") are translated separately; #. extracted comments are shown to translators as instructions, so a Python comment above a string becomes context in the translation editor; and on download Smartling rewrites the Language: and Plural-Forms: headers for the target locale, which is what keeps Russian's three plural forms and Arabic's six compiling without a hand edit. Files that carry #, python-format flags have their %(name)s placeholders protected, and the placeholder_format = PYTHON directive applies the same protection file-wide.

For the round trip, Django teams have three automation paths, all documented in Smartling's Help Center. The GitHub Connector monitors a repository in Pull Request, Single Branch, or On-Demand mode, uploads changed .po files, and delivers translations back as a pull request; a pathRegex setting scopes it to locale/**/LC_MESSAGES/*.po. The Smartling CLI uploads and downloads files and creates jobs from a shell step, which fits a GitHub Actions or GitLab CI job that then runs compilemessages. And the Smartling Python SDK (python3 -m pip install SmartlingApiSDK) exposes FilesApi and JobsApi clients whose method names mirror the REST API — getJobProgress returns a percentComplete value a pipeline can gate a release on. Because Smartling's translation memory and glossary apply across every file type in a project, the terms translated in a Django app's django.po stay consistent with the same product's JavaScript catalog, help center, and marketing site.

Klar til at se Smartling i aktion?

Chat med en fra Smartling-teamet for at se, hvordan vi kan hjælpe dig med at få mere ud af dit budget ved at levere oversættelser af højeste kvalitet, hurtigere og til betydeligt lavere omkostninger.