Comment faire un menu traduit et ordonné avec wagtail

Crée le 17 mai 2025 - Modifié le 18 mai 2025 | Tags:

Dans l'exemple donné pour créer un menu basique sur le site de wagtail, les titres des menus retournés par le site ne sont pas localisés (/traduits en fonction du langage choisi), ni classés dans un ordre précis choisi programmatiquement. Nous allons dans cet article voir comment obtenir ce résultat facilement.

Il est recommandé d'avoir déjà fait le tutoriel officiel de wagtail afin de pouvoir aborder sereinement les concepts évoqués plus loin.

Si vous n'avez pas encore crée de projet wagtail avant, cette page vous donnera les prérequis pour utiliser wagtail et créer votre 1er projet.

On utilisera pour les traductions le package wagtail-localize, un peu plus développé que le module servant à la traduction qui est déjà disponible dans la version de base de wagtail - wagtail.contrib.simple_translation.

Le tutoriel qui suit à été écrit avec ces versions :

  • wagtail==7.0
  • wagtail-localize==1.12

D'après la page github du projet https://github.com/wagtail/wagtail-localize/ voici ce qui est requis :

  • Python (3.9, 3.10, 3.11, 3.12, 3.13)
  • Django (4.2, 5.1, 5.2)
  • Wagtail (5.2 - 7.0)
  • wagtail-modeladmin si vous utilisez wagtail_localize.modeladmin et Wagtail >= 5.2

Je vais utiliser comme point de départ ces modèles de page. Vous pouvez utiliser les vôtres si vous en avez déjà, il ne sera pas nécessaire de les modifier pour les rendre traduisibles ensuite.

votre_projet/votre_app/models.py
from django.db import models

from wagtail.models import Page
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel


class HomePage(Page):
    welcome_msg = RichTextField(blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('welcome_msg'),
    ]

class ArticlesMainPage(Page):
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('intro'),
        FieldPanel('body'),
    ]

class AboutPage(Page):
    body = RichTextField(blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('body'),
    ]

# J'ai déjà crée une page correspondant à chacun de ces modèles, et j'ai indiqué
# dans l'onglet promotion de l'interface admin de création de page que je
# souhaitais avoir chacune de ces pages présentes dans le menu de mon site.

L'internationalisation avec django et wagtail#

Comme expliqué dans la documentation de django (le framework sur lequel wagtail se base):

"Le but de l’internationalisation et de la régionalisation est de permettre à une seule application web de présenter son contenu dans des langues et des formats adaptés à ses visiteurs."

L'internationalisation correspondant à la préparation du site afin de pouvoir effectuer des traductions (le coté dev).

La régionalisation (ou localisation) correspondant à la rédaction des traductions et des formats de langue (le coté traducteur).

Si on effectuait des traductions uniquement avec django, on utiliserait probablement des packages comme django-modeltranslation, django-parler, ou encore django-rosetta, qui requièrent un peu de configuration de la part de l'utilisateur. Ils peuvent aussi requérir qu'on intervienne directement en modifiant nos modèles.

(Si vous voulez aller plus loin et avoir une explication détaillée sur le fonctionnement de ce genre de package, je recommande cette conférence d'une djangocon précédente)


Wagtail quant à lui offre une manière plus simple de faire, en particulier quand on utilise le package wagtail-localize.

◗ Installer et utiliser wagtail-localize#

Afin de pouvoir traduire nos pages et leur contenu, nous allons installer le package wagtail-localize à notre projet. Si vous êtes perdus à un moment, vous pouvez consulter le tutoriel officiel du package.

pip install wagtail-localize

Puis nous allons ajouter les apps de wagtail-localize aux INSTALLED_APPS de notre fichier de settings :

your_project/your_configuration_app/settings.py
INSTALLED_APPS = [
    # ...
    "wagtail_localize",
    "wagtail_localize.locales", 
    # ...
]

Et enfin, tapez une commande afin de charger quelques éléments de frontend en plus apportés par nos nouvelles apps dans l'interface admin de notre site wagtail et effectuer les migrations qui permettront de traduire vos modèles de page déjà existants et les futurs:

python manage.py collectstatic
python manage.py migrate

◗ Préparer wagtail pour l'internationalisation#

D'abord, pour activer l'internationalisation avec wagtail et vous permettre d'avoir du contenu dans plusieurs langues, ajoutez au fichier settings.py de votre projet les lignes suivantes :

your_project/settings.py
#le setting originel pour activer la traduction dans django
USE_I18N = True 

WAGTAIL_I18N_ENABLED = True

#si vous voulez ajouter du support pour la traduction automatique de dates et de nombres
USE_L10N = True

Si vous n'utilisez pas l'anglais comme langue par défaut, changez le setting LANGUAGE_CODE déjà présent dans le fichier à la création d'un projet. De cette manière les pages déjà existantes dans votre projet ne seront pas automatiquement considérées comme "anglaises".

your_project/settings.py
# Par exemple ici en français
LANGUAGE_CODE = "fr-fr"

Ensuite, vous pourrez ajouter les langages que vous voulez rendre disponibles à la traduction grâce à ces deux réglages/settings à connaître:

  • LANGUAGES - Dans ce setting on indique quels langages sont disponibles sur la partie frontend du site.
  • WAGTAIL_CONTENT_LANGUAGES - Celui-ci indique dans quels langages on aura l'option d'écrire dans l'interface admin de wagtail (ou en général dans quel language on pourra écrire du contenu wagtail).


Ces deux settings peuvent être réglés sur exactement la même valeur et dans l'exemple suivant, on va aussi faire en sorte de pouvoir effectuer des traductions en français et en anglais :

your_project/settings.py
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
    ('en', "English"),
    ('fr', "French"),
]

(Rendu à ce stade, on ajouterait normalement wagtail.locales à nos INSTALLED_APPS si on choisissait d'utiliser le module de traduction natif de wagtail et pas wagtail-localize comme nous allons le faire)

Pour permettre à toutes les arborescences de pages d'être servies via le même domaine, par exemple antrodz/mon-contenu-en-français ou antrodz/mon-contenu-en-anglais, on aura besoin d'ajouter un préfixe URL pour chaque langage.

Rappel : l'organisation d'un site wagtail se base sur une forme en arborescence, où en haut de l'arborescence on a une page racine, d'où descendent d'autres pages enfants, qui elles-mêmes peuvent avoir des pages enfants etc.

Dans notre exemple, on aura une arborescence avec une page racine en français ET une autre arborescence en miroir de la première avec une page racine en anglais.

On aura donc une arborescence qui sera servie via le préfixe /fr/ et une autre arborescence à partir du préfixe /en/.

Un exemple pour l'arborescence française : https://www.mon-site/fr/mes-articles-de-blog/comment-faire-des-cookies

Un exemple pour l'arborescence anglaise : https://www.mon-site/en/my-blog-articles/how-to-bake-cookies


Pour implémenter ça, on pourra utiliser la fonction déjà présente nativement dans Django, i18n_patterns(), qui ajoute un préfixe de langage à tous les modèles d'urls qu'on lui passe. Ça active le code de langage spécifié dans l'url, et wagtail tiendra compte de ça quand on lui demandera de router une requête.


Dans le fichier principal d'urls.py de votre projet, ajoutez ensuite les urls de base de wagtail ainsi que tous les autres urls que vous voulez pouvoir traduire dans un block i18n_patterns() :

your_project/urls.py
from django.conf.urls.i18n import i18n_patterns

# On mettra ici les URLs que vous ne voulez pas traduire
# Note: Si vous utilisez l'API de Wagtail ou les sitemaps,
# ceux-ci ne devraient pas non plus être ajoutés aux `i18n_patterns`
urlpatterns = [
    path('django-admin/', admin.site.urls),
    path('admin/', include(wagtailadmin_urls)),
    path('documents/', include(wagtaildocs_urls)),
]

# On mettra ici les URLs que vous souhaitez traduire
# Elles seront disponibles avec un préfixe de langage. 
# Par exemple /en/search/ ou /fr/search/
urlpatterns += i18n_patterns(
    path('search/', search_views.search, name='search'),
    path("", include(wagtail_urls)),
)

Après avoir entouré vos motifs URLs avec i18n_patterns(), votre site répondra aux préfixes URL de langage que vous avez indiqué dans vos settings (par contre un utilisateur essayant d'accéder à une page servie par ces urls sans utiliser de préfixe ne sera pas encore redirigé). Pour qu'un utilisateur soit automatiquement redirigé vers le langage par défaut que nous avons choisi avec le setting LANGUAGE_CODE, ou qu'il soit redirigé vers une page que nous avons traduit dans le même langage que son navigateur (par exemple en anglais), on aura également besoin d'ajouter le middleware "django.middleware.locale.LocaleMiddleware" dans le setting MIDDLEWARE déjà existant dans notre projet :

your_project/settings.py
# Changez le setting déjà présent MIDDLEWARE dans votre fichier pour lui ajouter 
# cette ligne supplémentaire au-dessus de RedirectMiddleware

MIDDLEWARE = [
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "django.middleware.security.SecurityMiddleware",
    # Insert this here
    "django.middleware.locale.LocaleMiddleware",
    "wagtail.contrib.redirects.middleware.RedirectMiddleware",
]

À partir de ce moment, un utilisateur essayant de se connecter à votre site, par ex via l'adresse https://www.mon_site.com/mes-articles-de-blog/comment-faire-des-cookies, se verra automatiquement redirigé vers un autre chemin d'url localisé, https://www.mon_site.com/fr/mes-articles-de-blog/comment-faire-des-cookies ou https://www.mon_site.com/en/my-blog-articles/how-to-bake-cookies suivant le langage de son navigateur.

◗ Les changements à effectuer dans l'interface admin de wagtail#

Il reste encore une étape pour pouvoir traduire les pages wagtail de notre site. Si ce n'est pas déjà fait, créez un utilisateur admin avec la commande python manage.py createsuperuser, puis connectez-vous à l'interface admin de votre site wagtail avec vos identifiants.

En vous connectant, si vous voyez à coté du nom de vos pages un encadré avec le langage que vous avez choisi par défaut, c'est que les étapes précédentes ont réussies !

En bas à gauche de l'interface, allez ensuite dans l'onglet Paramètres -> Régions.

Cliquez sur "Ajouter une région", vous aurez alors les autres choix de langage que vous avez mis dans vos settings à part le langage que vous avez choisi par défaut. Cliquez sur le bouton "activer" de l'option "Synchroniser le contenu d'une autre région" afin de synchroniser les pages existantes et futures de votre langage par défaut avec ce nouveau langage que vous ajoutez. C'est ce qui va automatiquement ajouter une arborescence de page dans un autre langage à votre site. Cliquez ensuite sur "Enregister".

Et voilà ! En retournant à votre page racine, vous pouvez maintenant voir qu'elle a deux versions, de même que toutes ses pages descendantes. Dans notre exemple, une version anglaise et une version française.

En cliquant sur la version anglaise, on pourra maintenant la traduire (pour l'instant tous les champs de texte de cette page sont encore dans notre langage par défaut, le français).

De manière alternative, quand vous cliquez sur une page dans votre langage de base, et que vous entrez dans son interface d'édition, vous pouvez maintenant dans le menu des actions en haut de la page choisir l'option "Traduire la page" qui vous amènera vers une page dans le langage que vous avez sélectionné.

Une fois dans l'interface de traduction, vous aurez l'opportunité de traduire manuellement chaque champ de texte de votre page, en cliquant sur "Traduire" à coté.

Quand vous cliquerez en bas de la page sur "Publier"-suivi du langage dans lequel vous traduisez, vos utilisateurs auront maintenant la possibilité d'accéder à votre contenu traduit !

Pour plus d'explication sur le fonctionnement de cette interface : https://wagtail-localize.org/stable/tutorial/3-content

◗ Ajouter un sélecteur de langage#

Maintenant que l'on a la possibilité de traduire nos pages, au lieu de rajouter manuellement un préfixe comme /en/ ou /fr/ à notre barre de recherche à chaque fois pour voir notre page traduite ou avoir à revenir dans l'interface admin de wagtail pour cliquer dessus, on peut tout simplement rajouter un sélecteur de langage, accessible à un utilisateur anonyme, quelque part dans le template de notre page de base comme ceci :

your_wagtail_website/templates/base_language_picker.html
{# Assurez vous que la ligne ci-dessous est bien présente en haut de votre fichier #}
{% load wagtailcore_tags %}

{% if page %}
    {% for translation in page.get_translations.live %}
        <a href="{% pageurl translation %}" rel="alternate" hreflang="{{ translation.locale.language_code }}">
            {{ translation.locale.language_name_local }}
        </a>
    {% endfor %}
{% endif %}

Vous pourrez ensuite intégrer ce template, par exemple dans le <header> de votre page avec un {% include base_language_picker %}

Avoir des titres de menu traduits#

Maintenant que nous avons des pages et que nous pouvons les traduire, intégrons-les dans le menu de notre site en utilisant principalement l'exemple donné par le site de wagtail dans son tutoriel officiel:

where_you_put_your_templatags/templatetags/navigation_tags.py
from django import template

from wagtail.models import Site


register = template.Library()

@register.simple_tag(takes_context=True)
def get_site_root(context):
    # cf https://stackoverflow.com/questions/78170560/find-sibling-of-root-page-for-current-locale-in-wagtail
    return Site.find_for_request(context["request"]).root_page.localized
your_website/templates/includes/header.html
{% load wagtailcore_tags navigation_tags %}

<header>
    {% get_site_root as site_root %}
    <nav>
        <p>
        <a href="{% pageurl site_root %}">{{ site_root.title }}</a> |
        {% for menuitem in site_root.get_children.live.in_menu %}
            <a href="{% pageurl menuitem %}">{{ menuitem.title }}</a>
        {% endfor %}
        </p>
    </nav>
</header>
  • Dans le 1er fichier, on crée un template tag, get_site_root. Ce template tag nous permet d'avoir une référence à la page racine de notre site, qu'on pourra ensuite charger/loader dans tous nos templates. Notez que l'on ajoute .localized à la fin, afin de se voir retourner la version localisée/régionalisée de notre page racine, ce qui nous permettra d'accéder à la bonne arborescence en fonction du langage détecté par django et votre navigateur. (cf le site de django si vous avez besoin d'une explication sur ce que sont les template tags). IMPORTANT - pour que django trouve votre dossier templatetags, il faut que celui-ci soit placé dans une application présente dans la liste des INSTALLED_APPS de votre fichier de settings.
  • Dans le 2e fichier, où on a le header de notre page, on charge le template tag que nous avons crée, pour ensuite l'utiliser en tant que site_root {% get_site_root as site_root %} afin de pouvoir ensuite récupérer le titre et l'url de toutes les pages qui sont enfants (.get_children) à notre page racine (.site_root). L'utilisateur doit également avoir publié la page (.live) et indiqué dans l'interface admin de wagtail dans l'onglet "Promotion" de sa page, que la page doit être présente dans le menu (.in_menu) . C'est le sens desite_root.get_children.live.in_menu.

P.S : Si vous souhaitez avoir rapidement un menu de navigation et/ou un sélecteur de langage agréables à regarder, des frameworks CSS existent comment Bootstrap ou Tailwind.

Avoir des titres de menu ordonnés#

Voici ensuite deux choix pour réorganiser les titres de votre menu si vous souhaitez le faire :

Choix A : le plus simple, vous n'avez rien à changer dans votre code, seulement dans votre interface admin. Dans cette interface, quand vous cliquez à gauche sur "Pages" et que vous sélectionnez votre page racine, vous arriverez ensuite dans un espace ou vous pourrez voir toutes les pages enfants de votre page racine. Dans les options, à coté du + en haut de la page, vous pouvez sélectionner "Ordre de tri des sous-pages". En cliquant sur cette option, vous pouvez ensuite réorganiser l'ordre de vos sous-pages, selon le principe du "drag and drop". L'inconvénient étant que vous devez ensuite faire de même pour les enfants des autres pages racines que vous possédez, c'est à dire les pages traduites dans une autre langue.

Choix B : Si vous désirez intégrer une logique spécifique pour l'ordre de vos pages, si vous souhaitez ne pas avoir à réorganiser l'ordre dans chaque arborescence de langage, si toutes les pages qui composent votre menu ne sont pas toutes des filles directes de votre page racine ou que vous souhaitez en rajouter manuellement après etc. Vous pouvez alors attribuer un champ aux modèles de page qui seront dans vos menus, afin de facilement pouvoir leur donner un ordre :

your_project/your_app/models.py
menu_rank = models.PositiveSmallIntegerField(default=5, 
        validators=[MinValueValidator(1),MaxValueValidator(10)],
        help_text="""Hiérarchie de 1 à 10 pour l'ordre dans l'arborescence des menus de wagtail, 1 étant le plus élevé.""")

Et ensuite rajouter un nouveau template tag à votre fichier navigation_tags.py, get_site_menus qui remplacerait avantageusement le précédent, get_site_root :

your_wagtail_website/templatetags/navigation_tags.py
@register.simple_tag(takes_context=True)
def get_site_menus(context):
    # on crée une liste qui va contenir nos pages
    pages_li = []

    site_root = Site.find_for_request(context["request"]).root_page.localized

    # On peut ajouter ici la page racine du site afin de l'avoir dans le menu
    # pages_li.append(site_root.specific())

    menuitems = site_root.get_children().live().in_menu()

    # Ci-dessous j'ai utilisé specific() afin d'avoir les instances de mes modèles de page
    # comme <ArticlesMainPage: Article1> et <AboutPage: À propos> et non juste
    # des pages wagtail comme <Page: Article1> et <Page: À propos>
    for page in menuitems.specific():
        pages_li.append(page)

    # on trie notre liste par le champ menu_rank que implémenté sur ces pages
    pages_li.sort(key=lambda x: x.menu_rank) 
    
    return pages_li

Avec cette façon de faire, la valeur du champ menu_rank sera la même pour les multiples versions traduites de la page, on aura donc besoin de changer ce champ une seule fois dans la page originelle non traduite pour qu'il s'applique aux autres pages traduites.

Si jamais vous vous retrouvez bloqués pendant ce tutoriel, si vous aimeriez approfondir certains concepts ou voir toutes les options disponibles pour les technologies et les réglages vus dans cet article, voici quelques liens utiles :

  • Documentation de wagtail sur l'internationalisation avec wagtail - https://docs.wagtail.org/en/stable/advanced_topics/i18n.html
  • Documentation de django sur l'internationalisation et la régionalisation - https://docs.djangoproject.com/fr/5.1/topics/i18n/
  • Documentation de django sur la traduction, utile notamment si vous avez du contenu statique à traduire, par exemple dans vos templates. - https://docs.djangoproject.com/fr/5.1/topics/i18n/translation/#internationalization-in-template-code
  • Lien vers le tutoriel officiel de wagtail localize (en anglais) - https://wagtail-localize.org/stable/tutorial/1-project-set-up/
  • Retour aux articles