Django Internationalization: Avoiding Ambiguous Translations with Contextual Markers
When you localize a non-English product into English (or any other language), you’ll often hit a frustrating issue with ambiguous source strings:
- A button labeled “banco” is translated as “bank” even when the UI clearly means “bench.”
- In a restaurant UI, “carta” is translated as “letter” even though it should be “menu.”
- The label “cura” is translated as “cure” in a context where it actually means “priest.”
The root cause is simple:
Computers don’t understand context. If two strings are identical, they’re treated as the same.
This article shows how to keep your source code unchanged while attaching different translations to the same string using Django’s Contextual Markers.
Why Does This Happen? The Basics of gettext
Django’s translation system is built on GNU gettext. It follows a very simple rule set:
- Original string →
msgid - Translated string →
msgstr - If
msgidis the same, the samemsgstris used
# django.po
msgid "banco"
msgstr "bank"
Once a mapping exists in the .po file, every occurrence of “Polish” will use the same translation, regardless of its intended meaning.
This is why the two UI elements above cannot be distinguished.
To avoid this, developers sometimes change the source code:
<!-- Avoid this approach -->
{% trans "Polish (verb)" %}
{% trans "Polish (language)" %}
While the translations may work, the visible strings become awkward or hard to reuse. What we really want is:
- Keep the source string as "Polish"
- Provide a separate explanation of which meaning is intended
That’s where Contextual Markers come in.
Solving It in Templates: {% translate %} + context
In Django templates, you can attach a context option to the {% translate %} (or legacy {% trans %}) tag to differentiate the same string by context.
1) Original Code (conflict)
{% load i18n %}
<button>{% translate "Polish" %}</button>
<span>{% translate "Polish" %}</span>
In the .po file, both entries have msgid "Polish", so only one translation is possible.
2) Improved Code (using context)
{% load i18n %}
<button>
{% translate "Polish" context "verb: to refine UI" %}
</button>
<span>
{% translate "Polish" context "language name" %}
</span>
Key points:
- The string after
contextis not shown to users. - It’s purely metadata for the translation system and translators.
- Keep it short and descriptive: e.g., "verb: to refine UI", "language name", "menu label", "button text".
3) Using {% blocktranslate %}
For longer sentences, you can still use context with {% blocktranslate %} (or {% blocktrans %}):
{% load i18n %}
{% blocktranslate context "greeting message" with username=user.username %}
Hello {{ username }}
{% endblocktranslate %}
This allows the same sentence style to be reused in different contexts.
Solving It in Python Code: pgettext
In Python code (views, models, forms), use the pgettext family instead of plain gettext.
pgettext(context, message)pgettext_lazy(context, message)– lazy evaluation (e.g., model fields)npgettext(context, singular, plural, number)– handles plural forms with context
1) Basic Example
from django.utils.translation import pgettext
def my_view(request):
# 1. Month name "May"
month = pgettext("month name", "May")
# 2. Person name "May"
person = pgettext("person name", "May")
# 3. Auxiliary verb "may" (~might)
verb = pgettext("auxiliary verb", "may")
All three use the same msgid but different contexts, yielding distinct translations.
2) Using pgettext_lazy in Models
from django.db import models
from django.utils.translation import pgettext_lazy
class Order(models.Model):
type = models.CharField(
verbose_name=pgettext_lazy("order model field", "Order type"),
max_length=20,
)
STATUS_CHOICES = [
("open", pgettext_lazy("order status", "Open")),
("opened", pgettext_lazy("log action", "Open")),
]
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
Even with the same word "Open", you can distinguish status vs. action.
3) Handling Plurals: npgettext
from django.utils.translation import npgettext
def get_notification(count):
return npgettext(
"user notification", # context
"You have %(count)d message", # singular
"You have %(count)d messages", # plural
count
) % {"count": count}
What the .po File Looks Like
After extracting messages with python manage.py makemessages -l ko, the .po file includes a msgctxt field:
# django.po
# 1) "Polish" = UI refinement (verb)
msgctxt "verb: to refine UI"
msgid "Polish"
msgstr "다듬기"
# 2) "Polish" = language name
msgctxt "language name"
msgid "Polish"
msgstr "폴란드어"
Because msgctxt differs, translation tools treat them as separate entries, making it easier for translators to understand the intended meaning.
Key takeaways:
- The source string remains unchanged.
- The
.pofile becomes smarter withmsgctxt.
Tips for Writing Good Context
- Describe the role (e.g., "button label", "menu item", "tooltip", "error message", "form field label").
- Add a concept if the same word appears in multiple domains (e.g., "File" as a menu item vs. an uploaded file object).
- Never put the translated string in the context; keep it in English.
When to Use Contextual Markers
Consider using them when:
- Short strings (1–2 words) appear in multiple places.
- The same word is reused with different meanings in the UI.
- Translators work without seeing the live UI (e.g., via a translation platform).
Summary
- Django maps a single
msgidto one translation by default. - Homonyms like "Polish", "May", or "Book" often cause confusion.
- Don’t alter the source string; instead, add context:
- Templates:
{% translate "…" context "…" %} - Python:
pgettext,pgettext_lazy,npgettext - The
.pofile will containmsgctxt, allowing distinct translations for the same word. - This keeps code readable while improving translation quality and maintainability.
In an era where multilingual support is increasingly essential, mastering Contextual Markers is a must‑know i18n tool for Django developers.
Related Articles