Hai contrassegnato le stringhe per la traduzione, generato i file .po, eseguito compilemessages, e la tua app mostra ancora l'inglese. Non sei il solo. Il framework i18n di Django è potente, ma ha spigoli vivi che colgono anche gli sviluppatori esperti.
Questa guida copre le 10 ragioni più comuni per cui le traduzioni di Django falliscono silenziosamente, con sintomi esatti e correzioni per ciascuna.
1. Dimenticare di eseguire compilemessages dopo la modifica dei file .po
Hai modificato un file .po (manualmente o con uno strumento), ma il testo tradotto non appare mai. L'app continua a mostrare le stringhe originali in inglese.
Django non legge i file .po a runtime. Legge invece i file binari .mo (machine object) compilati. Se modifichi un file .po senza ricompilare, Django non sa che qualcosa è cambiato.
Esegui compilemessages dopo ogni modifica ai file .po:
python manage.py compilemessages
Se automatizzi le traduzioni con TranslateBot, aggiungi compilemessages come ultimo passaggio nel tuo workflow:
python manage.py makemessages -a --no-obsolete
python manage.py translate
python manage.py compilemessages
2. Manca {% load i18n %} nei template
Usi {% trans "Hello" %} in un template, ma Django solleva un TemplateSyntaxError. O peggio, il tag non fa silenziosamente nulla se hai un motore di template mal configurato.
I tag {% trans %} e {% blocktrans %} si trovano nella libreria di tag template i18n di Django. Senza caricarla, il motore di template non li riconosce.
Aggiungi {% load i18n %} all'inizio di ogni template che usa tag di traduzione:
{% load i18n %}
<h1>{% trans "Welcome to our site" %}</h1>
<p>{% blocktrans with name=user.name %}Hello, {{ name }}!{% endblocktrans %}</p>
Questo è un requisito per ogni template. Anche se un template padre carica i18n, i template figli che usano tag di traduzione hanno bisogno della propria dichiarazione {% load i18n %}.
3. LocaleMiddleware non presente in MIDDLEWARE o nella posizione sbagliata
Django serve sempre i contenuti nella lingua predefinita indipendentemente dall'header Accept-Language del browser, dal prefisso URL o dalle impostazioni di sessione.
LocaleMiddleware determina la lingua attiva per ogni richiesta. Senza di esso, Django usa LANGUAGE_CODE come predefinito e ignora tutti i meccanismi di selezione della lingua. Anche la sua posizione nello stack dei middleware è importante, perché necessita di accesso ai dati di sessione e alla risoluzione URL.
Aggiungi LocaleMiddleware alla tua impostazione MIDDLEWARE, dopo SessionMiddleware e 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",
]
Assicurati anche che django.conf.urls.i18n sia incluso nella configurazione URL se usi il cambio lingua basato su URL:
from django.conf.urls.i18n import i18n_patterns
urlpatterns = i18n_patterns(
path("", include("myapp.urls")),
)
4. Mismatch dei codici lingua (es. pt-br vs pt_BR)
Le traduzioni esistono nei tuoi file .po, compilemessages ha successo, ma Django ignora la traduzione per certe lingue.
Django si aspetta che le directory delle lingue seguano il formato <language>_<COUNTRY> con un separatore underscore e codice paese maiuscolo. Ad esempio, pt_BR per il portoghese brasiliano. Se la tua directory si chiama pt-br, pt-BR o ptBR, Django non la troverà. Lo stesso vale per l'impostazione LANGUAGES: i codici lì usano i trattini (pt-br), ma il filesystem usa gli underscore (pt_BR).
Assicurati che la struttura delle directory corrisponda alle aspettative di Django:
locale/
pt_BR/
LC_MESSAGES/
django.po
django.mo
E nelle impostazioni, usa la forma con trattino:
LANGUAGES = [
("en", "English"),
("pt-br", "Brazilian Portuguese"),
("zh-hans", "Simplified Chinese"),
]
Quando esegui makemessages, usa la forma con underscore per il flag della lingua:
python manage.py makemessages -l pt_BR
5. Voci fuzzy saltate silenziosamente durante la compilazione
Una traduzione esiste nel file .po, ma Django mostra la stringa originale in inglese a runtime per quella specifica voce. Questo è particolarmente frustrante perché la traduzione è proprio lì nel file.
Quando il comando makemessages di Django rileva che una stringa sorgente è cambiata leggermente, contrassegna la traduzione esistente come "fuzzy" (ovvero una supposizione che necessita di revisione umana). Il comando compilemessages salta tutte le voci fuzzy, trattandole come non tradotte. Quindi la voce appare tradotta nel file .po, ma il file .mo la esclude completamente.
Una voce fuzzy appare così:
#, fuzzy
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"
Rivedi la traduzione, aggiorna msgstr se necessario, poi rimuovi il flag #, fuzzy:
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"
Poi ricompila:
python manage.py compilemessages
In un progetto più grande, le voci fuzzy si accumulano e sono facili da perdere. Il comando check_translations di TranslateBot le intercetta automaticamente:
python manage.py check_translations
locale/nl/LC_MESSAGES/django.po: 0 untranslated, 3 fuzzy
CommandError: Translation check failed
Aggiungi check_translations --makemessages alla tua pipeline CI e non spedirai mai più una voce fuzzy.
6. LOCALE_PATHS non configurato o che punta alla directory sbagliata
makemessages crea i file .po in una posizione, ma Django li cerca altrove. Le traduzioni esistono su disco ma non vengono mai caricate.
Django cerca i file di traduzione in un ordine specifico: prima le directory LOCALE_PATHS, poi la directory locale/ di ogni app, e infine la directory locale/ del progetto. Se LOCALE_PATHS non è impostato o punta al percorso sbagliato, Django potrebbe non trovare mai i tuoi file .po.
Imposta LOCALE_PATHS nelle tue impostazioni con un percorso assoluto:
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
LOCALE_PATHS = [
BASE_DIR / "locale",
]
Verifica che la directory esista e contenga la struttura prevista:
locale/
de/
LC_MESSAGES/
django.po
django.mo
nl/
LC_MESSAGES/
django.po
django.mo
Un errore comune è impostare LOCALE_PATHS su locale/ (relativo) invece di un percorso assoluto. Django non risolve i percorsi relativi dalla root del progetto. Dipende dalla directory di lavoro del processo, che spesso non è quella che ti aspetti.
7. La cache serve traduzioni obsolete
Hai aggiornato e compilato le traduzioni, ma il vecchio testo continua ad apparire. Riavviare il server risolve il problema.
Il catalogo delle traduzioni di Django viene caricato una volta per processo. In produzione, i server WSGI/ASGI come Gunicorn o Uvicorn mantengono i processi worker attivi per periodi prolungati. Il file .mo potrebbe essere cambiato su disco, ma il processo in esecuzione ha ancora le vecchie traduzioni in memoria. Inoltre, se usi il framework cache di Django o un reverse proxy come Nginx o Cloudflare, le risposte in cache serviranno contenuti vecchi fino alla loro scadenza.
Riavvia il server applicativo dopo aver distribuito le nuove traduzioni:
# Gunicorn
kill -HUP $(cat /tmp/gunicorn.pid)
# Systemd
sudo systemctl restart myapp
# Docker
docker compose restart web
Per il framework cache di Django, svuota la cache dopo aver aggiornato le traduzioni:
from django.core.cache import cache
cache.clear()
In sviluppo, runserver si ricarica automaticamente quando cambiano i file Python ma non monitora i file .mo. Dovrai riavviarlo manualmente dopo aver eseguito compilemessages.
8. Stringhe non racchiuse in gettext (_() o {% trans %})
makemessages non estrae certe stringhe, quindi non appaiono mai nei tuoi file .po e non vengono mai tradotte. Questo è il problema più basilare e anche il più facile da trascurare in un codebase grande.
Il comando makemessages di Django usa xgettext per scansionare il codice sorgente alla ricerca di marcatori di traduzione. Se una stringa non è racchiusa in gettext() (comunemente alias _()), gettext_lazy(), {% trans %} o {% blocktrans %}, è invisibile al processo di estrazione.
Racchiudi ogni stringa rivolta all'utente:
# 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 il flag --dry-run di TranslateBot per visualizzare in anteprima quali stringhe sono attualmente non tradotte, così puoi intercettare le stringhe mancate durante l'estrazione:
python manage.py translate --target-lang de --dry-run
Questo mostra tutte le voci non tradotte nei tuoi file .po senza effettuare chiamate API o modifiche.
9. Le f-string non possono essere tradotte (Limitazione di Django)
Racchiudi una f-string in _() e ottieni un errore di sintassi, oppure makemessages estrae una stringa rotta/parziale che non può essere tradotta.
Le f-string di Python vengono valutate a runtime. Lo strumento di estrazione xgettext analizza il codice sorgente staticamente, quindi non può valutare le espressioni Python all'interno delle parentesi {}. Questo significa che _(f"Hello, {name}") viene estratto come una stringa contenente un'espressione letterale {name} (o non riesce a essere estratto del tutto), e la voce .po risultante non corrisponderà mai alla stringa a runtime.
Usa la formattazione % di Django o .format() con segnaposto nominali:
# 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)
Questa non è una limitazione di TranslateBot o degli strumenti. È fondamentale per il funzionamento di gettext. La stringa sorgente deve essere un letterale statico per poter essere estratta e cercata a runtime.
TranslateBot preserva tutti questi formati di segnaposto (%(name)s, {0}, %s, tag HTML) durante la traduzione, così le stringhe tradotte rimangono pienamente funzionali.
10. Errori nei segnaposto delle stringhe di formato che rompono la compilazione .po
compilemessages fallisce con un errore, oppure il file .po ha un mismatch del flag #, python-format e la voce viene silenziosamente scartata.
Quando una stringa sorgente contiene segnaposto di formato Python come %(name)s, Django contrassegna la voce .po con #, python-format. Se la traduzione ha segnaposto diversi (un errore di battitura come %(nome)s, un segnaposto mancante o uno in più) gli strumenti gettext possono rifiutare la voce oppure compilemessages può fallire. Questo accade comunemente con le traduzioni manuali o con strumenti di traduzione AI che non comprendono la semantica dei segnaposto.
Una voce rotta appare così:
#, python-format
msgid "Hello, %(name)s! You have %(count)d new messages."
msgstr "Hallo, %(naam)s! Je hebt %(count)d nieuwe berichten."
Qui %(naam)s dovrebbe essere %(name)s. I segnaposto devono corrispondere esattamente alla sorgente.
Assicurati che le stringhe tradotte contengano esattamente gli stessi segnaposto della sorgente. Controlla errori di battitura, segnaposto mancanti e segnaposto in più.
Questa è un'area in cui TranslateBot offre un vero vantaggio. La sua logica di preservazione dei segnaposto assicura che tutte le stringhe di formato (%(name)s, {0}, %s) nell'output tradotto corrispondano esattamente alla sorgente. La gestione dei segnaposto è coperta al 100% dai test, quindi gli errori nelle stringhe di formato derivanti dalla traduzione vengono eliminati a livello di strumento piuttosto che intercettati al momento della compilazione.
Se traduci manualmente o con uno strumento che non gestisce i segnaposto, valida i tuoi file .po con:
msgfmt --check-format locale/de/LC_MESSAGES/django.po
Questo esegue la validazione delle stringhe di formato di gettext e segnala eventuali mismatch.
Mettere tutto insieme: un workflow difensivo
La maggior parte di questi problemi condivide una causa comune: passaggi manuali facili da dimenticare. Ecco un workflow che previene tutti i 10 problemi:
# 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
Aggiungi il passaggio 4 alla tua pipeline CI e le stringhe non tradotte, le voci fuzzy e gli errori di formato faranno fallire la build prima di raggiungere la produzione.
Tabella di riferimento rapido
| Causa | Sintomo | Correzione in una riga |
|---|---|---|
Nessun compilemessages |
Le traduzioni esistono ma non appaiono | python manage.py compilemessages |
Manca {% load i18n %} |
TemplateSyntaxError su {% trans %} |
Aggiungi {% load i18n %} al template |
| LocaleMiddleware mancante | La lingua è sempre l'inglese predefinito | Aggiungi django.middleware.locale.LocaleMiddleware a MIDDLEWARE |
| Mismatch codice lingua | Directory della lingua non trovata | Usa pt_BR (underscore) per le directory, pt-br (trattino) per le impostazioni |
| Voci fuzzy saltate | Traduzione nel .po ma non nell'app |
Rimuovi il flag #, fuzzy dopo la revisione |
| LOCALE_PATHS sbagliato | I file .po esistono ma Django li ignora |
Imposta LOCALE_PATHS su un percorso assoluto |
| Traduzioni in cache | Il vecchio testo appare dopo l'aggiornamento | Riavvia il server applicativo |
| Stringa non in gettext | Stringa mancante dai file .po |
Racchiudi in _() o {% trans %} |
| f-string in gettext | Estrazione rotta o mismatch a runtime | Sostituisci con segnaposto % o .format() |
| Mismatch segnaposto | compilemessages fallisce o voce scartata |
Fai corrispondere esattamente i segnaposto tra sorgente e traduzione |
La maggior parte di questi problemi scompare quando automatizzi il passaggio di traduzione e applichi i controlli nella CI. Il comando translate di TranslateBot gestisce la preservazione dei segnaposto e la traduzione incrementale, mentre check_translations intercetta tutto ciò che sfugge (voci non tradotte, flag fuzzy e problemi con le stringhe di formato) prima che raggiungano la produzione.