Makina Blog
Internationalisation avec Django
En tant que développeurs nous sommes parfois confronté à la problématique de l'accessibilité des utilisateurs parlant différentes langues. Cet article est à destination des développeurs Django souhaitant découvrir l'internationalisation (i18n) et propose un parcours pas à pas dans cet exercice.
Dans cet exemple, nous utiliserons Django 2.1.
0 - Projet basique :
Nous partirons d’un projet très simple.
Ici pas besoin de modèles, une simple vue introduisant du contexte à traduire et un template suffiront :
-
myproject/myapp/templates/my_template.html
:
<!doctype html>
<html>
<head>
</head>
<body>
<h1>translations</h1>
<p>This is a paragraph to translate with a variable : {{ static_string_1 }}</p>
<p>{{ second_paragraph }}</p>
<ul>
<li>{{ static_string_1 }}</li>
<li>{{ static_string_2 }}</li>
</ul>
</body>
</html>
-
myproject/myapp/views.py
:
from django.shortcuts import render
def my_view(request):
context = {
'static_string_1': 'first_static_string_to_translate',
'static_string_2': 'second_static_string_to_translate',
'second_paragraph': 'This is a second paragraph to translate',
}
return render(request, 'my_template.html', context)
-
myproject/myproject/urls.py
:
from django.urls import path, include
urlpatterns = [
path('', include('myapp.urls', namespace='myapp')),
]
-
myproject/myapp/urls.py
:
from django.urls import path
from .views import my_view
app_name = 'myapp'
urlpatterns = [
path('my_page', my_view, name='my_view'),
]
1 - Installer les outils d’internationalisation :
Il nous faut d’abord nous assurer que les outils d’internationalisation sont bien activés. Pour ce faire, il faut :
1- Définir
LANGUAGE_CODE.
Ce qui permet de définir une langue par défaut. (Sa valeur par défaut
est 'en-us'
). Ce paramètre est suffisant si une seule langue est
utilisée (les textes inclus avec Django sont traduits dans cette
langue).
LANGUAGE_CODE = 'fr'
2- Si plusieurs langues sont utilisées, il faut activer le
LocaleMiddleware
qui doit être inséré après le SessionMiddleware
et le
CacheMiddleware
(s’il ce dernier est utilisé), et avant le
CommonMiddleware
:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
3- Une des manières de faire pour indiquer à Django quelle langue utiliser est d’en préfixer les URL. Cela peut être fait assez simplement avec la fonction i18n_patterns :
from django.conf.urls.i18n import i18n_patterns
from django.urls import path, include
urlpatterns = i18n_patterns(
path('', include('myapp.urls', namespace='myapp')),
)
Cette fonction automatisera la prise en compte des codes de langue
passés en préfixe par l’URL et l’insertion de ce préfixe lors des
reverse()
en fonction de la locale à utiliser :
>>> from django.urls import reverse
>>> from django.utils.translation import activate
>>> reverse('fr')
'/fr/my_page'
>>> activate('it')
>>> reverse('myapp:my_view')
'/it/my_page'
À ce stade l’URL nécessite d’être préfixée du code de la locale à
utiliser, dans le cas contraire l’utilisateur est redirigé vers l’URL
correspondante préfixée par la locale par défaut. Si vous voulez
désactiver le préfixage de l’URL pour la langue par défaut, vous pouvez
passer prefix_default_language=False
à i18n_patterns()
.
Pour l’instant, aucune traduction n’est effective, mais vous pouvez constater les changements dans les URL.
Vous pouvez également trouver plus de détails sur l’installation des outils d’internationalisation ici.
2 - Marquer les chaînes à traduire dans le code :
Afin de pouvoir traduire nos textes, il faut indiquer à Django quels sont-ils afin que Django puisse dans un premier temps les repérer afin de créer les fichiers de traduction et dans un second temps les traduire lors du traitement des requêtes.
Pour ce faire il faut marquer les textes. Il y a pour cela deux manières :
Dans les templates :
Il y a deux manières principales de marquer les textes à traduire dans un template :
- Le tag {% trans %} : Ce tag permet de traduire de simples chaînes de caractère ou des variables.
- Le tag
{% blocktrans %} :
Ce tag permet de traduire des chaînes plus complexes et — contrairement
au tag
{% trans %}
— des chaînes intégrant des variables.
{% load i18n %}
<!doctype html>
<html>
<head>
</head>
<body>
<h1>{% trans "Translations" %} :</h1>
<p>{% blocktrans %}This is a paragraph to translate with a variable : {{ static_string_1 }}{% endblocktag %}</p>
<p>{{ second_paragraph }}</p>
<ul>
<li>{% trans static_string_1 %}</li>
<li>{% trans "second_static_string_to_translate" %}</li>
</ul>
</body>
</html>
Dans les deux cas n’oubliez pas de charger ces tags en insérant
{% load i18n %}
au début de votre template.
Vous pouvez noter ici l’usage d’une variable dans un
{% blocktrans %}
: il est tout à fait possible de traduire des
chaînes de caractères non-statiques dans un de ces blocs.
Dans le code python :
Il est possible également de traduire des chaînes directement dans le code python à l’aide des fonctions suivantes.
- (u)gettext
gettext()
permet de traduire une simple chaîne. - (u)gettext_lazy
gettext_lazy()
est équivalente àgettext()
. La différence résidant dans le fait que la traduction s’effectuera lors de l’accès à la valeur traduite plutôt que lors de l’appel de la fonction.
Usuellement ces fonctions sont importées sous l’alias _()
afin
d’alléger l’écriture du code.
from django.shortcuts import render
from django.utils.translation import gettext as _
def my_view(request):
context = {
'static_string_1': 'first_static_string_to_translate',
'static_string_2': 'second_static_string_to_translate',
'second_paragraph': _("This is a second paragraph to translate"),
}
return render(request, 'my_template.html', context)
Dans notre exemple, je marque dans la vue la seule chaîne qui n’est pas encore marquée dans notre template.
Dans quels cas?
Dans une définition de modèle ou de formulaire : Utilisez
gettext_lazy()
.
Dans une vue : Utilisez gettext()
.
En règle générale : Si vous devez appeler la fonction sur une chaîne à
un moment où Django ne sait pas encore quelle langue utiliser, utilisez
gettext_lazy()
. La chaîne ne sera traduite qu’au dernier moment (au
moment de son rendu).
Différence avec ou sans le préfixe u
:
Historiquement, les fonctions préfixées d’un u
étaient destinées à
gérer les chaînes en unicode avec Python2. Mais depuis Python3 elles
sont interchangeables. Une prochaine obsolescence des fonctions
préfixées est possible.
3 - Créer les message files avec la sous-commande makemessages
:
Une fois que les textes sont marqués, Django va pouvoir les repérer et les rassembler dans des fichiers afin de les traduire.
La sous-commande
makemessages
va parcourir tout le répertoire actuel et rassembler toutes les chaînes
de caractères à traduire dans un “message file” qui aura pour extension
.po
. Il sera ainsi beaucoup plus facile pour les traducteurs de
faire leur travail sans avoir à toucher au code.
Voici le fichier obtenu :
$ cd /path/to/app
$ mkdir -p locale/fr_FR
$ django-admin makemessages
processing locale fr_FR
$ cat locale/fr_FR/LC_MESSAGES/django.po
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-27 19:12+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: templates/my_template.html:7
msgid "Translations"
msgstr ""
#: templates/my_template.html:8
#, python-format
msgid "This is a paragraph to translate with a variable : %(static_string_1)s"
msgstr ""
#: templates/my_template.html:12
msgid "second_static_string_to_translate"
msgstr ""
#: views.py:8
msgid "This is a second paragraph to translate"
msgstr ""
Notes:
- Pour éviter d’avoir les commentaires précisant les emplacements des
chaînes à traduire, vous pouvez lancer cette commande avec l’option
--no-location
. - Ajouter des settings
STATIC_ROOT
etMEDIA_ROOT
évitera àmakemessages
de parcourir ces dossiers lors de la recherche de chaînes à traduire. - Il est nécessaire de créer un dossier
locale
avec un sous-dossier nommé d’aprés la locale correspondante s’il n’existe pas déjà dans l’application à traduire (par exempleapp/locale/fr_FR
. - Vous pouvez constater que le fichier créé ne contient pas de
traduction à compléter pour notre
{% trans static_string_1 %}
. C’est normal car Django ne sait pas à ce moment là ce que contiendra la variablestatic_string_1
. Vous pouvez ou bien ajouter manuellement votre traduction audjango.po
(mais relancermakemessages
écrasera ces changements) ou bien marquer cette traduction comme no-op dans votre code python avec gettext_noop() afin de la marquer pour les traductions sans la traduire avec cette fonction. Une fois cela fait vous pouvez relancermakemessages
.
'static_string_1': gettext_noop('first_static_string_to_translate'),
4 - Compléter les message files obtenus :
Cette tâche est tout simplement le travail que le traducteur aura à faire.
Il faudra compléter les msgstr
dans le fichier django.po
créé
par la commande makemessages
, éventuellement avec un éditeur conçu
pour (comme poedit).
$ cat locale/fr_FR/django.po
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-28 10:40+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: templates/my_template.html:7
msgid "Translations"
msgstr "Traductions"
#: templates/my_template.html:8
#, python-format
msgid "This is a paragraph to translate with a variable : %(static_string_1)s"
msgstr "Ceci est un paragraphe à traduire avec une variable : %(static_string_1)s"
#: templates/my_template.html:11
msgid "second_static_string_to_translate"
msgstr "seconde chaîne statique à traduire"
#: views.py:7
msgid "first_static_string_to_translate"
msgstr "première chaîne statique à traduire"
#: views.py:9
msgid "This is a second paragraph to translate"
msgstr "Ceci est un second paragraphe à traduire"
5 - Compiler les .po en .mo :
Une fois les message files complétés, vous pourez les compiler en “language files” avec la commande compilemessages.
$ ./manage.py compilemessages
processing file django.po in /path/to/myproject/myapp/locale/fr_FR/LC_MESSAGES
$ ls locale/fr_FR/LC_MESSAGES
django.mo django.po
Les pages devraient alors être traduites :
$ curl localhost:8000/fr/my_page
<!doctype html>
<html>
<head>
</head>
<body>
<h1>Traductions :</h1>
<p>Ceci est un paragraphe à traduire avec une variable : first_static_string_to_translate</p>
<p>Ceci est un second paragraphe à traduire</p>
<ul>
<li>première chaîne statique à traduire</li>
<li>seconde chaîne statique à traduire</li>
</ul>
</body>
</html>
Avec la version italienne :
$ curl -L localhost:8000/it/my_page
<!doctype html>
<html>
<head>
</head>
<body>
<h1>Translations :</h1>
<p>This is a paragraph to translate with a variable : first_static_string_to_translate</p>
<p>This is a second paragraph to translate with</p>
<ul>
<li>first_static_string_to_translate</li>
<li>second_static_string_to_translate</li>
</ul>
</body>
</html>
La version italienne n’est ici pas traduite faute d’avoir les traductions pour cette langue, mais il suffit de reproduire les étapes précédentes afin de pallier ce problème.
6 - Gérer les contextes :
Parfois le sens de certaines traductions va être ambigu :
Prenons par exemple le mot anglais “shell”. S’agit-il de la coquille d’un fruit de mer? Du langage de terminal? Potentiellement les deux. Pour écarter toute ambiguïté il est possible d’ajouter un contexte :
<h2>{% trans "Synonyms" %}</h2>
<ul>
<li>{% trans "shell" context "sea" %}</li>
<li>{% trans "shell" context "programming" %}</li>
</ul>
Il est également possible d’ajouter du contexte dans les templates avec
le tag {% blocktrans %}
et dans le code python avec des fonctions
comme
pgettext().
Il suffit ensuite de relancer makemessages
et ces nouvelles
traductions seront ajoutées, avec une occurence par contexte :
msgctxt "sea"
msgid "shell"
msgstr "coquillage"
msgctxt "programming"
msgid "shell"
msgstr "coquillage"
Puis compilemessages
fera le reste pour que la traduction soit
effective.
Bonus: Suivre les .po en version avec git.
Conclusion :
Nous avons pu voir tout au long de cet article les principales possibilités qu'offre Django pour traduire les textes d'une application. Django offre un double intérêt car il permet aux traducteurs de travailler sans avoir à manipuler directement le code tout en permettant aux développeurs de manipuler efficacement les chaînes à traduire dans différentes situations.
Actualités en lien
Utiliser des fonctions PostgreSQL dans des contraintes Django
Cet article vous présente comment utiliser les fonctions et les check constraints
PostgreSQL en tant que contrainte sur vos modèles Django.
Comment migrer vers une version récente de Django ?
Que ce soit pour avoir les dernières fonctionnalités ou les correctifs de sécurité, rester sur une version récente de Django est important pour la pérennité de son projet.
Le projet Agrégateur : fusionner des bases de données Geotrek
Le partage et la diffusion des données font partie des problématiques historiques au cœur du projet Geotrek.