Back to blog

Mastering Code for Language Translation in Django

2026-07-04 10 min read
Mastering Code for Language Translation in Django

Meta description: Tired of manual Django .po updates? Build a CLI-first localization workflow with glossaries, validation, and CI so translations stop blocking releases.

You merge a feature branch on Thursday afternoon, run makemessages, and your diff explodes. New msgids everywhere. A few changed strings. Fuzzy entries you forgot about. Three locales still lagging behind production. Someone suggests exporting the .po files and pasting them into a web portal. Someone else starts copying strings into ChatGPT. By Friday, one placeholder is broken, one HTML tag is escaped wrong, and nobody remembers which translations were reviewed.

That mess is why code for language translation matters in Django. Not as a marketing phrase, but as an engineering workflow. You want translation to live where the rest of your release process already lives: in manage.py, in Git, in CI, and in reviewable diffs.

The Frustration of Manual .po File Updates

The failure mode is always the same. You add what feels like a small feature. Maybe a billing banner, a few form errors, two onboarding screens. Then django-admin makemessages turns that into hundreds of changed lines across locale/fr/LC_MESSAGES/django.po, locale/de/LC_MESSAGES/django.po, and whatever other locales your app ships.

Where the pain actually shows up

The work isn't extracting messages. Django is good at that. The pain starts in the gap after extraction.

You stare at untranslated entries like these:

#: billing/templates/billing/upgrade.html:14
#, python-format
msgid "Hi %(name)s, your trial ends in %(days)s days."
msgstr ""

#: accounts/forms.py:52
msgid "Please enter a valid work email address."
msgstr ""

#: dashboard/templates/dashboard/empty.html:8
msgid "<strong>No projects yet.</strong> Create your first one."
msgstr ""

Now somebody has to translate them without breaking %(name)s, mangling the HTML, or changing the tone between releases. That's where manual workflows fall apart.

Copy paste doesn't survive real projects

Google Translate works until it doesn't. A TMS works until your developers stop opening it. Contractor output works until the same label gets translated three different ways. Even generic AI chat works badly when you feed it raw .po blocks with no glossary, no review trail, and no protection for placeholders.

Practical rule: If translation lives outside your repo, it will drift from your code.

The ugly part is that none of this feels like a language problem. It feels like build debt. You're not missing translators as much as you're missing a repeatable process for the PO file format in real projects.

A good workflow doesn't ask engineers to become localization coordinators. It makes translation another scripted step, like migrations or static asset collection.

The Standard Django i18n Workflow and Its Cracks

Django's official path is still the right baseline. Mark strings with gettext_lazy, extract them, translate them, compile them.

from django.utils.translation import gettext_lazy as _

class CheckoutLabels:
    pay_now = _("Pay now")
    card_declined = _("Your card was declined")
python manage.py makemessages --locale fr --locale de
python manage.py compilemessages

A hand-drawn illustration showing a developer confused by the broken workflow of Django i18n translation process.

The missing middle step

The official workflow leaves one giant hole in the middle. After makemessages, Django gives you .po files full of empty msgstr values. It doesn't tell you how to populate them in a way that fits engineering work.

That middle step is where teams end up with one of these approaches:

Method Typical Cost Speed for 500 strings Workflow Fit
Human translators Professional human translation services typically cost $0.10 to $0.25 per word, which can put a standard app at $600 for one language and $3,000 for five languages (pricing context) Slowest, depends on handoff and review Strong for critical copy, weak for rapid release cycles
Manual AI copy paste Pennies per run in practice, but time cost lands on developers Fast at first, then slows with cleanup Poor, because nothing is versioned or repeatable
TMS portal Subscription cost plus team overhead Moderate Better for non-engineering teams, worse when devs need code-adjacent control
CLI in repo Provider cost per run, usually low Fast once scripted Best fit for teams already shipping through Git and CI

Why old habits break under release pressure

Two things get expensive fast. One is money. The other is context switching.

A translator doesn't have your pgettext context, your model names, your product vocabulary, or your placeholder rules unless you build those into the process. A portal can help, but it still pulls work out of your editor and into another queue.

The standard Django i18n flow is fine. The unscripted translation step is what breaks.

If you're trying to keep release work inside the normal dev loop, the answer isn't another dashboard. It's adding a real translation command between makemessages and compilemessages.

Automating Translation with a Management Command

The cleanest setup is the one that behaves like every other Django task. Install a package, add it to INSTALLED_APPS, configure a provider, run a management command, review the diff.

Screenshot from https://translatebot.dev

Put translation inside manage.py

pip install translatebot-django
# settings.py
INSTALLED_APPS = [
    # ...
    "translatebot_django",
]

Set your provider credentials in the environment, then run extraction first:

python manage.py makemessages --locale fr --locale de

After that, add the translation step:

python manage.py translate --locale fr --locale de

Then compile as usual:

python manage.py compilemessages

That shape matters. It keeps the workflow in the same toolchain your team already uses.

Why .po awareness matters

The gettext standard sits under Django, WordPress, GNU/Linux tools, and C/C++ projects, so format-string preservation isn't optional when you handle .po and .pot files (gettext ecosystems and placeholder examples). If your translation layer doesn't preserve %s, {0}, and %(name)s, you're not automating localization. You're generating runtime risk.

A useful translation command should write directly back to files like:

locale/fr/LC_MESSAGES/django.po
locale/de/LC_MESSAGES/django.po

It should also leave a reviewable diff in Git.

Before translation:

#: notifications/email.py:18
#, python-format
msgid "Hello %(name)s, you have %s unread alerts."
msgstr ""

#: templates/account/reset_done.html:7
msgid "<a href=\"/login/\">Sign in</a> to continue."
msgstr ""

After translation:

#: notifications/email.py:18
#, python-format
msgid "Hello %(name)s, you have %s unread alerts."
msgstr "Bonjour %(name)s, vous avez %s alertes non lues."

#: templates/account/reset_done.html:7
msgid "<a href=\"/login/\">Sign in</a> to continue."
msgstr "<a href=\"/login/\">Connectez-vous</a> pour continuer."

If you also want your docs to keep up with code changes, the same discipline applies. Versioned automation helps teams achieve accurate, up-to-date documentation for the same reason it helps with i18n. The source of truth stays in the repo.

A short walkthrough helps if you're moving from ad hoc prompts to a repo-first flow. The guide on doing translation work inside a development process is a better mental model than thinking of localization as a separate business system.

Later in the setup, a live demo is worth more than another paragraph:

Ensuring High-Quality Translations with Glossaries

Running a command is the easy part. The hard part is keeping terms consistent across releases, languages, and contributors.

A glossary beats repeated prompt tweaking

Developers keep asking for a TRANSLATING.md style file because prompt-by-prompt correction doesn't scale. The bigger issue is time. 68% of localization teams spend more than 20 hours weekly on manual terminology alignment (discussion and data point).

A diagram illustrating the quality control process in translation, featuring glossary integration and placeholder preservation as key steps.

What to put in TRANSLATING.md

Keep the glossary in Git. Review it like code. Add the terms that repeatedly cause churn.

# TRANSLATING.md

## Product terms
- "Workspace" stays untranslated in all locales.
- "Project" in UI means a customer account container, not a generic task.

## Tone
- Use informal second person in French.
- Keep error messages brief and neutral.

## Interaction words
- Prefer "tap" for mobile UI.
- Prefer "click" for desktop UI.

## Preservation rules
- Never change placeholders like %(name)s, %s, or {0}.
- Preserve HTML tags exactly as written.

That file does two jobs. It gives your translation layer context, and it gives reviewers a stable place to discuss terminology.

Protect placeholders before you worry about style

Here is the kind of string that breaks in production when teams skip validation:

#: templates/invoices/reminder.html:11
#, python-format
msgid "<strong>%(name)s</strong>, your invoice is due in %s days."
msgstr ""

Your process should preserve both the HTML and the format arguments. That's not just a Django concern. It's a gettext concern across multiple ecosystems.

Short UI labels and format strings are where bad automation gets exposed first.

If you want a broader look at how prompt-based translation tools differ in practice, this practical guide for QuillBot is useful as a contrast. Generic text tools can help with raw language work, but they don't solve code-adjacent localization rules by themselves.

For glossary structure, examples, and naming conventions, the guide on glossary format for localization workflows is worth keeping next to your i18n docs.

Full Automation in Your CI/CD Pipeline

Once your translation step works locally, wire it into CI so it stops becoming release-week cleanup.

A diagram illustrating the automated translation workflow within a CI/CD software development pipeline.

A GitHub Actions job that does the boring part

name: Update translations

on:
  push:
    branches:
      - main
      - "feature/**"

jobs:
  translate:
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: |
          pip install -r requirements.txt

      - name: Extract messages
        run: |
          python manage.py makemessages --locale fr --locale de

      - name: Translate new strings
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          python manage.py translate --locale fr --locale de

      - name: Compile messages
        run: |
          python manage.py compilemessages

      - name: Commit updated locale files
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add locale/
          git diff --cached --quiet || git commit -m "Update translations"
          git push

Keep the review loop tight

You don't need auto-merge. A pull request with changed .po files is often better. Product, support, or native-speaking teammates can review only the lines that changed.

A good CI translation job should do four things:

  • Extract messages: Run makemessages on every branch where UI text changes.
  • Translate only deltas: Avoid reprocessing unchanged strings.
  • Compile before review: Catch syntax and formatting issues early.
  • Commit readable diffs: Let reviewers inspect actual msgstr changes in Git.

That turns localization into background maintenance instead of a release blocker.

Validating Outputs and When to Call a Human

Automation earns trust when you validate the result in a live app, not when you trust the provider blindly.

Review where the strings actually render

Use a review environment with LocaleMiddleware enabled. Switch the locale in the UI. Check buttons, empty states, emails, validation messages, and pluralized strings. Pay extra attention to pgettext cases, fuzzy entries, and languages with harder plural forms.

For new markets and low-resource locales, placeholder validation deserves special treatment. 73% of low-resource language apps fail localization due to broken placeholders, and no mainstream code repository offers a CLI tool with 100% test coverage for format-string handling across these languages (low-resource localization gap).

Where a human still helps most

Use AI for the bulk work. Bring in a human when the text is expensive to get wrong.

  • Marketing copy: Brand voice drifts fast.
  • Short UI labels: Tiny strings often lack context.
  • Legal and billing language: Precision matters more than speed.
  • Low-resource locales: Edge cases show up sooner.

Trust the automation for throughput. Trust humans for nuance and accountability.

That hybrid pattern shows up outside Django too. Teams handling catalogs and product content often use human review on top of generated output, and the same logic appears in AI workflows for e-commerce product data. Translation isn't special here. It's another place where human-in-the-loop beats blind full automation.

Your next step is boring on purpose. Add translation to the same place you already run tests and migrations. Then review diffs, not pasted text.


If you want that workflow without building the translation layer yourself, TranslateBot gives you a Django-native path: translate .po files through manage.py, keep terminology in a versioned glossary, preserve placeholders and HTML, and ship reviewable locale diffs from Git instead of another portal.

Stop editing .po files manually

TranslateBot automates Django translations with AI. One command, all your languages, pennies per translation.