Meta description: Technical document translation for Django without TMS sprawl. Automate .po files, preserve placeholders, review diffs, and ship faster.
You run python manage.py makemessages, open locale/de/LC_MESSAGES/django.po, and your stomach drops.
Half the file is empty. Product just merged a feature branch with new billing flows, onboarding copy, and admin labels. Release is close. Your choices look bad. Copy-paste strings into a browser translator and hope %(name)s survives, or push everything into a TMS portal and spend the rest of the day fighting another workflow your team already hates.
That’s the normal state of technical document translation in a Django app. The code is in Git. The review process is in Git. The deployment pipeline is in Git. Then translation gets kicked out into emails, spreadsheets, vendor dashboards, or manual copy-paste.
It’s also the wrong place to get sloppy. Technical content is the largest expertise area for professional translators at 34%, ahead of business at 15% and marketing at 12%, according to Redokun’s translation statistics roundup. If your app includes setup steps, API labels, billing rules, compliance copy, or user-facing operational messages, bad wording isn’t just awkward. It creates support tickets and broken flows.
If you need a refresher on what’s inside a gettext file, this guide on the gettext .po file format is worth skimming before you automate anything.
That Familiar Dread of an Untranslated .po File
The friction in Django localization is not extracting strings. It starts right after makemessages, when msgid entries pile up, msgstr stays empty, and a normal feature branch suddenly needs translation work nobody planned for.
A common release-week failure looks the same every time. Billing copy changed. A few admin labels were added. Someone updated onboarding text. The code is ready, but the locale file is half-finished, and now the team has to choose between slow manual edits, a browser translator that might break placeholders, or a TMS workflow that lives outside the repo.
That split is the primary problem. Source strings change in Git. Review happens in Git. Deployment happens in Git. Translation gets pushed into spreadsheets, vendor dashboards, or copy-paste tabs, then comes back as a blob of changes with very little context.
What usually goes wrong
The failure modes are boring and expensive:
- Placeholders get damaged.
%(name)sturns into plain text, disappears, or comes back in the wrong order. - Markup gets touched. HTML tags are translated, stripped, or escaped incorrectly.
- Terms drift. “Workspace,” “project,” and “team space” end up meaning the same thing in different parts of the app.
- Reviews lose precision. A giant
.poupdate lands after the feature work, so reviewers cannot easily match a translation change to the source string that caused it.
Traditional TMS products reduce some of that risk. They also add another sync path, another permission model, another invoice, and another interface your Django team has to keep in step with the codebase. For some organizations, that trade-off is fine. For a lot of product teams shipping technical content, it is overhead they do not need.
Practical rule: If source string changes and translation changes do not land in the same pull request, review quality drops fast.
A better default for Django teams
A simpler workflow usually holds up better:
- Extract strings with Django.
- Translate
.pofiles from the terminal. - Review the diff in Git.
- Compile messages and ship.
That keeps technical document translation inside the tooling developers already use every day. The editor stays the same. The branch stays the same. Comments stay attached to the actual lines that changed.
I have found that this matters more than teams expect. If a translation looks wrong in review, the fix is immediate. If the source string lacks context, the developer can correct it and regenerate the file in the same branch. Nothing has to leave the normal engineering workflow just to get localized text out the door.
If you need a quick refresher on headers, msgid, msgstr, plural forms, and flags, review this explanation of the gettext .po file format before automating your pipeline.
The Higher Stakes of Technical Strings
Technical strings are less forgiving than generic UI copy. Setup instructions, operational warnings, billing rules, compliance text, and API-facing labels need stable wording and intact formatting. A vague translation does more than sound awkward. It changes meaning, creates support load, or sends users down the wrong path.
That is why “translate now, clean it up later” fails so often in technical products. Later usually means after release, when the bad wording is already in front of users and the support team is compensating for it manually.
The Standard Django i18n Workflow Reviewed
Before you automate anything, it helps to keep the built-in Django flow clean. Django’s internationalization docs are still the canonical reference for marking and extracting strings, especially if you’re working across Python, templates, and plural forms.
Mark strings in Python and templates
In Python, use gettext_lazy for model fields, forms, and admin labels.
from django.db import models
from django.utils.translation import gettext_lazy as _
class Invoice(models.Model):
customer_name = models.CharField(_("Customer name"), max_length=255)
status = models.CharField(_("Status"), max_length=50)
class Meta:
verbose_name = _("Invoice")
verbose_name_plural = _("Invoices")
In templates, use Django’s translation tag:
{% load i18n %}
<h1>{% translate "Billing settings" %}</h1>
<p>{% blocktranslate with name=user.first_name %}Welcome back, {{ name }}.{% endblocktranslate %}</p>
For context-specific strings, use pgettext in Python. That matters when the same English word appears in different places with different meanings.
from django.utils.translation import pgettext
label = pgettext("button label", "Open")
status = pgettext("support ticket status", "Open")
Extract messages and inspect the file layout
Generate or update a locale like this:
python manage.py makemessages -l de
Django writes output to the standard path:
locale/de/LC_MESSAGES/django.po
A realistic .po excerpt looks like this:
#: billing/templates/billing/settings.html:12
msgid "Billing settings"
msgstr ""
#: accounts/templates/accounts/welcome.html:8
#, python-format
msgid "Welcome back, %(name)s."
msgstr ""
#: invoices/models.py:14
msgid "Customer name"
msgstr ""
That #, python-format marker matters. It tells you the string includes a placeholder Django expects to survive translation intact.

Compile translations before runtime
Once msgstr values are filled, compile them:
python manage.py compilemessages
That generates .mo files Django loads at runtime. Your normal path is still the same:
- Mark strings in code and templates.
- Extract with
makemessages. - Translate the
.pofiles. - Compile with
compilemessages.
If you skip the last step in environments that need compiled catalogs, your translations won’t show up even if the .po file looks correct.
Keep your locale tree boring.
locale/<lang>/LC_MESSAGES/django.pois easier to script, review, and debug than custom folder sprawl.
Automating Technical Document Translation from Your Terminal
Friday afternoon is a common time to notice 240 empty msgstr entries sitting in locale/de/LC_MESSAGES/django.po, right after a feature branch touched templates, forms, and model labels. Copying those strings into a portal breaks the normal review path. Editing them by hand inside the .po file burns time on repetitive work that a command can handle.
The practical target for automation is narrow and useful. Fill untranslated entries, keep the catalog in the repo, and let Git show the diff.
The terminal-first workflow
Install a Django-aware translation command as a dev dependency:
pip install translatebot-django
Run it against the locale you want to fill:
python manage.py translate --target-lang de
That keeps the whole workflow where Django developers already work. The .po file stays under locale/, the changes stay reviewable in a pull request, and there is no export or import step to forget. If you want the command syntax and expected behavior before wiring it into scripts, the guide on how to do a translation in Django from the terminal covers the basic flow.
Before and after in a real .po file
Before:
#: billing/templates/billing/settings.html:12
msgid "Billing settings"
msgstr ""
#: accounts/templates/accounts/welcome.html:8
#, python-format
msgid "Welcome back, %(name)s."
msgstr ""
#: invoices/models.py:14
msgid "Customer name"
msgstr ""
After:
#: billing/templates/billing/settings.html:12
msgid "Billing settings"
msgstr "Abrechnungseinstellungen"
#: accounts/templates/accounts/welcome.html:8
#, python-format
msgid "Welcome back, %(name)s."
msgstr "Willkommen zurück, %(name)s."
#: invoices/models.py:14
msgid "Customer name"
msgstr "Kundenname"
That is the useful result. No browser tabs, no CSV exports, no one manually filling 200 low-risk strings that already have enough context in the source.
Why this fits engineering teams better than a portal
For technical document translation, the useful parts of CAT workflows are reuse and consistency, not the portal itself. Professional translators still rely heavily on CAT tools. 88% use them, while translation memory is used by 82.5%, according to Redokun’s translation statistics roundup. The developer takeaway is straightforward. Reuse approved phrasing, avoid retranslating unchanged strings, and keep terminology close to the codebase.
A management command gives engineering teams that behavior without splitting work across systems:
| Approach | Where work happens | Review path | Fit for Django |
|---|---|---|---|
| Manual copy-paste | Browser and editor | Weak | Bad |
| TMS portal | External dashboard | Split between systems | Mixed |
| Management command | Terminal and Git | Pull request diff | Strong |
I have found this model easier to maintain than a TMS-first setup for product UI, admin panels, release-driven documentation, and internal tools. Developers already trust the terminal, the editor, and Git history. Translation work gets better when it follows the same path as code changes.
Trade-offs you should expect
Automation handles repetitive strings well. It does not understand product risk on its own.
Use it when:
- You have repetitive app strings across views, forms, and templates.
- You want branch-local changes reviewed in the same PR.
- You need technical consistency more than marketing polish.
Review output carefully when:
- The source string is too short to carry enough meaning.
- The locale has complex plural behavior and you have not checked the result in context.
- The copy has legal or regulatory impact and requires human approval.
The command removes the routine filling work. Your team still owns terminology, edge cases, and final approval.
Ensuring Quality and Consistency in Automated Runs
A translation run can finish cleanly and still leave you with a broken release. The failures usually show up later, during template rendering, form validation, or a reviewer noticing that the same term was translated three different ways across the same feature.

The fix is not another portal. It is a set of checks that run beside your .po files in Git, where developers already review diffs and catch regressions. That is the practical advantage of keeping technical document translation in the terminal instead of pushing it into a separate TMS workflow.
Handling placeholders and HTML safely
Translation systems often handle prose well enough. They fail on syntax.
For Django projects, placeholders such as %(name)s, plural forms, template fragments, and inline HTML are part of the contract. If a translated string changes that contract, compilemessages may fail, or worse, the app may pass compilation and break at runtime.
Correct:
#: accounts/templates/accounts/welcome.html:8
#, python-format
msgid "Welcome back, %(name)s."
msgstr "Willkommen zurück, %(name)s."
Wrong:
#: accounts/templates/accounts/welcome.html:8
#, python-format
msgid "Welcome back, %(name)s."
msgstr "Willkommen zurück, %name."
Also wrong:
#: accounts/templates/accounts/welcome.html:8
#, python-format
msgid "Welcome back, %(name)s."
msgstr "Willkommen zurück."
HTML needs the same discipline. Tags and attributes must survive the translation unchanged unless the source itself changed.
Correct:
msgid "Read the <a href=\"%(url)s\">setup guide</a>."
msgstr "Lesen Sie den <a href=\"%(url)s\">Einrichtungsleitfaden</a>."
Wrong:
msgid "Read the <a href=\"%(url)s\">setup guide</a>."
msgstr "Lesen Sie den Einrichtungsleitfaden."
I treat this as a validation problem, not a style problem. If your automation writes .po entries, add checks that compare placeholder names, count HTML tags, and fail the run when they drift. That catches the expensive mistakes before a reviewer has to spot them manually.
Keep your rules in the repo
Terminology drift is less dramatic than a broken placeholder, but it causes more churn over time. One release uses one term, the next uses another, and reviewers keep re-litigating the same wording.
Store translation rules with the codebase. A TRANSLATING.md file is usually enough. It keeps glossary decisions, forbidden translations, locale-specific rules, and reviewer notes in the same pull request as the strings they affect. That also lines up with good document version control best practices, because terminology changes get history, context, and diff review instead of disappearing into a vendor dashboard.
Example:
# TRANSLATING.md
## Product terms
- Translate "workspace" as "Arbeitsbereich" in German.
- Do not translate "TranslateBot".
- Keep "API" as "API", do not expand it.
## Technical rules
- Preserve placeholders like %(name)s, %s, and {0}.
- Preserve HTML tags and attribute values.
- Keep button labels short.
That mirrors a real translation best practice. Professional technical translation workflows rely heavily on terminology management and CAT tools, with 88% of translators using them, as described in this overview of technical translation challenges and terminology workflows.
Git discipline matters more than people admit
Locale files are documents, but you should treat them like code artifacts. Review history, diff quality, and merge hygiene matter.
If your team needs a good refresher on that side of the problem, these document version control best practices map well to translation files too. The same habits apply. Small commits, readable diffs, and one source of truth beat giant “localization update” dumps every time.
A practical review checklist:
- Check placeholders first. They fail loudly.
- Scan glossary terms. Product names drift.
- Open high-risk templates. Especially those with links, formatting, or inline variables.
- Review changed source comments. Translator hints often matter more than the raw string.
Integrating Translation into Your CI/CD Pipeline
Friday afternoon is a bad time to discover that a release branch still has empty msgstr entries. The fix is usually simple. The cost is not. Someone has to regenerate messages, fill gaps, compile locales, and make sure placeholders survived. If that work lives outside the pipeline, it gets skipped until the worst possible moment.

CI fixes the timing problem. Every pull request can extract strings, translate missing entries, compile catalogs, and fail fast if locale files changed unexpectedly. That keeps translation inside the same loop as tests, formatting, and migrations. No portal export. No side spreadsheet. Just a diff in Git.
Reviewable diffs are the point
A .po update should look like any other code review artifact. If a support lead or native-speaking teammate can read the diff in GitHub and comment inline, the workflow is working.
Example diff:
#: accounts/templates/accounts/welcome.html:8
#, python-format
msgid "Welcome back, %(name)s."
-msgstr ""
+msgstr "Willkommen zurück, %(name)s."
#: billing/templates/billing/settings.html:12
msgid "Billing settings"
-msgstr ""
+msgstr "Abrechnungseinstellungen"
That format gives reviewers enough context to catch the mistakes that matter in practice: wrong product terms, awkward phrasing, broken placeholders, or strings translated without the surrounding UI in mind.
Professional technical translation teams still rely heavily on terminology management and CAT-style consistency. 88% of translators use those tools, according to this guide on technical translation workflows and glossary use. In a Django repo, the practical version is simpler: keep your glossary in the repo, make translation runs deterministic, and let CI enforce the sequence.
A GitHub Actions example
This job runs on pull requests and manual dispatch. It installs gettext, extracts messages, fills missing translations, compiles catalogs, then fails if the working tree changed. That last check matters. It prevents "works on my machine" locale drift.
name: i18n
on:
workflow_dispatch:
pull_request:
paths:
- "**/*.py"
- "**/*.html"
- "locale/**"
- "TRANSLATING.md"
jobs:
translate:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install gettext
run: |
sudo apt-get update
sudo apt-get install -y gettext
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install translatebot-django
- name: Extract messages
run: |
python manage.py makemessages -l de
- name: Translate missing strings
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
python manage.py translate --target-lang de
- name: Compile messages
run: |
python manage.py compilemessages
- name: Check for uncommitted locale changes
run: |
git diff --exit-code
Use the secret that matches your configured backend. The command order matters more than the provider.
For teams tightening their release process, broader CI/CD pipeline best practices apply here too. Deterministic generation, small diffs, and repeatable builds make translation automation boring in the best way. If you need the package-specific setup, the docs for running Django translation automation in CI cover the environment variables and job shape in more detail.
A short demo helps if you’re selling the workflow internally:
Where CI translation earns its keep
The payoff is highest in a few common cases:
- Feature branches add new UI copy every week. Locale updates stay with the branch instead of piling up before release.
- The team ships continuously. Translation stops being a manual checkpoint someone remembers late.
- Review has to happen in Git. Engineers, PMs, and support can all inspect the same diff without another tool.
There is a trade-off. CI-generated translations can produce noisy pull requests if string extraction runs on every tiny template change. I usually limit the workflow to changed paths, keep glossary updates explicit, and fail only on meaningful locale drift. That keeps the automation strict enough to catch problems without turning every PR into a localization event.
Portal-based localization still fits larger organizations with dedicated language operations. For a Django team that already owns code, deploys, and docs in Git, terminal-first translation in CI is usually the faster and cheaper setup.
A Pragmatic Guide to QA and Cost
You still need QA. You just don’t need the heaviest process for every string in every release.
QA that matches app development reality
In formal technical translation, the gold standard is TEP, meaning Translation, Editing, Proofreading, with at least two linguists in the process, as explained in JR Language’s overview of scientific and technical translation workflows. That’s right for high-stakes material.
Most Django teams won’t run a full TEP process on every admin label and settings screen. A practical version looks more like this:
- Review high-risk flows. Billing, auth, legal notices, and onboarding deserve attention first.
- Use native speakers where you have them. A teammate can proof a diff quickly.
- Check in the UI, not only in the
.pofile. Context errors show up faster in templates. - Treat technical strings differently from marketing copy. Product prose often needs more rewriting.
Raw machine output is a draft. Your shipping artifact is the reviewed diff.
That level of review catches most embarrassing mistakes without turning translation into a separate department.
The pricing problem nobody explains well
A lot of translation content talks about quality and almost none of it talks about ownership cost. That gap is real. As discussed in this analysis of translation workflow ROI and cost opacity, development teams rarely get a clean way to compare build-integrated workflows with vendor portals.
So compare them directly.
| Method | Estimated Cost | Workflow |
|---|---|---|
| AI translation in your Django workflow | Qualitatively, often pennies per run when translating only changed strings | makemessages, translate in terminal, review diff, compilemessages |
| Traditional TMS subscription | Recurring subscription cost, usually tied to seats, projects, or usage | Sync locale files to external portal, review there, pull back into repo |
| Human agency translation | Higher per-project cost, strongest fit for critical content | Send source externally, wait for delivery, re-import and review |
The exact cost depends on provider, string length, and how much content changed. The key difference is structural. A build-integrated workflow translates only new or changed entries. A portal subscription is still a subscription even when your release is quiet.
When to spend more
Some content deserves human specialists from the start:
- Regulatory text
- Safety instructions
- Contracts and legal notices
- Public marketing pages with strong brand voice
For the rest, the most effective split is usually:
| Content type | Best first pass |
|---|---|
| Internal tools, admin UI, repetitive settings text | Automated translation with Git review |
| Product UX copy with moderate nuance | Automated draft plus native review |
| Compliance or safety content | Human-led translation |
That’s a much easier budget conversation than “we need a localization platform because we support another language now.”
Troubleshooting Your Automated Translation Workflow
Even a clean pipeline will fail in familiar ways. The good news is that most of the fixes are mechanical.
A string stayed untranslated
Check whether the entry is marked fuzzy or whether the source changed without a clean extraction pass.
#, fuzzy
msgid "Billing settings"
msgstr "Abrechnung"
Fix:
- Remove the
#, fuzzyflag if the translation is valid enough to rework. - Re-run
makemessages. - Re-run your translation command.
If the string still doesn’t show up, confirm it’s marked for translation in Python or templates.
The translation is out of context
Short UI strings are the usual culprit. Add translator comments or explicit context.
Python:
from django.utils.translation import pgettext_lazy
label = pgettext_lazy("billing action button", "Charge")
Template comment for translators:
{% load i18n %}
{# Translators: Button that charges the customer's saved payment method immediately. #}
<button>{% translate "Charge" %}</button>
Then regenerate messages:
python manage.py makemessages -l de
python manage.py translate --target-lang de
Placeholders broke
Stop and fix that before anything else. Don’t patch around it in templates.
Look for changes to:
- Python format strings like
%(name)s - Percent placeholders like
%s - Brace placeholders like
{0} - HTML attributes inside translated text
If your workflow can’t preserve those reliably, it isn’t safe for technical document translation in a Django app.
Plural forms look wrong
Pluralized strings need inspection in the target locale. Don’t trust a one-line review.
Example:
msgid "%(count)s invoice"
msgid_plural "%(count)s invoices"
msgstr[0] ""
msgstr[1] ""
Fix path:
- Verify Django extracted plural forms correctly.
- Translate all plural entries.
- Test with values that hit different plural branches in the target language.
The diff is too noisy to review
That’s usually process, not translation quality.
Try this:
- Commit extracted source changes first
- Commit translation updates second
- Keep glossary changes separate
- Avoid mixing locale rewrites with unrelated refactors
A reviewable diff is your lightweight proofreading step. The full TEP process uses multiple specialists, but a Git diff still gives a native speaker on your team a fast way to catch obvious issues before merge, which is the practical part worth preserving from that model.
compilemessages fails in CI
Most often, either gettext isn’t installed in the runner or the .po file has syntax damage.
Check:
- gettext package exists on the runner
- Quotes are balanced in the
.pofile - Multi-line strings are valid
- Placeholders weren’t malformed during editing
When in doubt, run the same commands locally in a clean environment and compare output.
If you want to keep technical document translation inside Django instead of pushing it into another platform, TranslateBot is built for that workflow. It runs as a manage.py translate command, writes directly to your .po files, preserves placeholders and HTML, and keeps changes reviewable in Git so your team can ship translations the same way it ships code.