Vous avez marque des chaines pour la traduction, genere des fichiers .po, lance compilemessages, et votre application affiche toujours l'anglais. Vous n'etes pas seul. Le framework i18n de Django est puissant, mais il comporte des pieges qui surprennent meme les developpeurs experimentes.
Ce guide couvre les 10 raisons les plus courantes pour lesquelles les traductions Django echouent silencieusement, avec les symptomes exacts et les correctifs pour chacune.
1. Oublier de lancer compilemessages apres avoir modifie les fichiers .po
Vous avez modifie un fichier .po (manuellement ou avec un outil), mais le texte traduit n'apparait jamais. L'application continue d'afficher les chaines anglaises originales.
Django ne lit pas les fichiers .po au moment de l'execution. Il lit les fichiers binaires compiles .mo (machine object) a la place. Si vous modifiez un fichier .po sans recompiler, Django ne sait pas que quelque chose a change.
Lancez compilemessages apres chaque modification d'un fichier .po :
python manage.py compilemessages
Si vous automatisez vos traductions avec TranslateBot, ajoutez compilemessages comme derniere etape de votre workflow :
python manage.py makemessages -a --no-obsolete
python manage.py translate
python manage.py compilemessages
2. {% load i18n %} manquant dans les templates
Vous utilisez {% trans "Hello" %} dans un template, mais Django leve une TemplateSyntaxError. Ou pire, le tag ne fait rien silencieusement si votre moteur de template est mal configure.
Les tags {% trans %} et {% blocktrans %} se trouvent dans la bibliotheque de tags de template i18n de Django. Sans la charger, le moteur de template ne les reconnait pas.
Ajoutez {% load i18n %} en haut de chaque template qui utilise des tags de traduction :
{% load i18n %}
<h1>{% trans "Welcome to our site" %}</h1>
<p>{% blocktrans with name=user.name %}Hello, {{ name }}!{% endblocktrans %}</p>
C'est une exigence par template. Meme si un template parent charge i18n, les templates enfants qui utilisent des tags de traduction ont besoin de leur propre declaration {% load i18n %}.
3. LocaleMiddleware absent de MIDDLEWARE ou mal positionne
Django sert toujours le contenu dans la langue par defaut, quel que soit l'en-tete Accept-Language du navigateur, le prefixe d'URL ou les parametres de session.
LocaleMiddleware determine la langue active pour chaque requete. Sans lui, Django utilise LANGUAGE_CODE par defaut et ignore tous les mecanismes de selection de langue. Sa position dans la pile de middleware est importante aussi, car il a besoin d'acceder aux donnees de session et a la resolution d'URL.
Ajoutez LocaleMiddleware a votre parametre MIDDLEWARE, apres SessionMiddleware et 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",
]
Assurez-vous egalement que django.conf.urls.i18n est inclus dans votre configuration d'URL si vous utilisez le changement de langue base sur l'URL :
from django.conf.urls.i18n import i18n_patterns
urlpatterns = i18n_patterns(
path("", include("myapp.urls")),
)
4. Codes de langue non concordants (ex. pt-br vs pt_BR)
Les traductions existent dans vos fichiers .po, compilemessages reussit, mais Django ignore la traduction pour certaines locales.
Django s'attend a ce que les repertoires de locale suivent le format <language>_<COUNTRY> avec un separateur underscore et un code pays en majuscules. Par exemple, pt_BR pour le portugais bresilien. Si votre repertoire s'appelle pt-br, pt-BR ou ptBR, Django ne le trouvera pas. Il en va de meme pour le parametre LANGUAGES : les codes utilisent des tirets (pt-br), mais le systeme de fichiers utilise des underscores (pt_BR).
Assurez-vous que votre structure de repertoires correspond aux attentes de Django :
locale/
pt_BR/
LC_MESSAGES/
django.po
django.mo
Et dans vos parametres, utilisez la forme avec tirets :
LANGUAGES = [
("en", "English"),
("pt-br", "Brazilian Portuguese"),
("zh-hans", "Simplified Chinese"),
]
Lors de l'execution de makemessages, utilisez la forme avec underscore pour le drapeau locale :
python manage.py makemessages -l pt_BR
5. Les entrees fuzzy sont silencieusement ignorees lors de la compilation
Une traduction existe dans le fichier .po, mais Django affiche la chaine anglaise originale a l'execution pour cette entree specifique. C'est particulierement frustrant car la traduction est bien la dans le fichier.
Quand le makemessages de Django detecte qu'une chaine source a legerement change, il marque la traduction existante comme "fuzzy" (ce qui signifie que c'est une estimation necessitant une verification humaine). La commande compilemessages ignore toutes les entrees fuzzy, les traitant comme non traduites. Donc l'entree semble traduite dans le fichier .po, mais le fichier .mo l'exclut entierement.
Une entree fuzzy ressemble a ceci :
#, fuzzy
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"
Verifiez la traduction, mettez a jour msgstr si necessaire, puis supprimez le drapeau #, fuzzy :
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"
Puis recompilez :
python manage.py compilemessages
Dans un projet plus important, les entrees fuzzy s'accumulent et sont faciles a manquer. La commande check_translations de TranslateBot les detecte automatiquement :
python manage.py check_translations
locale/nl/LC_MESSAGES/django.po: 0 untranslated, 3 fuzzy
CommandError: Translation check failed
Ajoutez-la a votre pipeline CI avec check_translations --makemessages et vous ne livrerez plus jamais d'entree fuzzy.
6. LOCALE_PATHS non configure ou pointant vers le mauvais repertoire
makemessages cree des fichiers .po a un endroit, mais Django les cherche ailleurs. Les traductions existent sur le disque mais ne sont jamais chargees.
Django recherche les fichiers de traduction dans un ordre specifique : d'abord les repertoires LOCALE_PATHS, puis le repertoire locale/ de chaque application, et enfin le repertoire locale/ du projet. Si LOCALE_PATHS n'est pas defini ou pointe vers le mauvais chemin, Django pourrait ne jamais trouver vos fichiers .po.
Definissez LOCALE_PATHS dans vos parametres avec un chemin absolu :
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
LOCALE_PATHS = [
BASE_DIR / "locale",
]
Verifiez que le repertoire existe et contient la structure attendue :
locale/
de/
LC_MESSAGES/
django.po
django.mo
nl/
LC_MESSAGES/
django.po
django.mo
Une erreur courante est de definir LOCALE_PATHS a locale/ (relatif) au lieu d'un chemin absolu. Django ne resout pas les chemins relatifs depuis la racine de votre projet. Cela depend du repertoire de travail du processus, qui n'est souvent pas ce que vous attendez.
7. Le cache sert des traductions obsoletes
Vous avez mis a jour et compile les traductions, mais l'ancien texte continue d'apparaitre. Redemarrer le serveur resout le probleme.
Le catalogue de traductions de Django est charge une seule fois par processus. En production, les serveurs WSGI/ASGI comme Gunicorn ou Uvicorn maintiennent les processus worker en vie pendant de longues periodes. Le fichier .mo a peut-etre change sur le disque, mais le processus en cours d'execution a toujours les anciennes traductions en memoire. En plus, si vous utilisez le framework de cache de Django ou un proxy inverse comme Nginx ou Cloudflare, les reponses en cache serviront l'ancien contenu jusqu'a leur expiration.
Redemarrez votre serveur d'application apres avoir deploye de nouvelles traductions :
# Gunicorn
kill -HUP $(cat /tmp/gunicorn.pid)
# Systemd
sudo systemctl restart myapp
# Docker
docker compose restart web
Pour le framework de cache de Django, videz le cache apres avoir mis a jour les traductions :
from django.core.cache import cache
cache.clear()
En developpement, runserver recharge automatiquement quand des fichiers Python changent mais ne surveille pas les fichiers .mo. Vous devrez le redemarrer manuellement apres avoir lance compilemessages.
8. Chaines non enveloppees dans gettext (_() ou {% trans %})
makemessages n'extrait pas certaines chaines, donc elles n'apparaissent jamais dans vos fichiers .po et ne sont jamais traduites. C'est le probleme le plus basique et aussi le plus facile a negliger dans une base de code importante.
La commande makemessages de Django utilise xgettext pour scanner votre code source a la recherche de marqueurs de traduction. Si une chaine n'est pas enveloppee dans gettext() (couramment aliase en _()), gettext_lazy(), {% trans %} ou {% blocktrans %}, elle est invisible pour le processus d'extraction.
Enveloppez chaque chaine destinee a l'utilisateur :
# 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>
Utilisez le drapeau --dry-run de TranslateBot pour previsualiser les chaines actuellement non traduites, afin de detecter les chaines manquees lors de l'extraction :
python manage.py translate --target-lang de --dry-run
Cela affiche toutes les entrees non traduites dans vos fichiers .po sans effectuer d'appels API ni de modifications.
9. Les f-strings ne peuvent pas etre traduites (limitation de Django)
Vous enveloppez un f-string dans _() et vous obtenez soit une erreur de syntaxe, soit makemessages extrait une chaine cassee/partielle qui ne peut pas etre traduite.
Les f-strings Python sont evaluees a l'execution. L'outil d'extraction xgettext analyse le code source de maniere statique, donc il ne peut pas evaluer les expressions Python a l'interieur des accolades {}. Cela signifie que _(f"Hello, {name}") est extraite comme une chaine contenant une expression litterale {name} (ou echoue completement a l'extraction), et l'entree .po resultante ne correspondra jamais a la chaine a l'execution.
Utilisez le formatage % de Django ou .format() avec des espaces reserves nommes a la place :
# 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)
Ce n'est pas une limitation de TranslateBot ni des outils. C'est fondamental au fonctionnement de gettext. La chaine source doit etre un litteral statique pour pouvoir etre extraite et recherchee a l'execution.
TranslateBot preserve tous ces formats d'espaces reserves (%(name)s, {0}, %s, balises HTML) lors de la traduction, donc les chaines traduites restent pleinement fonctionnelles.
10. Erreurs de format de chaine de caracteres des espaces reserves cassant la compilation .po
compilemessages echoue avec une erreur, ou le fichier .po a une discordance de drapeau #, python-format, et l'entree est silencieusement supprimee.
Quand une chaine source contient des espaces reserves de format Python comme %(name)s, Django marque l'entree .po avec #, python-format. Si la traduction a des espaces reserves differents (une faute de frappe comme %(nome)s, un espace reserve manquant ou un supplementaire), les outils gettext peuvent rejeter l'entree ou compilemessages peut echouer. Cela arrive couramment avec les traductions manuelles ou avec les outils de traduction IA qui ne comprennent pas la semantique des espaces reserves.
Une entree cassee ressemble a ceci :
#, python-format
msgid "Hello, %(name)s! You have %(count)d new messages."
msgstr "Hallo, %(naam)s! Je hebt %(count)d nieuwe berichten."
Ici %(naam)s devrait etre %(name)s. Les espaces reserves doivent correspondre exactement a la source.
Assurez-vous que les chaines traduites contiennent exactement les memes espaces reserves que la source. Verifiez les fautes de frappe, les espaces reserves manquants et les espaces reserves supplementaires.
C'est un domaine ou TranslateBot offre un reel avantage. Sa logique de preservation des espaces reserves garantit que toutes les chaines de format (%(name)s, {0}, %s) dans la sortie traduite correspondent exactement a la source. La gestion des espaces reserves est couverte par une couverture de tests a 100%, donc les erreurs de chaines de format de traduction sont eliminees au niveau de l'outil plutot que detectees au moment de la compilation.
Si vous traduisez manuellement ou avec un outil qui ne gere pas les espaces reserves, validez vos fichiers .po avec :
msgfmt --check-format locale/de/LC_MESSAGES/django.po
Cela execute la validation des chaines de format de gettext et signale toute discordance.
Tout assembler : un workflow defensif
La plupart de ces problemes partagent une cause racine : des etapes manuelles faciles a oublier. Voici un workflow qui previent les 10 problemes :
# 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
Ajoutez l'etape 4 a votre pipeline CI et les chaines non traduites, les entrees fuzzy et les erreurs de format feront echouer le build avant d'atteindre la production.
Tableau de reference rapide
| Cause | Symptome | Correctif en une ligne |
|---|---|---|
Pas de compilemessages |
Les traductions existent mais n'apparaissent pas | python manage.py compilemessages |
{% load i18n %} manquant |
TemplateSyntaxError sur {% trans %} |
Ajouter {% load i18n %} au template |
| LocaleMiddleware manquant | La langue est toujours l'anglais par defaut | Ajouter django.middleware.locale.LocaleMiddleware a MIDDLEWARE |
| Code de langue non concordant | Repertoire de locale introuvable | Utiliser pt_BR (underscore) pour les repertoires, pt-br (tiret) pour les parametres |
| Entrees fuzzy ignorees | Traduction dans .po mais pas dans l'app |
Supprimer le drapeau #, fuzzy apres verification |
| LOCALE_PATHS incorrect | Les fichiers .po existent mais Django les ignore |
Definir LOCALE_PATHS avec un chemin absolu |
| Traductions en cache | Ancien texte apparait apres mise a jour | Redemarrer le serveur d'application |
| Chaine pas dans gettext | Chaine absente des fichiers .po |
Envelopper dans _() ou {% trans %} |
| f-string dans gettext | Extraction cassee ou discordance a l'execution | Remplacer par des espaces reserves % ou .format() |
| Espaces reserves non concordants | compilemessages echoue ou entree supprimee |
Faire correspondre exactement les espaces reserves entre source et traduction |
La plupart de ces problemes disparaissent quand vous automatisez l'etape de traduction et appliquez des verifications en CI. La commande translate de TranslateBot gere la preservation des espaces reserves et la traduction incrementale, tandis que check_translations detecte tout ce qui passe a travers les mailles du filet (entrees non traduites, drapeaux fuzzy et problemes de chaines de format) avant qu'ils n'atteignent la production.