Meta description: Translation of quotes can break your Django build. Use a safe workflow to preserve placeholders, HTML, and quote syntax in .po files.
You run python manage.py compilemessages before a deploy, and gettext throws an error at a line you touched five minutes ago.
You open locale/fr/LC_MESSAGES/django.po and find a quote inside a msgstr that looked harmless in review. It isn't. One translator changed a placeholder, another pasted curly quotes from a doc, and now your build is blocked by punctuation.
#: billing/templates/billing/invoice.html:18
#, python-format
msgid "\"%(customer_name)s\" said, \"Pay now\""
msgstr ""%(customer_name)s" a dit, "Payez maintenant""
That file is broken in two ways. The outer PO string is malformed, and the internal quoting is no longer escaped. If the string also carried HTML or a plural form, cleanup gets worse fast.

The translation of quotes isn't an editorial edge case. Translation scholarship treats quotations as a distinct problem because they carry wording, tone, context, and attribution together, and small changes can alter meaning and authority in the result, as noted in this study of quotations in translation. In Django, you get that linguistic risk plus parser fragility.
If you're debugging parse failures beyond gettext, these language-agnostic error solutions are useful because the failure pattern is the same. A tiny delimiter mistake can invalidate the whole file.
Keep one rule in mind from the start: .po files are data with syntax, not text blobs. If you need a refresher on escaping rules and layout, this PO file format guide is worth bookmarking.
Your msgstr Is a Syntax Error Waiting to Happen
The hard part isn't translating words. The hard part is preserving everything around the words.
A quote in product copy often includes at least one of these:
- A placeholder like
%(name)sor%s - HTML like
<strong>or<a> - Nested punctuation like
'inside" - Source intent that changes if tone shifts
Practical rule: treat any quoted UI string as structured content. Review it like code, not marketing copy.
Here's a more realistic failure from a Django app:
#: app/templates/dashboard/welcome.html:12
#, python-format
msgid "Click \"Start trial\" to continue, %(name)s."
msgstr "Cliquez sur "Commencer l’essai" pour continuer, %s."
Three breakages in one line:
| Problem | What changed | Result |
|---|---|---|
| Delimiters | Internal " not escaped |
PO parser fails |
| Placeholder | %(name)s became %s |
Runtime formatting can fail |
| Typography | apostrophe changed style | Fine visually, but mixed conventions spread |
Manual copy-paste causes this because translators work in browsers, docs, spreadsheets, chat, or vendor portals that don't expose the actual syntax constraints. You only find the mistake when compilemessages trips over it.
Why Translating Quotes Breaks Your Django App
A quote bug usually lands in production as something that looked harmless in review. One changed character, one copied sentence from a rich text editor, one translator trying to make the string read naturally.

In Django, quoted strings fail in a few repeatable ways. The hard part is that each failure hits a different stage. Some break compilemessages. Some survive compilation and fail at render time. Some render fine but create inconsistent UI that reviewers only notice after the release.
Placeholders get rewritten
This happens constantly in localized quotes because the placeholder sits right next to human text, so it looks editable.
#: app/views.py:44
#, python-format
msgid "\"%(user)s\" added %(count)s file(s)"
msgstr "\"%(usuario)s\" agregó %(count)d archivo(s)"
That translation is invalid for two separate reasons:
%(user)swas renamed to%(usuario)s%(count)schanged type to%(count)d
Django expects the translated string to preserve the same placeholder contract as the source string. If the code passes {"user": "Ana", "count": "3"}, this msgstr can fail even though the PO file itself parses cleanly.
Correct version:
#: app/views.py:44
#, python-format
msgid "\"%(user)s\" added %(count)s file(s)"
msgstr "\"%(user)s\" agregó %(count)s archivo(s)"
If the target language needs different grammar, solve that in the message design. Use ngettext, split the sentence, or restructure the source string. Do not push grammar fixes into placeholder edits.
HTML inside quotes gets mangled
Quoted UI copy often carries markup because product teams want part of the sentence emphasized.
#: app/templates/account/delete.html:9
msgid "Type \"DELETE\" to confirm. <strong>This cannot be undone.</strong>"
msgstr "Tapez \"SUPPRIMER\" pour confirmer. <strong>Ceci ne peut pas être annulé.</strong>"
The risky part is not the French. The risky part is that PO editors, browser forms, and CAT tools all treat markup a little differently. A translator may preserve the words and still break the string by moving a tag, dropping a close tag, or changing escaping.
Bad edit:
#: app/templates/account/delete.html:9
msgid "Type \"DELETE\" to confirm. <strong>This cannot be undone.</strong>"
msgstr "Tapez "SUPPRIMER" pour confirmer. <strong>Ceci ne peut pas être annulé.<strong>"
Now you have two classes of failure in one line. The quotes are no longer escaped, and the HTML is malformed.
The production rule is simple. Keep HTML outside translatable strings whenever you can. If the HTML must stay inside, validate both the PO syntax and the rendered HTML before shipping.
Nested quotes stop being obvious
Nested quotes are where good translators and safe strings start pulling in opposite directions.
msgid "The admin wrote, \"Mark this as 'urgent'.\""
msgstr "L’administrateur a écrit : « Marquez ceci comme “urgent”. »"
The rendered result may be correct for the locale. The source file can still become fragile if someone edits that line in a plain text environment and replaces one set of quotes with another without checking escaping. Straight quotes, curly quotes, guillemets, and apostrophes all look close enough to invite mistakes.
I treat nested quotes as a review hotspot for that reason. If the UI can be rewritten to avoid quoting quoted text, do it. If it cannot, keep a house style per locale and enforce it in review instead of letting each translator choose punctuation on the fly.
Smart quotes create noisy diffs and inconsistent UI
Smart quotes are not wrong in themselves. Uncontrolled smart quotes are the problem.
One string comes from a PO editor and keeps straight quotes. Another is pasted from Word and gets curly quotes. A third comes from a spreadsheet round-trip and loses escaping. Django does not care about the typography until a delimiter, placeholder boundary, or HTML fragment gets caught in the shuffle.
Typical failure points:
| Input source | Typical issue | What shows up later |
|---|---|---|
| Word or Google Docs | Auto-converted quotes | Mixed punctuation across screens |
| Spreadsheet export/import | Escapes dropped or doubled | Broken msgstr |
| Vendor portal with rich text handling | Curly quotes plus normalized HTML | Valid PO, wrong render |
| Manual edits in raw files | Quote style changed near placeholders | Review misses a runtime bug |
This is a workflow bug, not a language bug. Teams need a quote policy for each locale, plus a check that flags unexpected punctuation changes in translated strings.
Template syntax adds another layer
Templates make quote handling less forgiving because translation syntax and template syntax sit next to each other.
<p>{% translate "Click \"Save\" to keep your changes." %}</p>
Or with interpolation:
{% blocktranslate with plan_name=plan.name %}
You chose "{{ plan_name }}".
{% endblocktranslate %}
The second example is where edge cases pile up. A translator may switch the quote style correctly for the locale, but also move {{ plan_name }} inside a different punctuation pattern, add spacing that changes output, or copy in smart quotes around the variable itself. The PO file can still compile while the rendered sentence looks wrong or breaks test expectations.
The safer pattern is intentionally boring:
- Use
blocktranslateonly when interpolation is required - Keep HTML outside the translatable string where possible
- Use named placeholders, not positional ones
- Reduce nested quoting in source copy if the UI can express the same meaning more directly
- Review rendered output for strings that combine quotes, variables, and markup
Quotes break Django apps because they sit at the intersection of language, syntax, and rendering. A normal sentence rarely hits all three at once. A quoted string often does.
Moving Beyond Manual Copy-Paste
A translator drops a French quote into django.po, swaps straight quotes for guillemets, and the string looks fine in the editor. Then someone notices the entry also contains %(name)s, a <strong> tag, and a literal quote inside the sentence. Manual copy-paste breaks down fast once one string mixes punctuation, placeholders, and markup.
The fix is to give translators better inputs and to keep quote policy close to the code.
Context beats guessing
Quoted UI text is often ambiguous in a way plain labels are not. If two source strings both contain "Close", one can be a button label and the other part of user feedback. Without context, the translator has to infer intent from a bare msgid, and quote style often changes with that intent.
from django.utils.translation import gettext_lazy as _
from django.utils.translation import pgettext_lazy
button_label = pgettext_lazy("button label", "Close")
quoted_feedback = pgettext_lazy("customer quote", "\"Close\" was confusing")
title = _("Billing")
pgettext will not save a bad source string, but it does remove one common source of mistranslation. That matters more with quotes because translators may change both wording and punctuation to match local conventions.
Put quote rules in the repo
If quote handling lives only in a reviewer's head, the same string will come back in three different styles across three releases. Keep the rules in version control so translators, reviewers, and developers are using the same reference.
A small TRANSLATING.md file is usually enough:
- Brand terms: keep
TranslateBot,Admin, or plan names untranslated if that is the policy - Quote style: define the expected quote marks for each locale, including spacing rules
- Placeholder policy: never alter
%(name)s,%s,{0},{{ variable }} - HTML policy: preserve tags exactly, translate text nodes only
- Smart quote policy: decide whether translators should output locale-native curly quotes directly or whether normalization happens later in the pipeline
That last point matters. Some teams want typographically correct quotes in msgstr. Others normalize punctuation during rendering or content review. Either approach can work. Mixing both creates churn and hard-to-review diffs.
If you want the translation step to stop being a manual editor exercise, this guide to automating PO file translation in Django is the right next step.
Keep source strings boring
The hardest quote bugs come from source strings that try to do everything at once. A sentence with nested quotes, HTML, placeholders, and legal text gives translators too many ways to break syntax or output.
Bad source:
_("\"%(name)s\" accepted the <strong>Terms of Service</strong> on %(date)s.")
Better:
pgettext_lazy("audit log event", "\"%(name)s\" accepted the Terms of Service on %(date)s.")
Then apply emphasis in the template instead of embedding it in the translatable string.
This also helps with nested quotes. If the source copy already contains a quoted phrase inside another quoted sentence, reconsider the copy before it reaches translators. In production, the cleanest fix is often editorial, not technical. Split one overloaded sentence into two smaller messages, or remove the inner quote marks if the UI already signals quoted content visually.
Manual copy-paste works for plain strings. Quote-heavy strings need structure, context, and rules the team can enforce.
Automating Safe Translation of .po Files
You already know the outer loop. Extract messages, fill translations, compile them, ship.
The failure is always in the middle step, where someone opens django.po and edits raw strings by hand.

Use commands, not copy-paste
A safe Django workflow starts the same way every time:
python manage.py makemessages --all
That updates files like:
locale/fr/LC_MESSAGES/django.po
locale/de/LC_MESSAGES/django.po
locale/es/LC_MESSAGES/django.po
From there, the middle step should parse the PO file, translate only untranslated entries, and preserve placeholders and markup. If you're automating Django i18n already, this write-up on automating PO file translation in Django covers the shape of that workflow.
Before automation:
#: app/templates/support/quote.html:7
#, python-format
msgid "\"%(agent)s\" said, \"Your ticket is now <strong>resolved</strong>.\""
msgstr ""
After automation, what you want is not "creative." You want disciplined output:
#: app/templates/support/quote.html:7
#, python-format
msgid "\"%(agent)s\" said, \"Your ticket is now <strong>resolved</strong>.\""
msgstr "\"%(agent)s\" dijo: \"Su ticket ahora está <strong>resuelto</strong>.\""
The string changed language. The structure stayed intact.
Add a reviewable translation step
The useful automation step is one command in your existing flow, between extraction and compile. TranslateBot fits there as a manage.py command, so the sequence stays in Django instead of bouncing through a portal.
python manage.py makemessages --all
python manage.py translate --locale fr
python manage.py compilemessages
That matters for quote-heavy strings because the tool can preserve placeholders, HTML, and syntax while writing directly back to locale/<lang>/LC_MESSAGES/django.po.
A short product walkthrough helps if you want to see the flow in action:
Keep the source boring
Automation works best when the source strings are disciplined.
- Prefer named placeholders like
%(name)sover positional%s - Use
pgettextfor ambiguous quoted labels - Keep tags stable instead of embedding layout logic in translatable text
- Split overloaded strings when one message is trying to be copy, markup, and audit log at once
If a source string is messy, the translated result will still be messy. You'll just get there faster.
How to Review and Validate Translated Quotes
Automation buys consistency. It doesn't remove review.
The good news is that quoted strings are much easier to review when the changes land as normal Git diffs instead of side effects from a portal export. Scholarly guidance on quote translation keeps circling the same decision points, quote directly, paraphrase, or annotate, and in software the key requirement is consistency across the app. Version-controlled changes are how teams enforce that policy in practice, as discussed in this case study on editing non-English quotations and translations.

Review the diff first
Use --word-diff when the risky part is punctuation:
git diff --word-diff locale/fr/LC_MESSAGES/django.po
Example:
msgid "\"%(name)s\" said, \"Save changes\""
-msgstr ""
+msgstr "\"%(name)s\" a dit : « Enregistrer les modifications »"
That view makes it easy to spot:
- Placeholder drift
- Dropped escapes
- Unexpected quote style
- HTML moved outside its original boundary
If you want test ideas for translation review in CI, this collection of translation test examples is a solid place to start.
Reviewers should spend most of their time on
msgstr, placeholders, and markup boundaries. Grammar tweaks are secondary if the string can't compile.
Compile locally before you push
compilemessages isn't just a packaging step. It's the fastest validation gate you have.
python manage.py compilemessages
If it passes, your PO syntax is intact. That doesn't guarantee the translated quote is good, but it removes an entire class of breakage before code review even starts.
You should also add UI checks for the pages that are quote-heavy:
| Check | Why it matters |
|---|---|
| Render forms and modals | Quotes often appear in confirmation copy |
| Test RTL locales | Punctuation order can look wrong even when syntax is valid |
| Inspect HTML output | Inline emphasis and links are easy to damage |
Teams that treat this as part of release QA tend to improve software reliability over time because translation defects stop bypassing the same guardrails used for other user-facing changes. If you're tightening that side of engineering practice, this guide on how testing workflows improve software reliability is a useful companion read.
Check what the parser can't judge
A compiled message file can still be wrong on screen.
Look for these in rendered pages:
- RTL punctuation: quotation marks can look visually off around Latin brand names
- Mixed smart and straight quotes: usually a sign that text came from different tools
- Broken emphasis:
<strong>and<a>may still compile but render in the wrong place
One pass in the browser catches a lot. Two passes, one by the developer who touched the source string and one by a reviewer who knows the locale, is even better.
Your Pre-Deploy Checklist for Translations
Treat translated quotes like code that happens to be language-shaped.
Run the same sequence every time a release touches localized content:
Extract messages
python manage.py makemessages --allTranslate new or changed strings
python manage.py translate --locale frCompile for syntax validation
python manage.py compilemessagesReview the PO diff in the PR
git diff --word-diff locale/fr/LC_MESSAGES/django.poSpot-check rendered UI
Open the pages with quoted strings, especially forms, alerts, modals, and RTL locales.
For CI, keep the rule boring. Fail the build if messages don't compile. Review diffs like any other code change. Avoid hand-editing translated quote strings unless you're fixing a specific issue with a clear policy behind it.
That process removes a whole class of release-day bugs. More importantly, it makes the translation of quotes predictable instead of fragile.
If you want that middle step to stay inside Django instead of a translation portal, TranslateBot gives you a manage.py workflow for translating .po files while preserving placeholders and HTML, so quote-heavy strings remain reviewable in Git and safe to compile before deploy.