Meta description: Translate a page in Django without copy-paste or a TMS. Use a CLI workflow to generate, review, and ship .po translations in Git.
You ran python manage.py makemessages -l es and got exactly what Django promised: a fresh locale/es/LC_MESSAGES/django.po full of empty msgstr values.
That isn't the hard part. The hard part is everything after.
You can paste strings into browser translators and hope you don't break %(name)s, %s, {0}, or inline HTML. Or you can move your app into a TMS and accept a separate UI, separate billing, and a workflow that feels detached from your codebase. Neither option fits how most Django teams ship.
The Gap Between Makemessages and a Translated Site
Django's i18n stack is solid. You mark strings with gettext_lazy, use {% translate %} in templates, run makemessages, and commit your locale files. Then you hit the dead zone between extraction and a translated site.

Most content about translate a page is about browser features for readers, not developers maintaining dynamic apps. Google's Chrome help covers page translation for browsing, but that doesn't solve .po files, placeholder safety, or Git-reviewable diffs for Django projects (Chrome page translation help).
The two paths that usually waste time
One path is manual copy-paste.
- Fast at first: You can translate a handful of strings quickly.
- Fragile under load: Placeholder mistakes show up late, often in templates or emails.
- Hard to review: Nobody wants to diff a spreadsheet against
django.po.
The other path is a full TMS.
- Better process control: You get review workflows and translators.
- More moving parts: Another UI, another sync step, another monthly bill.
- Weak fit for small teams: A two-to-ten person Django team usually wants changes to live in Git, not a portal.
Practical rule: If your translations don't end up as normal code changes, review quality drops and release friction goes up.
There's a third option that fits engineers better. Keep translation in the terminal, write output directly to locale/<lang>/LC_MESSAGES/django.po, and review it in pull requests like any other generated artifact. If you work across multiple stacks, the same developer-first mindset shows up in guides on optimizing Sitecore's global language support, where the underlying problem isn't toggling language features. It's building a workflow your team can maintain.
Generating Translations with a Single Command
The useful version of automation starts where Django stops. You already have source strings. You need a command that fills msgstr, preserves format strings, and leaves a readable diff behind.
Start with a real .po file
A fresh Spanish file often looks like this:
#: templates/dashboard.html:12
#, python-format
msgid "Welcome back, %(name)s"
msgstr ""
#: templates/billing.html:48
msgid "Upgrade plan"
msgstr ""
#: templates/account.html:33
msgid "<strong>Email</strong> is required."
msgstr ""
#: views.py:91
#, python-format
msgid "You have %s remaining credits."
msgstr ""
That file is already the contract. Your translation step should update msgstr only, and it shouldn't damage placeholders or tags.
Run the translation command
Install the package in your Django project:
pip install translatebot-django
Add minimal configuration in settings.py:
INSTALLED_APPS = [
# ...
"translatebot_django",
]
TRANSLATEBOT_PROVIDER = "openai"
TRANSLATEBOT_API_KEY = "your-api-key"
TRANSLATEBOT_MODEL = "gpt-4o-mini"
Then run:
python manage.py translate --target-lang es
If you want the exact command options and supported arguments, check the TranslateBot command reference.
After the command runs, the same file should look like this:
#: templates/dashboard.html:12
#, python-format
msgid "Welcome back, %(name)s"
msgstr "Bienvenido de nuevo, %(name)s"
#: templates/billing.html:48
msgid "Upgrade plan"
msgstr "Mejorar plan"
#: templates/account.html:33
msgid "<strong>Email</strong> is required."
msgstr "El <strong>correo electrónico</strong> es obligatorio."
#: views.py:91
#, python-format
msgid "You have %s remaining credits."
msgstr "Te quedan %s créditos."
What works and what usually breaks
The command is the easy part. The value is in the constraints.
| Concern | What you want |
|---|---|
| Placeholders | Preserve %(name)s, %s, {0} exactly |
| HTML | Keep tags intact |
| Git diff | Write back to django.po, not an external export |
| Incremental runs | Translate only new or changed strings |
| Provider choice | Use the model or API your team already trusts |
AI is good at generating first-pass translations for UI copy, notifications, and standard product text. It still needs help when strings are too short, too ambiguous, or too tied to your brand voice.
Translate the file in place. Review the diff. Compile messages. That's the loop.
Django still does its normal job afterward:
python manage.py compilemessages
If your app serves translated pages directly, this closes the gap between extraction and an actual localized UI without dragging the team into a browser tab for every release.
Guiding the AI with Context and Glossaries
A raw string like View is a bad translation prompt. Is it a verb, a noun, a button label, or a permission name? If you want better output, feed the model better context.

Research on decomposed translation shows better results when translation is split into phases like research, drafting, and refinement. In practice, a versioned glossary file plays that research role and improves terminology consistency (decomposed translation approaches on arXiv).
Use pgettext when the English string is ambiguous
Django already gives you the tool for this.
from django.utils.translation import gettext_lazy as _
from django.utils.translation import pgettext_lazy
button_label = pgettext_lazy("button action", "View")
table_column = pgettext_lazy("analytics table column", "View")
page_title = _("Account settings")
Those message contexts matter. They reduce bad guesses before translation even starts.
Short labels are where AI drifts most. A single word with no context can be translated correctly in one screen and incorrectly in another.
Keep a TRANSLATING.md file in the repo
A glossary in Markdown works well because your team can edit it in the same place as the code. No portal. No hidden project memory.
# Translation glossary
## Product terms
- AcmeCloud stays as "AcmeCloud" in every language.
- Workspace means the customer account area, not a physical office.
- Seat means a paid user license.
## Tone
- Use informal second-person tone in Spanish.
- Avoid translating "API", "SSO", and "SAML".
## UI conventions
- "Billing" should map to the subscription/payment area.
- "Member" and "User" are different concepts and should not be merged.
TranslateBot supports this kind of context directly. The docs for translation context and glossary guidance are worth reading before you automate anything beyond a toy project.
If your team also experiments with operational automation outside code, the same pattern shows up in articles about streamline workflows with no-code AI. The tool changes, but the principle doesn't. Outputs get better when you define terms and guardrails up front.
Better translations usually come from better inputs, not more retries.
Reviewing Translations in Your Pull Request
Automation should produce a draft, not a final answer.
The reason a CLI-first workflow works so well is that the output lands in Git. Your reviewer doesn't need access to a translation platform. They need a diff.

Translation quality work has moved away from single-pass checks and toward review and adjudication. The TRAPD method treats review as part of the process, not an optional cleanup step. For engineering teams, the practical version is clear: put translated strings in a pull request and let humans review them where they already review code (TRAPD and translation quality assessment).
What reviewers should look for
Don't review every string with the same suspicion. Focus where AI tends to miss.
- Short UI labels:
Open,Save,View,Charge - Plural forms: especially in languages with more complex plural rules
- Gendered agreement: common in Romance languages
- Cultural phrasing: onboarding copy, billing messages, legal text
- Segmentation issues: common in CJK output and strings with embedded markup
A normal PR review works well here:
git status
git add locale/es/LC_MESSAGES/django.po
git commit -m "Update Spanish translations"
Then the team reviews the diff like any generated file. Native speakers can suggest wording changes inline. Product people can comment on tone. Engineers can catch placeholder damage before deploy.
Keep the quality gate boring
You don't need a committee for every locale. You need a repeatable rule.
Review changed strings in the PR, then run
compilemessagesbefore merge.
That keeps translation in the release cycle instead of pushing it into a side process that nobody owns.
Automating Your Translation Pipeline with CI/CD
Once the local workflow is stable, move it into CI. Nightly updates work well for apps with constant copy changes. Pull-request or main-branch triggers work better when you want tighter review.
A GitHub Actions job can run translation, compile messages, and open a commit for review:
name: Update translations
on:
workflow_dispatch:
schedule:
- cron: "0 2 * * *"
jobs:
translate:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
with:
persist-credentials: true
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Generate translations
env:
TRANSLATEBOT_API_KEY: ${{ secrets.TRANSLATEBOT_API_KEY }}
run: |
python manage.py translate --target-lang es
python manage.py compilemessages
- name: Commit changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add locale/
git diff --cached --quiet || git commit -m "Update translations"
git push
A safer variation opens a pull request instead of pushing directly. That's usually the better default for customer-facing copy.
If you're designing wider automation chains around AI tasks, examples from streamlining AI workflows with Select.ax are useful because they show the same engineering trade-off. Keep the automation visible, reviewable, and easy to rerun.
How This Workflow Compares to Other Options
The translation market is projected to reach $4.2 billion by 2030, and that growth explains why there are so many tools competing for the same job. It doesn't change the day-to-day question for a Django team: what fits your codebase and budget. The cost spread is wide, from AI translation at roughly $0.15 per million tokens for GPT-4o-mini, to human translation around $0.12 to $0.25 per word, to TMS plans starting at $50+ per month (translation market and pricing context).

A deeper comparison of tooling options helps if you're still deciding between approaches. This overview of software for translating is useful for that.
Translation Workflow Comparison
| Method | Cost | Developer Workflow | Review Process |
|---|---|---|---|
| Manual browser translation | No platform fee, but heavy time cost | Outside the editor, copy-paste into django.po |
Ad hoc, usually after mistakes show up |
| Traditional TMS | Subscription, often starting at $50+/month | Separate UI, sync/import/export steps | Structured, but outside normal code review |
| CLI-based translation | AI cost per run, often pennies | Terminal-first, writes directly to locale files | Pull request review in Git |
If your app changes often, the CLI model fits better than either extreme. Manual work doesn't scale. A TMS can be the right answer for large content operations, but it's often too much process for a product team that just wants updated locale files before the next deploy.
If you want that Git-based workflow in a Django project, TranslateBot is built for it. It runs as a manage.py translate command, preserves placeholders and HTML, and keeps translations in your repo where your team can review them normally.