In the example for creating a basic menu given by the wagtail website, the menu's titles that are returned aren't localized (/traducted in a chosen language), nor programmaticaly ordered. We will see in this tutorial how to achieve that result.
I recommend having already done the official wagtail tutorial to have an easier understanding of the following article.
If you haven't already created a wagtail project before, this page will give you the prerequisites for using wagtail and creating your first project.
We will use the wagtail-localize package for our traductions, that is a bit more developped than the translation module already available in a base wagtail install - wagtail.contrib.simple_translation.
The following tutorial was written with these versions :
- wagtail==7.0
- wagtail-localize==1.12
According to the project's github page https://github.com/wagtail/wagtail-localize/, here is what is required :
- 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 if you use
wagtail_localize.modeladmin
and Wagtail >= 5.2
I will personnaly use these page models for this tutorial. You can use yours if you already have some, it won't be necessary to modify them to make them translatables.
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'),
]
# I already created a page corresponding to each one of these models, and I checked
# in the promotion tab of the page creation admin interface that I
# wished to have each of these pages in my site menu.
Internationalization with django and wagtail#
As explained if the django documentation (the framework from which wagtail derives):
The goal of internationalization and localization is to allow a single web application to offer its content in languages and formats tailored to the audience.
Internationalization will correspond to the preparation of a website for making translations (the dev side).
Localization will correspond to the writing of the translations and language formats used (the translator side).
If we only used django for translations, we would probably use packages like django-modeltranslation, django-parler, or django-rosetta, that require a bit of configuration on our end. Some of these would also need us to directly modify our models.
(If you want to go further and have a detailed explanation of how that kind of package works, I recommend this conference of a previous djangocon)
Wagtail offers a simpler way to translate content out of the box, in particular when you add the wagtail-localize package.
◗ Install and use wagtail-localize#
With the goal of being able to translate our pages and their contents, we will install the wagtail-localize package to our project. If you get lost at some point, you can consult the package official tutorial.
pip install wagtail-localize
Then we will add the wagtail-localize apps to the INSTALLED_APPS of our project settings file :
INSTALLED_APPS = [
# ...
"wagtail_localize",
"wagtail_localize.locales",
# ...
]
Then you will have to type these commands so that you can load a few added frontend elements that our newly installed apps bring in the admin interface of our wagtail website and make migrations that will allow us to translate the content of your already existing and future page models:
python manage.py collectstatic
python manage.py migrate
◗ Prepare wagtail for internationalization#
First, to activate internationalization with wagtail and having content in multiple languages, add these lines to your project's settings.py file:
# the original setting for activating translations in django
USE_I18N = True
WAGTAIL_I18N_ENABLED = True
#if you want to add support for the automatic translation of dates and numbers
USE_L10N = True
If you don't use english for your content default language, change the LANGUAGE_CODE
setting that already exists in the settings file on project creation. This way, all your existing pages won't be automatically considered as "english" ones.
#For example, here for french :
LANGUAGE_CODE = "fr-fr"
Then, you can add the languages that you want to make available for translation thanks to this couple of useful settings to know:
LANGUAGES
- In this setting, we indicates which languages are available on the frontend part of our website.WAGTAIL_CONTENT_LANGUAGES
- This one will determinate in which languages we will be able to write into wagtail's admin interface (or generally in which language we will be able to write wagtail content).
These two settings can be set on exactly the same value, and in the following example, we will choose french and english as our options.
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
('en', "English"),
('fr', "French"),
]
(At this point, we would normally add wagtail.locales
to our INSTALLED_APPS
if we had chosen to use the native translation module of wagtail, and not wagtail-localize as we will do)
To allow all the pages trees to be served from the same domain, for example antrodz/my-french-content or antrodz/my english content, we will need to add an URL prefix for each language.
Reminder : the organization of a wagtail website is based on a tree shape. At the top of the tree, there is a root page, from which descends children pages, from which can descends great-children pages etc.
In our exemple, we will have a page tree with an english root page AND another page tree, that mirrors the first, that will have a root page in french.
So, we will have a page tree served with the /en/ language prefix, and another page tree served with the /fr/ prefix.
An exemple for the french tree : https://www.my-site/fr/mes-articles-de-blog/comment-faire-des-cookies
An exemple for the english tree : https://www.my-site/en/my-blog-articles/how-to-bake-cookies
To implement this, we can use the function that already exists natively in Django, i18n_patterns(), that adds a language prefix to all the url models that we pass into it. It activates the language code specified in the url, and wagtail will understand this when we will ask it to route a request.
In the main urls.py
file of your project, add all the base urls of wagtail and all the other urls that you want to be served with a language prefix in a i18n_patterns()
block:
from django.conf.urls.i18n import i18n_patterns
# We will add here the URLs that you don't want to serve with a language prefix
# Note: If you use wagtail's API or sitemaps,
# these should not either be included to the `i18n_patterns`
urlpatterns = [
path('django-admin/', admin.site.urls),
path('admin/', include(wagtailadmin_urls)),
path('documents/', include(wagtaildocs_urls)),
]
# Here you will put the URLs that you wish to translate
# They will be served with a language prefix.
# For example /en/search/ or /fr/search/
urlpatterns += i18n_patterns(
path('search/', search_views.search, name='search'),
path("", include(wagtail_urls)),
)
After enclosing your urls patterns with i18n_patterns()
, your website will respond to the language prefixes that you indicated in your settings. Now if we want a user to be automatically redirected to the default language that we chose with the LANGUAGE_CODE
setting, or if we want him to be redirected towards a page that we translated in the same language as his web browser (for example in french), we will also need to add the "django.middleware.locale.LocaleMiddleware"
middleware in the MIDDLEWARE
setting that already exists in our project.
# Change the already existing setting MIDDLEWARE in your setting file to add
# this line above 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",
]
From now on, an user that will try to connect to your website, for example by the https://www.y_site.com/my-blog-articles/how-to-bake-cookies adress, will be redirected automatically towards another localized url path, like https://www.my_site.com/en/my-blog-articles/how-to-bake-cookies or https://www.my_site.com/fr/mes-articles-de-blog/comment-faire-des-cookies depending on its web browser language.
◗ Changes to make in wagtail admin interface#
There still exists a necessary step to translate the wagtail pages of our website. If it isn't already done, create an admin superuser with the command python manage.py createsuperuser
, then connect to the admin interface of your wagtail website with your identifiers.
After connecting, if you see a box near the name of your pages with the language that you chose by default, it means the preceding steps worked !
Down left of your interface, go next to the Parameters -> Regions tab.
Click on "Add a region", you will then have the other language choices that you put into your settings, excluding the language you chose by default. Click on the "Activate" button of the "Synchronize the content from another region" so that you can synchronize the existing and future pages of your project that are in your default language with this new lanugage that you add. This will automatically create a page tree in this other language to your website. Then click on "Save".
And voilà ! By returning to your root page, you can now see that it has 2 versions, as all its descendants pages. In our example, a french version and an english version.
By clicking on the french version of a page, we will now be able to traduct it (for now all the text fields of that page are still in our default language, english).
As an alternative, when you click on a page in your default language, and you enter its edit interface, you can now see in the actions menu of the top of the page the option "Translate the page" that will bring you to a page in the language that you selected.
Once in the translation interface, you will have the opportunity to manually traduct each text field of your page, by clicking on "Translate" next to it.
When you will click towards the bottom of the page on "Publish"-followed by the language in which your traduct, your users will now have the possibility to see your translated content !
For more explanations on how this interface works : https://wagtail-localize.org/stable/tutorial/3-content
◗ Add a language picker#
Now that we have the possibility to translate our pages, instead of manually adding a prefix like /en/ or /fr/ to our browser search bar each time to see our translated page or coming back to the admin interface to click on it, we can just add a language picker to the base template or our header, that any anonymous user will be able to click to select its prefered language.
{# Make sur this line is at the top of your file #}
{% 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 %}
You will then be able to implement that template, in the <header> of your page with a {% include base_language_picker %} for example.
Having translated menu titles#
Now that we have pages, and that we can traduct them, let's integrate them in the menu of our site, mainly by using the example given on the wagtail's website in its official tutorial:
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
{% 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>
- In the first file, we create a template tag,
get_site_root
. This template tag allows us to have a reference to the root page of our website, that we will then load in all of our templates. Note that we add.localized
at the end, so that we are returned with our localized root page. With this, we can access the right page tree depending on what language django and your browser detected. (if you need explanations on what are template tags). IMPORTANT - to find your template tags folder, django needs it to be located in an app inside theINSTALLED_APPS
setting of your project settings file. - In the 2nd file, where we have the header of our page, we load the template tag that we created, so that we can use it as site_root
{% get_site_root as site_root %}
. We will next be able to get the title and the urls of all the children pages (.get_children) of our root page (.site_root). We further want the pages that have the published status (.live), and that have the "show in menu" option checked in the "Promotion" tab of the edit admin interface of these pages (.in_menu). That is the meaning ofsite_root.get_children.live.in_menu
.
P.S: If you want to quickly make a navigation bar and/or a language picker that look nice, CSS frameworks exist like Bootstrap or Tailwind.
Having ordered menu titles#
Now here is two choices for reorganizing the titles of your menu if you wish to do so :
Choice A : the easiest. You don't have to change anything in your code, only in your admin interface. In this interface, when you click on the left on "Pages" and that you select your root page, you will then land on a space where you can see each one of the children pages of your root page. In the options, next to the + at the top of the page, you can select "Sort menu order". By clicking on this option, you can reorganize the order of your pages, with the drag and drop principle. The main default being that you have to do it again for the children of the other root pages of the other page trees that are translated in other languages.
Choice B : If you want to implement a specific logic for the order of your pages, if you want not having to reorganize the order in each of the language trees, if all the pages that are referenced by your menu aren't all direct children of your root page or if you want to manually add other pages next etc. Then you can give the page models that will be in your menu a new field, so that you can easily give them an order in the admin edit interface :
menu_rank = models.PositiveSmallIntegerField(default=5,
validators=[MinValueValidator(1),MaxValueValidator(10)],
help_text="""Rank from 1 to 10, in your menu, 1 being the highest.""")
And then add a new template tag to your navigation_tags.py file, get_site_menus
that will advantageously replace the previous one, get_site_root
:
@register.simple_tag(takes_context=True)
def get_site_menus(context):
# we reate a list that will hold our pages
pages_li = []
site_root = Site.find_for_request(context["request"]).root_page.localized
# We can add our root page here if we want it in the menu
# pages_li.append(site_root.specific())
menuitems = site_root.get_children().live().in_menu()
# Under here I used specific() to get all the instances of my page models
# like <ArticlesMainPage: Article1> et <AboutPage: About> and not just
# wagtail pages instances like <Page: Article1> et <Page: About>
for page in menuitems.specific():
pages_li.append(page)
# We sort our list by the menu_rank field I implemanted in these page models
pages_li.sort(key=lambda x: x.menu_rank)
return pages_li
With that way of doing it, the value of the menu_rank field will be the same for the multiple translated versions of the page, so we will only need to modify that field once in the original page and it will apply to all other translated pages of that one.
Useful links#
If at some point you are lost or stuck during this tutorial, if you would like to deepen some concepts or know all the options availables for the packages or settings seen in this article, here are some useful links :