Django
How Babelize translates Django projects with gettext — what it marks, where the .po files go, how settings.py is configured, and current limitations.
Babelize supports Django projects. It works the way Django's own i18n does: strings in your code are marked with gettext / {% translate %}, translations live in gettext catalogues (locale/<lang>/LC_MESSAGES/django.po), and Django looks them up by their source text. There is no runtime library to add.
Detection
A project is treated as Django when it has a manage.py (at the root or in a subfolder), or lists Django in requirements*.txt, pyproject.toml, Pipfile, setup.py or setup.cfg. The package manager is detected from uv.lock, poetry.lock / [tool.poetry], Pipfile, pdm.lock or requirements.txt (pip).
What gets translated
Strings you already marked are picked up as they are:
- Python:
_(),gettext,gettext_lazy,ngettext,pgettext,npgettextand their lazy/legacy variants, under whatever name you import them as. - Templates:
{% translate %}/{% trans %}(includingcontext),{% blocktranslate %}/{% blocktrans %}(includingcount … {% plural %},with,trimmed,context) and_("…")in tag and filter arguments. # Translators:and{# Translators: … #}comments are carried into the catalogue for translators.
Hardcoded user-facing text is marked for you, in place:
| Where | Becomes |
|---|---|
| Template text | {% translate "Our books" %} |
| A sentence with variables or inline markup | {% blocktranslate with name=user.name %}Hi {{ name }}, <a href="/x/">sign in</a>{% endblocktranslate %} — lookups and filters are bound with with, because blocktranslate only accepts plain names |
title, alt, placeholder, aria-label, button value attributes | placeholder="{% translate 'Search' %}" |
default / default_if_none filter arguments | {{ bio|default:_("Nothing yet.") }} |
Model and form verbose_name, verbose_name_plural, help_text, label, empty_label; class Meta / AppConfig verbose_name | gettext_lazy (_("…")) |
choices labels (lists, choices=, TextChoices, optgroups), error_messages, widget attrs placeholders/titles, @admin.action(description=…) | gettext_lazy |
messages.info/success/…, message_user(), ValidationError — and string variables passed to them | gettext (evaluated per request) |
Text returned by template tags and filters (templatetags/*.py) | gettext |
f-strings are rewritten the Django way: f"Thanks {order.customer_name}!" becomes _("Thanks %(customer_name)s!") % {"customer_name": order.customer_name}. {% load i18n %} and the django.utils.translation import are added where needed, and the base template's <html lang> follows the active language (with dir="rtl" for right-to-left languages).
Edits replace exact spans of the original file — formatting, comments, quote style and line endings are untouched. Running Babelize again on the result changes nothing.
Left alone: dict keys, codes and identifiers, log messages, URLs, docstrings, migrations, settings, tests, static/ files, and text in <script>, <style>, <pre>, <code>, {% comment %} and {% verbatim %}. Strings Babelize can't mark safely — an f-string with a format spec, a sentence built with +, an attribute that mixes text and template tags — are listed in the run's "not wired" report instead of being guessed at. Add # babelize-ignore to a Python line to skip it.
Catalogues
Translations are written to <LOCALE_PATHS[0]>/<lang>/LC_MESSAGES/django.po (by default locale/ next to manage.py), with Django's directory names (pt_BR, zh_Hans). If the project already has catalogues, Babelize merges into them:
- An existing translation is never overwritten or re-translated — it counts as done, and costs no credits. Hand edits made after a run are kept.
fuzzyentries are left for you to review (Django ignores them until the flag is removed).- Empty entries are filled. Entries Babelize doesn't touch are written back byte-for-byte — comments, references, flags, obsolete
#~entries and line wrapping included. Plural-Formsis set per language (Arabic gets 6 forms, Russian 3, Japanese 1) andmsgstr[n]follows it. An existing header is respected.#, python-format/python-brace-formatflags are set likemakemessagesdoes.
If your repository commits compiled .mo files, they are regenerated too. Otherwise run:
python manage.py compilemessages(this needs GNU gettext installed). Re-runs use the lockfile: only new or changed strings are translated.
settings.py
Babelize opts the project into i18n, conservatively — nothing is reordered or rewritten, and a setting you already chose is left alone:
USE_I18N = True(an explicitFalseis flipped)django.middleware.locale.LocaleMiddlewareafterSessionMiddlewareand beforeCommonMiddlewareLOCALE_PATHS,LANGUAGE_CODEif missingLANGUAGESwith the source and target languages. A list Babelize added grows with new languages; a list you wrote is never edited — you get a note about any language missing from it.
When the settings module star-imports a base module (settings/dev.py → from .base import *), the shared base file is edited.
Babelize does not add i18n_patterns or the set_language view. Language is chosen from the browser's Accept-Language header (and the language cookie). For a switcher, add path("i18n/", include("django.conf.urls.i18n")) and a small form — the project page in the dashboard has a copyable snippet.
Placeholders and quality checks
%(name)s, %s, %d, %%, {name}, {} and HTML entities such as — are protected through translation. After translation every form is checked the way msgfmt --check-format would: a translation that drops, adds or reorders a placeholder is not written — it's reported as a failure with the reason and retried next run. The source text is never shipped as a "translation".
Limitations
- Review the diff before merging, as with any automated change, especially templates with unusual structure.
- Plurals with the cloud (NLLB) translator. gettext plurals need one translation per grammatical form. The NLLB translator translates the singular and plural separately, so languages with more than two forms (Arabic, Russian, Polish, …) get the plural text for every non-singular form, and the run notes say so. An AI provider (BYOK, or
babelize translatewith your key) translates each plural as one ICU message and fills every form correctly. Hand-written{% if count == 1 %}…{% else %}…{% endif %}blocks are translated as two separate messages; converting them toblocktranslate countis up to you. See issue #261. - Jinja2 templates,
.txtemail templates and JavaScript catalogues (djangojs.po) are not handled yet. - Strings built at runtime (
"Hello " + name,"%s items" % noutside a recognised call), and strings that would need data-flow analysis to mark safely, can't be marked automatically. They are listed in the run's "not wired" report. - Best quality: bring your own LLM key (BYOK) for Django projects. It handles plurals, context and placeholders better than the cloud NLLB translator.
Last updated: 2026-10-13
Dashboard Overview
A tour of the Babelize dashboard: what each main section does, how the project detail page is organized, and the keyboard shortcuts that speed you up.
MCP Server
Make apps global-ready from Claude Code, Cursor, Codex and other AI agents with @babelize/mcp. The same local engine as the CLI.