Volver al blog

Por qué fallan las traducciones en Django (y cómo solucionar las 10 causas más comunes)

2026-02-04 11 min de lectura
Por qué fallan las traducciones en Django (y cómo solucionar las 10 causas más comunes)

Marcaste cadenas para traduccion, generaste archivos .po, ejecutaste compilemessages y tu aplicacion sigue mostrando ingles. No eres el unico. El framework i18n de Django es potente, pero tiene aristas afiladas que atrapan incluso a desarrolladores experimentados.

Esta guia cubre las 10 razones mas comunes por las que las traducciones de Django fallan silenciosamente, con sintomas exactos y soluciones para cada una.

1. Olvidar ejecutar compilemessages despues de editar archivos .po

Editaste un archivo .po (manualmente o con una herramienta), pero el texto traducido nunca aparece. La aplicacion sigue mostrando las cadenas originales en ingles.

Django no lee archivos .po en tiempo de ejecucion. Lee los archivos binarios compilados .mo (machine object). Si editas un archivo .po sin recompilar, Django no tiene idea de que algo cambio.

Ejecuta compilemessages despues de cada cambio en un archivo .po:

python manage.py compilemessages

Si automatizas tus traducciones con TranslateBot, agrega compilemessages como el paso final en tu flujo de trabajo:

python manage.py makemessages -a --no-obsolete
python manage.py translate
python manage.py compilemessages

2. Falta {% load i18n %} en las plantillas

Usas {% trans "Hello" %} en una plantilla, pero Django lanza un TemplateSyntaxError. O peor, la etiqueta no hace nada silenciosamente si tienes un motor de plantillas mal configurado.

Las etiquetas {% trans %} y {% blocktrans %} viven en la biblioteca de etiquetas de plantilla i18n de Django. Sin cargarla, el motor de plantillas no las reconoce.

Agrega {% load i18n %} al inicio de cada plantilla que use etiquetas de traduccion:

{% load i18n %}

<h1>{% trans "Welcome to our site" %}</h1>
<p>{% blocktrans with name=user.name %}Hello, {{ name }}!{% endblocktrans %}</p>

Este es un requisito por plantilla. Incluso si una plantilla padre carga i18n, las plantillas hijas que usan etiquetas de traduccion necesitan su propia declaracion {% load i18n %}.

3. LocaleMiddleware no esta en MIDDLEWARE o esta en la posicion incorrecta

Django siempre sirve contenido en el idioma predeterminado independientemente del encabezado Accept-Language del navegador, el prefijo de URL o la configuracion de sesion.

LocaleMiddleware determina el idioma activo para cada solicitud. Sin el, Django usa LANGUAGE_CODE por defecto e ignora todos los mecanismos de seleccion de idioma. Su posicion en la pila de middleware tambien importa, porque necesita acceso a los datos de sesion y la resolucion de URL.

Agrega LocaleMiddleware a tu configuracion MIDDLEWARE, despues de SessionMiddleware y CommonMiddleware:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.locale.LocaleMiddleware",  # Must be after SessionMiddleware
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
]

Tambien asegurate de que django.conf.urls.i18n este incluido en tu configuracion de URL si usas cambio de idioma basado en URL:

from django.conf.urls.i18n import i18n_patterns

urlpatterns = i18n_patterns(
    path("", include("myapp.urls")),
)

4. Codigos de idioma no coinciden (ej. pt-br vs pt_BR)

Las traducciones existen en tus archivos .po, compilemessages tiene exito, pero Django ignora la traduccion para ciertos locales.

Django espera que los directorios de locale sigan el formato <language>_<COUNTRY> con un separador de guion bajo y codigo de pais en mayusculas. Por ejemplo, pt_BR para portugues brasileno. Si tu directorio se llama pt-br, pt-BR o ptBR, Django no lo encontrara. Lo mismo aplica para la configuracion LANGUAGES: los codigos ahi usan guiones (pt-br), pero el sistema de archivos usa guiones bajos (pt_BR).

Asegurate de que tu estructura de directorios coincida con las expectativas de Django:

locale/
    pt_BR/
        LC_MESSAGES/
            django.po
            django.mo

Y en tu configuracion, usa la forma con guiones:

LANGUAGES = [
    ("en", "English"),
    ("pt-br", "Brazilian Portuguese"),
    ("zh-hans", "Simplified Chinese"),
]

Al ejecutar makemessages, usa la forma con guion bajo para el flag de locale:

python manage.py makemessages -l pt_BR

5. Entradas fuzzy omitidas silenciosamente durante la compilacion

Una traduccion existe en el archivo .po, pero Django muestra la cadena original en ingles en tiempo de ejecucion para esa entrada especifica. Este es particularmente frustrante porque la traduccion esta ahi mismo en el archivo.

Cuando el makemessages de Django detecta que una cadena fuente cambio ligeramente, marca la traduccion existente como "fuzzy" (lo que significa que es una suposicion que necesita revision humana). El comando compilemessages omite todas las entradas fuzzy, tratandolas como no traducidas. Asi que la entrada parece traducida en el archivo .po, pero el archivo .mo la excluye por completo.

Una entrada fuzzy se ve asi:

#, fuzzy
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"

Revisa la traduccion, actualiza msgstr si es necesario, luego elimina el flag #, fuzzy:

msgid "Welcome to our website!"
msgstr "Welkom op onze website!"

Luego recompila:

python manage.py compilemessages

En un proyecto mas grande, las entradas fuzzy se acumulan y son faciles de pasar por alto. El comando check_translations de TranslateBot las detecta automaticamente:

python manage.py check_translations
locale/nl/LC_MESSAGES/django.po: 0 untranslated, 3 fuzzy
CommandError: Translation check failed

Agregalo a tu pipeline de CI con check_translations --makemessages y nunca mas enviaras una entrada fuzzy.

6. LOCALE_PATHS no configurado o apuntando al directorio incorrecto

makemessages crea archivos .po en una ubicacion, pero Django los busca en otro lugar. Las traducciones existen en disco pero nunca se cargan.

Django busca archivos de traduccion en un orden especifico: primero los directorios de LOCALE_PATHS, luego el directorio locale/ de cada aplicacion, y finalmente el directorio locale/ del proyecto. Si LOCALE_PATHS no esta configurado o apunta a la ruta incorrecta, Django podria nunca encontrar tus archivos .po.

Configura LOCALE_PATHS en tus ajustes con una ruta absoluta:

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

LOCALE_PATHS = [
    BASE_DIR / "locale",
]

Verifica que el directorio existe y contiene la estructura esperada:

locale/
    de/
        LC_MESSAGES/
            django.po
            django.mo
    nl/
        LC_MESSAGES/
            django.po
            django.mo

Un error comun es configurar LOCALE_PATHS como locale/ (relativo) en lugar de una ruta absoluta. Django no resuelve rutas relativas desde la raiz de tu proyecto. Depende del directorio de trabajo del proceso, que a menudo no es lo que esperas.

7. Cache sirviendo traducciones obsoletas

Actualizaste y compilaste traducciones, pero el texto antiguo sigue apareciendo. Reiniciar el servidor lo soluciona.

El catalogo de traducciones de Django se carga una vez por proceso. En produccion, servidores WSGI/ASGI como Gunicorn o Uvicorn mantienen los procesos worker vivos por periodos extendidos. El archivo .mo puede haber cambiado en disco, pero el proceso en ejecucion todavia tiene las traducciones antiguas en memoria. Ademas, si usas el framework de cache de Django o un proxy inverso como Nginx o Cloudflare, las respuestas cacheadas serviran contenido antiguo hasta que expiren.

Reinicia tu servidor de aplicacion despues de desplegar nuevas traducciones:

# Gunicorn
kill -HUP $(cat /tmp/gunicorn.pid)

# Systemd
sudo systemctl restart myapp

# Docker
docker compose restart web

Para el framework de cache de Django, limpia la cache despues de actualizar traducciones:

from django.core.cache import cache
cache.clear()

En desarrollo, runserver recarga automaticamente cuando cambian archivos Python pero no vigila archivos .mo. Necesitaras reiniciarlo manualmente despues de ejecutar compilemessages.

8. Cadenas no envueltas en gettext (_() o {% trans %})

makemessages no extrae ciertas cadenas, por lo que nunca aparecen en tus archivos .po y nunca se traducen. Este es el problema mas basico y tambien el mas facil de pasar por alto en un codebase grande.

El comando makemessages de Django usa xgettext para escanear tu codigo fuente en busca de marcadores de traduccion. Si una cadena no esta envuelta en gettext() (comunmente con alias _()), gettext_lazy(), {% trans %} o {% blocktrans %}, es invisible para el proceso de extraccion.

Envuelve cada cadena orientada al usuario:

# Python code
from django.utils.translation import gettext_lazy as _

class Article(models.Model):
    class Meta:
        verbose_name = _("article")
        verbose_name_plural = _("articles")

# Views
from django.utils.translation import gettext as _

def my_view(request):
    message = _("Your changes have been saved.")
    return HttpResponse(message)
<!-- Templates -->
{% load i18n %}
<h1>{% trans "Welcome" %}</h1>
<p>{% blocktrans with count=items|length %}You have {{ count }} items.{% endblocktrans %}</p>

Usa el flag --dry-run de TranslateBot para previsualizar que cadenas estan actualmente sin traducir, para que puedas detectar cadenas que se pasaron por alto durante la extraccion:

python manage.py translate --target-lang de --dry-run

Esto muestra todas las entradas sin traducir en tus archivos .po sin hacer ninguna llamada API ni cambios.

9. Los f-strings no se pueden traducir (limitacion de Django)

Envuelves un f-string en _() y obtienes un error de sintaxis, o makemessages extrae una cadena rota/parcial que no se puede traducir.

Los f-strings de Python se evaluan en tiempo de ejecucion. La herramienta de extraccion xgettext analiza el codigo fuente estaticamente, por lo que no puede evaluar expresiones Python dentro de llaves {}. Esto significa que _(f"Hello, {name}") se extrae como una cadena que contiene una expresion literal {name} (o falla completamente en la extraccion), y la entrada .po resultante nunca coincidira con la cadena en tiempo de ejecucion.

Usa el formateo % de Django o .format() con marcadores de posicion nombrados en su lugar:

# Wrong -- f-string cannot be extracted
message = _(f"Hello, {user.name}! You have {count} new messages.")

# Correct -- named placeholders
message = _("Hello, %(name)s! You have %(count)d new messages.") % {
    "name": user.name,
    "count": count,
}

# Also correct -- .format() with positional args
message = _("Hello, {0}! You have {1} new messages.").format(user.name, count)

Esto no es una limitacion de TranslateBot ni de las herramientas. Es fundamental a como funciona gettext. La cadena fuente debe ser un literal estatico para que pueda ser extraida y buscada en tiempo de ejecucion.

TranslateBot preserva todos estos formatos de marcadores de posicion (%(name)s, {0}, %s, etiquetas HTML) durante la traduccion, por lo que las cadenas traducidas permanecen completamente funcionales.

10. Errores de formato de cadena de marcadores de posicion rompiendo la compilacion de .po

compilemessages falla con un error, o el archivo .po tiene una discrepancia en el flag #, python-format, y la entrada se descarta silenciosamente.

Cuando una cadena fuente contiene marcadores de posicion de formato Python como %(name)s, Django marca la entrada .po con #, python-format. Si la traduccion tiene marcadores de posicion diferentes (un error tipografico como %(nome)s, un marcador de posicion faltante o uno extra), las herramientas gettext pueden rechazar la entrada o compilemessages puede fallar. Esto ocurre comunmente con traducciones manuales o con herramientas de traduccion con IA que no entienden la semantica de los marcadores de posicion.

Una entrada rota se ve asi:

#, python-format
msgid "Hello, %(name)s! You have %(count)d new messages."
msgstr "Hallo, %(naam)s! Je hebt %(count)d nieuwe berichten."

Aqui %(naam)s deberia ser %(name)s. Los marcadores de posicion deben coincidir exactamente con la fuente.

Asegurate de que las cadenas traducidas contengan exactamente los mismos marcadores de posicion que la fuente. Verifica errores tipograficos, marcadores de posicion faltantes y marcadores de posicion extra.

Esta es un area donde TranslateBot proporciona una ventaja real. Su logica de preservacion de marcadores de posicion asegura que todas las cadenas de formato (%(name)s, {0}, %s) en la salida traducida coincidan exactamente con la fuente. El manejo de marcadores de posicion esta cubierto por 100% de cobertura de pruebas, por lo que los errores de cadenas de formato de traduccion se eliminan a nivel de herramienta en lugar de detectarse en tiempo de compilacion.

Si traduces manualmente o con una herramienta que no maneja marcadores de posicion, valida tus archivos .po con:

msgfmt --check-format locale/de/LC_MESSAGES/django.po

Esto ejecuta la validacion de cadenas de formato de gettext e informa cualquier discrepancia.

Uniendo todo: un flujo de trabajo defensivo

La mayoria de estos problemas comparten una causa raiz: pasos manuales que son faciles de olvidar. Aqui hay un flujo de trabajo que previene los 10 problemas:

# 1. Extract strings (catches #8 -- any new gettext-wrapped strings)
python manage.py makemessages -a --no-obsolete

# 2. Translate (catches #1, #5, #8, #9, #10 -- handles untranslated,
#    fuzzy, and placeholder issues automatically)
python manage.py translate

# 3. Compile (catches #1 -- generates .mo files)
python manage.py compilemessages

# 4. Verify in CI (catches everything that slipped through)
python manage.py check_translations --makemessages

Agrega el paso 4 a tu pipeline de CI y las cadenas sin traducir, las entradas fuzzy y los errores de formato haran fallar la build antes de que lleguen a produccion.

Tabla de referencia rapida

Causa Sintoma Solucion en una linea
Sin compilemessages Las traducciones existen pero no aparecen python manage.py compilemessages
Falta {% load i18n %} TemplateSyntaxError en {% trans %} Agregar {% load i18n %} a la plantilla
LocaleMiddleware faltante El idioma siempre es ingles por defecto Agregar django.middleware.locale.LocaleMiddleware a MIDDLEWARE
Codigo de idioma no coincide Directorio de locale no encontrado Usar pt_BR (guion bajo) para directorios, pt-br (guion) para configuracion
Entradas fuzzy omitidas Traduccion en .po pero no en la app Eliminar el flag #, fuzzy despues de revisar
LOCALE_PATHS incorrecto Los archivos .po existen pero Django los ignora Configurar LOCALE_PATHS con una ruta absoluta
Traducciones en cache Texto antiguo aparece despues de actualizar Reiniciar el servidor de aplicacion
Cadena no en gettext Cadena faltante en archivos .po Envolver en _() o {% trans %}
f-string en gettext Extraccion rota o discrepancia en tiempo de ejecucion Reemplazar con marcadores % o .format()
Marcadores de posicion no coinciden compilemessages falla o entrada descartada Hacer coincidir marcadores de posicion exactamente entre fuente y traduccion

La mayoria de estos problemas desaparecen cuando automatizas el paso de traduccion y aplicas verificaciones en CI. El comando translate de TranslateBot maneja la preservacion de marcadores de posicion y la traduccion incremental, mientras que check_translations detecta todo lo que se escapa (entradas sin traducir, flags fuzzy y problemas de cadenas de formato) antes de que lleguen a produccion.

Deja de editar archivos .po manualmente

TranslateBot automatiza las traducciones de Django con IA. Un comando, todos tus idiomas, centavos por traducción.