Makina Blog

Le blog Makina-corpus

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 et MEDIA_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 exemple app/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 variable static_string_1. Vous pouvez ou bien ajouter manuellement votre traduction au django.po (mais relancer makemessages é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 relancer makemessages.
'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

Image
Django PostgreSQL
07/11/2023

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.

Voir l'article
Image
Encart Django
06/11/2023

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.

Voir l'article
Image
Agrégateur Geotrek
08/06/2023

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.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus