BabelizeBabelize
Integrations

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, npgettext and their lazy/legacy variants, under whatever name you import them as.
  • Templates: {% translate %} / {% trans %} (including context), {% blocktranslate %} / {% blocktrans %} (including count … {% 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:

WhereBecomes
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 attributesplaceholder="{% 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_namegettext_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 themgettext (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.
  • fuzzy entries 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-Forms is set per language (Arabic gets 6 forms, Russian 3, Japanese 1) and msgstr[n] follows it. An existing header is respected.
  • #, python-format / python-brace-format flags are set like makemessages does.

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 explicit False is flipped)
  • django.middleware.locale.LocaleMiddleware after SessionMiddleware and before CommonMiddleware
  • LOCALE_PATHS, LANGUAGE_CODE if missing
  • LANGUAGES with 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 &mdash; 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 translate with 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 to blocktranslate count is up to you. See issue #261.
  • Jinja2 templates, .txt email templates and JavaScript catalogues (djangojs.po) are not handled yet.
  • Strings built at runtime ("Hello " + name, "%s items" % n outside 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

How is this guide?

On this page