Internacionalización en Django: evitar colisiones de significado con marcadores contextuales

Cuando se implementa bien el soporte multilingüe (i18n), es habitual encontrarse con situaciones como estas:

  • Un botón con “Polish” (en el sentido de “pulir” o “refinar”) acaba traducido como “polaco”.
  • En un selector de fechas, “May” puede traducirse como un nombre propio en algunos idiomas.
  • El ítem de menú “Book” se traduce solo como “libro” y no como “reservar”.

La causa es sencilla:

Las computadoras no entienden el contexto. Si dos cadenas son idénticas, se tratan como la misma.

En este artículo veremos cómo usar los marcadores contextuales de Django para mantener el texto fuente intacto y, aun así, asignar traducciones distintas a una misma cadena.


¿Por qué ocurre esto? Comportamiento básico de gettext



El sistema de traducción de Django se apoya en GNU gettext, que funciona con reglas muy simples:

  • Cadena fuente → msgid
  • Traducción → msgstr
  • Si msgid es el mismo, se reutiliza el mismo msgstr
# django.po

msgid "Polish"
msgstr "polaco"

Una vez creada esa asociación en el .po, cualquier aparición de "Polish" usará la misma traducción, aunque el sentido sea distinto.

Por eso no se distinguen estos dos usos:

  • "Polish" = verbo (pulir / refinar)
  • "Polish" = nombre de idioma o gentilicio

Para evitarlo, a veces se modifica el texto fuente:

<!-- Evita este enfoque -->
{% trans "Polish (verb)" %}
{% trans "Polish (language)" %}

Aunque “funcione”, el texto visible se vuelve artificial y se pierde reutilización. Lo que queremos es:

  • Mantener la cadena fuente "Polish"
  • Añadir una pista externa sobre el significado en ese punto

Ahí entran los marcadores contextuales.


Solución en plantillas: {% translate %} + context

En plantillas de Django, el tag {% translate %} (o el antiguo {% trans %}) admite la opción context para diferenciar cadenas idénticas según su uso.

1) Código original (colisión)

{% load i18n %}

<button>{% translate "Polish" %}</button>
<span>{% translate "Polish" %}</span>

En el .po ambas entradas quedan como msgid "Polish", así que solo puede existir una traducción.

2) Código mejorado (con contexto)

{% load i18n %}

<button>
  {% translate "Polish" context "verb: to refine UI" %}
</button>

<span>
  {% translate "Polish" context "language name" %}
</span>

Puntos clave:

  • El texto de context no se muestra al usuario.
  • Es metainformación para el sistema y para quienes traducen.
  • Conviene que sea breve y específico (por ejemplo: "button text", "menu item", "language name").

3) También con {% blocktranslate %}

En frases más largas, context también funciona con {% blocktranslate %}:

{% load i18n %}

{% blocktranslate context "greeting message" with username=user.username %}
  Hello {{ username }}
{% endblocktranslate %}

Así puedes reutilizar estructuras de frase sin perder el significado por falta de contexto.


Solución en código Python: pgettext



En código Python (vistas, modelos, formularios), en lugar de gettext se usa la familia pgettext:

  • pgettext(context, message)
  • pgettext_lazy(context, message) – evaluación diferida (por ejemplo, en modelos)
  • npgettext(context, singular, plural, number) – plural + contexto

1) Ejemplo básico

from django.utils.translation import pgettext

def my_view(request):
    month = pgettext("month name", "May")
    person = pgettext("person name", "May")
    verb = pgettext("auxiliary verb", "may")

Aunque el msgid sea el mismo, el context permite traducciones distintas.

2) pgettext_lazy en modelos

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)

Con esto, una misma cadena puede traducirse de forma distinta según sea “estado” o “acción”.

3) Plurales con npgettext

from django.utils.translation import npgettext

def get_notification(count):
    return npgettext(
        "user notification",
        "You have %(count)d message",
        "You have %(count)d messages",
        count
    ) % {"count": count}

¿Cómo se ve en el archivo .po?

Tras extraer mensajes, el .po incluirá msgctxt para las cadenas con contexto:

python manage.py makemessages -l <locale>
# django.po

msgctxt "verb: to refine UI"
msgid "Polish"
msgstr "pulir"

msgctxt "language name"
msgid "Polish"
msgstr "polaco"

Aunque msgid sea idéntico, msgctxt separa las entradas y las herramientas de traducción las gestionan por separado.


Consejos para escribir buenos contextos

El contexto no se ve en pantalla, pero suele ser la pista más útil para traducir bien.

  • Describe el rol: "button label", "menu item", "tooltip", "error message", "form field label".
  • Aclara el concepto cuando haga falta: "File""file menu item" / "uploaded file object".
  • No metas la traducción en el contexto: debe ser una descripción breve en el idioma fuente (normalmente inglés).

Cuándo usar marcadores contextuales

Suele ser buena idea usar context/pgettext cuando:

  1. La cadena es corta (1–2 palabras).
  2. Se reutiliza con significados distintos en la UI.
  3. Quien traduce no ve la interfaz (plataformas de traducción o entrega de .po).

Resumen

Desenfoque de un desarrollador con homónimos

  • Django asocia por defecto una única traducción a cada msgid.
  • Palabras ambiguas como "Polish", "May" o "Book" generan colisiones.
  • En vez de cambiar el texto fuente, añade contexto:

  • Templates: {% translate "…" context "…" %}

  • Python: pgettext, pgettext_lazy, npgettext
  • El .po incorpora msgctxt, lo que permite traducciones distintas y mejora la mantenibilidad.

En proyectos multilingües, los marcadores contextuales son una herramienta clave para mantener el código limpio y las traducciones coherentes.


Artículos relacionados