You’ve already done the easy part. You marked strings, ran makemessages, and now your repo is full of half-empty .po files waiting for attention. That’s where most Django i18n workflows go bad.
The messy part isn’t extracting strings. It’s keeping placeholders intact, not breaking HTML, handling copy changes without retranslating everything, and reviewing translation changes in Git like they’re normal code. If your current process involves copy-pasting msgid values into a browser tab, you already know how fragile that gets.
A lot of content about examples of translations is still stuck in school math. It talks about sliding triangles across a grid, not shipping a multilingual Django app without corrupting %(name)s or turning email markup into invalid HTML. That gap matters because developers keep dealing with translation problems that generic localization guides barely touch.
One reason I take format safety seriously is that technical translation work shows how costly small string mistakes become in specialized domains. In a set of engineering translation projects, client data sheets had terminology inconsistency across annual reports before expert translation, then reached much tighter consistency afterward, with zero breakage in technical placeholders and much faster Git-diff review cycles once outputs became reproducible in these technical translation examples. Django apps aren’t nuclear reports, but the lesson is the same. Broken formatting in translated strings causes real review pain and real release risk.
The examples below stay close to the code. No screenshots from a portal you’ll stop using in a month. Just the kind of before-and-after diffs that show what good translation looks like in a local Django workflow.
1. Django Placeholder Preservation
The fastest way to break a translated Django app is to let a tool rewrite placeholders.
If your source string contains %(name)s, %s, or {0}, the translation has to preserve it exactly. Not approximately. Not with “cleaned up” spacing. Exactly.

What correct output looks like
Take a common Django string:
msgid "Hello %(name)s, welcome to our platform"
msgstr ""
A safe French translation looks like this:
msgid "Hello %(name)s, welcome to our platform"
-msgstr ""
+msgstr "Bonjour %(name)s, bienvenue sur notre plateforme"
The important part isn’t the French. It’s that %(name)s survived untouched.
Same pattern for transactional strings:
msgid "Your order %(order_id)s has shipped"
-msgstr ""
+msgstr "Votre commande %(order_id)s a été expédiée"
And for trial messaging:
msgid "Hi %(first_name)s, your trial ends in %(days)s days"
-msgstr ""
+msgstr "Hola %(first_name)s, tu prueba termina en %(days)s días"
That’s one of the most useful examples of translations in app code because it’s where “looks fine in review” often turns into a runtime error.
Practical rule: If a translation tool can’t guarantee placeholder preservation, don’t let it touch your
.pofiles.
What works and what doesn’t
What works:
- Keep placeholder names descriptive:
%(first_name)sis easier to translate safely than%(x)s. - Add translator comments in code: Explain what a variable means when the string is ambiguous.
- Test with real values: Render the translated string in a test or shell before shipping.
What doesn’t work:
- Manual browser translation: People accidentally edit placeholder names all the time.
- String concatenation: You lose context and make translation quality worse.
- Treating placeholders as translator knowledge: The tool should protect them. You shouldn’t rely on luck.
Good writing helps too. If you want source strings that survive translation better, write them like you expect another language to read them. The advice in writing for translation applies directly to Django templates and gettext() calls.
2. HTML Tag Preservation in Rich Text
HTML inside translatable strings is annoying, but sometimes it’s the least bad option.
You see it in email templates, CMS snippets, banner copy, and admin help text. The problem is simple. The translator needs to change the words, not the markup.
A safe rich-text translation
Start with this:
msgid "<strong>Sale ends soon</strong>"
msgstr ""
A valid French result keeps the tag structure intact:
msgid "<strong>Sale ends soon</strong>"
-msgstr ""
+msgstr "<strong>L'offre se termine bientôt</strong>"
The same rule applies when placeholders and tags appear together:
msgid "<em>New message from %(user)s</em>"
-msgstr ""
+msgstr "<em>Nouveau message de %(user)s</em>"
And for links:
msgid "<a href=\"/help\">Click here</a> for support"
-msgstr ""
+msgstr "<a href=\"/help\">Cliquez ici</a> pour obtenir de l'aide"
What you want is balanced tags, preserved attributes, and translated text only.
Keep the markup boring
Most HTML translation problems are self-inflicted.
If your msgid contains nested spans, inline styles, and a chunk of template logic, the translator has no chance. Keep the string small and keep the markup simple.
A good pattern in templates:
{% blocktrans trimmed %}
<strong>Limited time:</strong> Save on your next order
{% endblocktrans %}
A less good pattern:
{% blocktrans %}
<span class="promo" data-id="{{ promo.id }}"><strong>Limited time:</strong> Save on your next order</span>
{% endblocktrans %}
Move the noisy parts outside the translatable text when you can.
HTML in translations should carry meaning, not layout.
That means strong, em, and links are usually fine. A pile of presentational markup usually isn’t.
Rendered output matters more than the .po diff here. Review the page in the target language. Check broken wrapping. Check whether linked text still makes sense. Check whether email clients mangle the result. A string can be technically valid and still look bad in production.
3. Incremental Translation Detection
Retranslating the whole file every time copy changes is wasteful. It’s also how teams start avoiding translation updates because the process feels expensive and slow.
The better pattern is boring and effective. Translate only what changed.
A realistic diff
Say your product team changes a button label:
-msgid "Sign up"
+msgid "Create account"
You don’t want to touch every existing French, Spanish, and German string because one msgid changed. You want the tool to detect the new or fuzzy entry and update only that unit.
A typical flow looks like this:
django-admin makemessages -a
translatebot --languages fr,es,de
git diff locale/
Then your diff stays small:
#: templates/signup.html:18
-msgid "Sign up"
-msgstr "S'inscrire"
+msgid "Create account"
+msgstr "Créer un compte"
That’s the difference between a reviewable commit and a noisy translation dump.
Why this matters in practice
In fast-moving apps, copy changes constantly. Button text changes. Error messages get clarified. Onboarding screens get rewritten. If translation only runs on delta strings, you can keep localization close to normal product work instead of batching it into a dreaded “language pass.”
That style of workflow also maps well to translation memory ideas. If you’re comparing approaches, this piece on translation memory programs is a useful reference point for understanding why repeated content shouldn’t be processed like brand new text every time.
A few habits make incremental updates work better:
- Commit
.pofiles after each pass: Small diffs are easier to trust. - Review fuzzy entries carefully: Copy edits often change meaning, not just wording.
- Run translation after extraction, not before: The pipeline should follow Django’s source-of-truth files.
One reason delta-based workflows are worth caring about is that software localization content is still underserved compared with generic “translation” content. A lot of top-ranking material focuses on geometry, not placeholder-safe .po updates. The gap shows up in developer frustration too. A summary of that gap points out unresolved questions around Django gettext placeholder corruption and the lack of practical CLI-based guidance in common search results in this discussion of translation examples versus localization reality.
4. Glossary-Driven Consistency
Translation quality often fails on the boring words. Not on poetry. On product nouns.
“Workspace.” “Organization.” “Terms of Service.” “Cart.” If those drift between releases, your app feels sloppy fast.
Put the glossary in Git
For Django teams, a plain TRANSLATING.md file in the repo beats a hidden glossary inside some separate platform.
Example:
# TRANSLATING.md
## Product terms
- workspace
- fr: espace de travail
- note: never translate as a personal desk or physical office
- organization
- fr: organisation
- note: refers to a user group inside the app, not a company name
- Terms of Service
- fr: Conditions d'utilisation
- note: keep legal wording consistent across auth, footer, and checkout
Now the rules live next to code changes. If product language changes, the glossary changes in the same PR.
Real examples where consistency matters
E-commerce teams run into this with “cart.” In French, you probably want panier, not a literal word that sounds like a shopping trolley in the wrong context.
SaaS teams hit it with “organization.” If one screen says the equivalent of “company” and another means “user group,” users notice. Legal text is even less forgiving. “Terms of Service” and “Conditions of Use” aren’t interchangeable once your product has settled on one phrasing.
Glossary-driven translation has become more relevant as teams push localization into CI. One future-dated conference trend summary says glossary-driven models improved terminology consistency in CI/CD pipelines according to this 2025 projection referenced in a translation examples PDF context. The exact number matters less than the workflow lesson. Teams get better output when they write down terminology rules instead of hoping the model infers them every time.
Write glossary rules the same way you write API constraints. Short, explicit, versioned.
Start with the terms users see everywhere. Brand names. Plan names. Billing language. Security wording. Don’t try to document every noun in week one. The high-frequency terms do most of the work.
5. Plural Form Handling
Pluralization is where “looks translated” and “is correct” split apart.
English lets people get lazy. Plenty of apps fake it with string concatenation or one singular and one plural form. That falls apart fast once you add languages with more complex rules.

Use ngettext() or accept bugs
Bad pattern:
message = str(count) + " items"
Good pattern:
from django.utils.translation import ngettext
message = ngettext(
"%(count)s item",
"%(count)s items",
count,
) % {"count": count}
In a .po file, that gives translators the plural forms Django expects for the target locale.
French is easy enough to illustrate:
msgid "%(count)s item"
msgid_plural "%(count)s items"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%(count)s article"
+msgstr[1] "%(count)s articles"
The main point isn’t French. It’s that your code should give the translation layer enough structure to apply language-specific plural rules.
Where teams get this wrong
The most common bug is still concatenation. The second most common is reusing one English-shaped message in every locale and assuming it generalizes.
It doesn’t.
Polish, Russian, and Arabic need more thought. Chinese needs different thought. If your app has carts, notifications, invoices, or usage quotas, plural handling isn’t optional.
Test actual counts. Don’t stop at 1 and 2. Render 0, 1, 2, 5, and 21 in the target language and look at the final UI.
One useful reminder from AI translation evaluation in advertising content is that context and linguistic authenticity matter a lot. In that comparison, Bard did better on native Hindi phrasing in some cases, while Claude handled technical language and contextual nuance especially well, and manual translations previously often needed multiple revision cycles before AI-assisted workflows reduced revision needs and improved first-pass authenticity in the ARF case study on translation of text. For plural strings, that same lesson applies. Don’t judge quality by “did the words get translated.” Judge it by whether the output reads like a real sentence for that count.
6. Context Comments for Ambiguous Strings
Some English strings are traps.
“Account” can mean billing account or user profile. “Present” can be a gift or a verb. “Bank” can be finance or geography. Translating those without context is bad engineering.
Add guidance where the ambiguity starts
Django already gives you tools for this.
In code:
from django.utils.translation import pgettext
label = pgettext("user profile section", "Account")
Or with a translator comment:
# Translators: "Account" means a user's profile, not a bank account.
title = gettext("Account")
That flows into the .po file:
#. Translators: "Account" means a user's profile, not a bank account.
msgid "Account"
msgstr ""
That one sentence often fixes the whole problem.
Context beats cleanup later
Developers usually add comments only after someone reports an awkward translation. That’s backwards.
If a word has multiple meanings in English, document it at extraction time. You already know the context while writing the feature. A translator or model won’t.
Examples that deserve comments:
- Button labels: “Open”, “Close”, “Save”
- Product nouns: “Workspace”, “Seat”, “Plan”
- Finance terms: “Account”, “Balance”, “Charge”
- Support copy: “Issue”, “Ticket”, “Case”
Keep comments short. One sentence. Domain plus meaning. That’s enough.
A translator can’t recover context you never encoded.
One of the better habits for examples of translations in Django is to show not just the string, but the note above it. The note often makes the difference between a polished locale file and one full of technically correct but wrong-in-context phrasing.
7. CI/CD Integration in Git Workflows
If translation happens outside your normal release flow, it gets skipped.
That’s why portal-first localization feels wrong for many small Django teams. You make code changes in Git, but translations live somewhere else, behind manual upload steps and UI clicks nobody wants to own.
Keep the pipeline boring
A translation job should look like any other build step.
Here’s a simple GitHub Actions shape:
name: translate
on:
push:
branches: [main]
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: django-admin makemessages -a
- run: translatebot --languages fr,es,de
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- run: django-admin compilemessages
That’s enough for many teams. Extract. Translate. Compile. Review the diff.
For teams already investing in modern CI/CD pipelines, localization belongs in the same automation path as tests and asset builds.
Here’s a quick demo format that matches the same idea:
Why Git-based automation is better than portal clicks
The big win is reviewability. A pull request shows exactly which strings changed. You don’t have to trust a sync from some dashboard.
Another win is that the tooling stays local. The workflow described in translation and technology fits how developers already work. Terminal commands, environment variables, CI jobs, and diffs.
A few practical rules help:
- Run translation after
makemessages: Otherwise you’re translating stale files. - Compile in CI too: Catch broken syntax before deploy.
- Review locale diffs like code: Small language mistakes are still defects.
The less your team has to leave Git, the more likely translations stay current.
8. Reproducible Translation Diffs
A translation platform can tell you that French was updated. Git can tell you exactly what changed, when it changed, and what else changed with it.
That’s the standard I want for localization work.
The audit trail lives in the repo
When translations write directly to locale/*/django.po, you get normal Git history:
git log --oneline, locale/fr/django.po
git diff HEAD~1, locale/es/django.po
git blame locale/de/django.po
That matters for real maintenance work.
If a release introduces awkward wording, you can inspect the commit. If legal text changed in one locale but not another, the diff shows it. If someone asks why a product term changed, the commit that updated TRANSLATING.md can sit next to the locale update.
A clean commit often looks like this:
git add locale/ TRANSLATING.md
git commit -m "Update onboarding copy translations with glossary changes"
That’s much easier to reason about than “someone changed strings in the portal last week.”
What review looks like
Say a translator or tool changes a phrase in French:
-msgstr "Créer un espace de travail"
+msgstr "Créer un espace de travail d'équipe"
Now you can ask the right question. Did the product meaning change, or did the wording drift? Git makes that visible.
If you want a quick visual comparison outside the terminal, a tool like diff check online can help for one-off reviews, but the long-term value still comes from versioned locale files in the repo.
The strongest examples of translations for developers aren’t glamorous. They’re reproducible. They survive review, can be reverted cleanly, and don’t disappear into a vendor account.
8-Example Translation Comparison
| Feature | Implementation Complexity 🔄 | Resource / Setup Effort ⚡ | Expected Outcomes ⭐ | Ideal Use Cases 📊 | Key Advantages & Tips 💡 |
|---|---|---|---|---|---|
| Django Placeholder Preservation: %(name)s → %(name)s (French) | Low, automatic detection and protection of format placeholders | Low, minimal infra; translator guidance recommended | Prevents runtime VariableDoesNotExist errors; preserves injection safety | UI strings, emails, notifications, e-commerce order messages | Document placeholders (TRANSLATING.md), run makemessages, test with real data |
| HTML Tag Preservation in Rich Text: 'Sale ends soon' → French | Medium, HTML parsing, locking and tag validation required | Medium, translator training for inline HTML and rendering tests | Avoids broken HTML and sanitization errors; preserves styling | Marketing copy, email templates, rich-text UI fields | Keep tags minimal, move complex markup to templates, test rendered output |
| Incremental Translation Detection: Changed Strings Only → Re-translation Workflow | High, change detection, fuzzy tracking, version-aware logic | Low ongoing token costs; initial setup for .po management | Large reductions in cost and latency; only changed strings translated | Rapid-iteration SaaS, frequent releases, A/B testing | Mark fuzzy entries, run in CI, commit .po files for clear diffs |
| Glossary-Driven Consistency: TRANSLATING.md Context Rules → Unified Terminology | Medium, integrate versioned glossary with LLM prompts | Medium, initial curation and periodic maintenance effort | Consistent terminology and reproducible translations across releases | Brand-sensitive products, legal/technical domains, multi-team projects | Start with 20–30 key terms, review quarterly, commit glossary changes with code |
| Plural Form Handling: '1 item' vs. '%(count)s items' → Language-Specific Rules | Medium, ngettext integration and CLDR rule application | Low–Medium, testing of plural edge cases per language | Grammatically correct plural forms for target languages | Notification counts, cart badges, quantity displays | Use ngettext, test 0/1/2/5/21, document complex plural contexts |
| Context Comments for Ambiguous Strings: '%(context)s' → Translator Guidance | Low–Medium, add pgettext and concise translator comments | Low, developer time to add contextual comments | Reduces mistranslations of ambiguous single words | Short labels, UI buttons, domain-specific one-word strings | Keep comments concise (1–2 sentences), include domain context, use pgettext |
| CI/CD Integration: Automated locale/ File Updates in Git Workflows | High, CI pipeline, secrets, timeouts, and Git automation | Medium, CI configuration and runner resources; review workflows | Automated, reviewable translations committed to repo; reproducible builds | Teams using Git-based reviews and automated releases | Run after makemessages, store keys in secrets, review translation diffs before merge |
| Reproducible Translation Diffs: Version-Controlled locale/*/django.po → Auditable History | Medium, write .po to repo and manage commit metadata | Low–Medium, Git discipline and conflict resolution practices | Full audit trail, easy rollback, transparent terminology evolution | Compliance-sensitive projects, open-source, teams needing traceability | Commit locale changes separately, include TranslateBot version, use git blame |
Translation as Code, Not as a Chore
These examples all point to the same shift. Good Django translation work doesn’t happen in a side channel. It happens in the same places you already trust for code changes. Your editor. Your terminal. Your tests. Your Git history.
That matters because the worst translation workflows fail in familiar ways. They hide changes behind a web UI. They mix language review with file export problems. They make simple copy edits feel expensive. They turn .po files into artifacts you only touch right before release. Then everyone avoids touching them until the app drifts out of sync across languages.
A code-level workflow fixes a lot of that.
Placeholder preservation stops being a manual spot-check and becomes a format safety guarantee. HTML preservation stops depending on whether someone notices a broken closing tag in review. Incremental translation means copy updates don’t drag the whole project into another full localization pass. A versioned glossary stops product terminology from drifting because one screen said “workspace” and another said the equivalent of “office.” Context comments make ambiguous English strings translatable on the first pass instead of after a bug report.
CI helps too, but only if you keep it simple. Extract strings. Translate changed entries. Compile messages. Review diffs. Merge. That’s a workflow developers will keep running. The more it looks like normal engineering work, the less likely it gets ignored.
The Git part is easy to underestimate. Reproducible diffs are not just nice for process people. They’re practical. They let you answer very concrete questions. What changed in French between releases? When did legal wording shift in Spanish? Which commit introduced an awkward translation in onboarding? Portal-based systems usually answer those questions badly, or not at all. A repo answers them in seconds.
There’s also a quality lesson hiding underneath all this. Translation quality is rarely about one dramatic mistake. It’s usually death by small cuts. A placeholder gets edited. A count string uses the wrong plural form. A rich text string loses its tag balance. A product noun drifts. A vague English word gets translated because no one added a comment. None of those issues are hard individually. They become hard when the workflow makes them invisible.
That’s why I think “examples of translations” are most useful when they’re shown as code diffs instead of abstract language theory. Developers don’t need another definition of translation. They need to see what a safe, reviewable, boringly reliable translation change looks like inside locale/fr/django.po.
Start with one thing. Add a TRANSLATING.md file to your repo. Write down the handful of terms that users see everywhere. Then run your next translation pass in a way that updates only changed strings and leaves a diff you can review like any other commit. If the output is small, readable, and safe to merge, you’re on the right path.
If you want that workflow without another portal, TranslateBot is built for it. It translates Django .po files from the CLI, preserves placeholders and HTML tags, updates only new or changed strings, and writes the results straight back to locale/*/django.po for clean Git diffs. For a small team or solo project, that’s usually the difference between “we should localize later” and shipping translated releases.