Wagtail is an open source CMS project developed with Django web framework by Torchbox. As a CMS, Wagtail supports the creation and modification of content using an easy interface; users can create and publish a new page from a template in few minutes without the need of a developer.

Creating a multilingual page is a common demand among publishers that usually involves different strategies and challenges regarding how the pages and translations are managed and mantained.

Wagtail has different ways to tackle these challenges:

  • Using internationalisation on the urls and duplicate each translatable text field, providing a separate field for each language.

    • Using this system, it will provide you a Wagtail page with multiple content, which will be served depending on the language.

    • The main weakness of this approach is that the url will be the same for the different languages. The language will only change at the beginning of the url.

    • For example, a wagtail page with the following path /home/people/ will have the following url in English /en/home/people/ and in Spanish /es/home/people/.

  • Duplicating the pages tree for every language, starting with a empty content Page which slug has to be the language code.

    • This approach also duplicates the content but allows you to have custom urls for each language.

    • Using the example above the url in english could be the same but in spanish the url could be /es/principal/personas/ for example. The pages tree for the example will be:

alt text

In short, the decision of which approach you should use will relay on your urls.

On our multi-language project at APSL, the possibility of having custom urls for each language was a must. For that reason, we choose the second approach. This approach has three points to prepare, in order to get a multilingual site ready: the Root Page, the links between the translated pages and the way to render them at the templates.

The Root page has to identify the browser’s language and redirect the user to the right language page. This Root page has to be placed at the site root, because of its function. The redirection should be done at the serve method from Page provided by Wagtail and it’s so simple. First, we have to detect the language from the request using get_language_from_request() from django.utils.translation. Django utils needs that the developer has set the LANGUAGES config in the settings.py. In the second place, we have to do the redirection.

The code of the Root Page is the following:

class RedirectionRootPage(Page):

    def serve(self, request):
        # This will only return a language that is in the LANGUAGES Django setting
        language = translation.get_language_from_request(request)
        return HttpResponseRedirect(self.url + language + '/')

The reader should know that each Wagtail’s Page has its own url.

If we are working in a multilingual website, it’s important that the user can easily change the language from a page. The best option is to link the sibling pages together. Taking in account that it’s important to add this behaviour to all the Wagtail Pages that should be translated, the best option is to put the links on a mixin. If you declare this class as abstract=True on the Meta class, a new table on the database won’t be created.

The first thing that you have to decide is which language will be the main language because the implementation to obtain the siblings page will change a bit. The second step is to declare the relationships between the Pages. Django models Foreign Keys must be used for that purpose. Regarding the behavior of this kind of relationship is not necessary to declare the link to the pages of the main language. Then, to make it easier for the web editor, the panels chosen for these relationships should be PageChooserPanels.

The code for this relationships is as follows:

class TranslatePageMixin(models.Model):
    # One link for each alternative language
    french_link = models.ForeignKey(Page, null=True, on_delete=models.SET_NULL,
                                    blank=True, related_name='+')
    english_link = models.ForeignKey(Page, null=True, on_delete=models.SET_NULL,
                                     blank=True, related_name='+')

    panels = [ PageChooserPanel('french_link'), PageChooserPanel(english_link) ]

    class Meta:
        abstract = True

Then, we need a method to get the language code of the page. This method will get all the ancestors of the page and then select the language homepage, which is located at depth three of the website tree. Finally, the method will return the slug (remember that is the language code!) of the page to get the language. This method is as follows:

def get_language(self):
    # This returns the language code for this page.
    language_homepage = self.get_ancestors(inclusive=True).get(depth=3)
    return language_homepage.slug

Then we need a method that finds the main language version of a page. This methods works by reversing the relation links. First, we obtain the language using the method above and then return main language page. If the language is the main one we return the page itself. On the other hand, we return the main language page using the relation. It consists of making a query from the page type filtering the results by the relation. This will retrieve a Wagtails Page. Then, if we want the Custom Model Page, we should use the amazing wagtails model’s Page property specific. The method code is:

def spanish_page(self):
    # This finds the spanish version of this page
    language = self.get_language()
    if language == 'es':
        return self
    elif language == 'fr':
        return type(self).objects.filter(french_link=self).first().specific
    elif language == 'en':
        return type(self).objects.filter(english_link=self).first().specific

The next step that we have to do is to prepare a method for each language that finds out the corresponding page. These methods firstly find out the main version of the page and then they follow the link to the correct language page:

def french_page(self):
    # This finds the french version of this page
    spanish_page = self.spanish_page()
    if spanish_page and spanish_page.french_link:
        return spanish_page.french_link.specific

Finally, we can extend our Wagtail’s Pages with the Language Mixin and add the panels to the model’s panels in order to add the relationships. But seriously, do the web editors have to create all the pages and the links manually? Are we monkeys or computer scientists? We can automate the process. Doing that, the web’s editors will only have to introduce the content.

The process will need a pre-step: create the main pages of the website, which will be named with the language codes. Then the process makes sense. We can override the save method of our Wagtail’s Pages to check if there is a new page and the links that need to be added.

The first thing that we have to do is to check if the page is a new one or not, checking if the id attribute is None or not. Then, we must call the super method in order to not lose the changes.

Secondly, we have to get the top level page on the wagtail’s tree in order to get the language code. For that purpose, we check if the parent’s page is the Main Page or not comparing its specific class. If it’s not, we get the Main Page using the get_ancestors method provided by Wagtail. We know that the domain will be at the first place, the root level at the second and the Main Page with the language code at the third.

Thirdly, if the page is a new one and it has been created on the main language, we make a copy for each with the same content and update the page links. The methods is as follows:

def save(self, *args, **kwargs):
    from cms.models import MainPage

    is_new = self.id is None
    result = super().save(*args, **kwargs)

    parent = self.get_parent()
    if parent.specific_class == MainPage:
        home_page = parent
    else:
        # Our home pages are always placed at depth 3
        home_page = parent.get_ancestors()[2]

    if is_new and home_page.title == 'es':
        # Pages must be created on the main language, in this case: es
        update_attrs = {
            'english_link': None,
            'french_link': None,
        }
        for lang, lang_name in settings.LANGUAGES:
            if 'es' not in lang:
                if lang == 'en':
                    self.english_link = self.copy(to=parent.specific.english_link,
                                                                   update_attrs=update_attrs)
                elif lang == 'fr':
                    self.french_link = self.copy(to=parent.specific.french_link,
                                                                  update_attrs=update_attrs)
                result = super().save(*args, **kwargs)
        return result

Using a method like this one, the pages on the other languages will be generated automatically when the main language page is saved for the first time. In other words, the Wagtail’s tree will grow up synchronized in all the languages. Then, the editors “only” have to fill up the proper content for each language.

Nearly to finish, there is a last step that has to be done: paint the links at the templates that allow the user to change from one language to another. We will do that using the methods that we declare on the Language Mixin at the template:

{% if page.english_page and page.get_language != 'en' %}
    <a href="{{ page.english_page.url }}">{% trans "View in English" %}</a>
{% endif %}
{% if page.french_page and page.get_language != 'fr' %}
    <a href="{{ page.french_page.url }}">{% trans "View in French" %}</a>
{% endif %}

To sum up, we have seen how to develop a multilingual website using Wagtails and a way to make the editors’ work easier. As I wrote at the beginning, we choose this method because of the independence of the urls, which was a must in our project. This means that this solutions fits our problem, but maybe it’s not useful for other cases. Thank you to the amazing Wagtail documentation on the internationalisation topic, which has been very useful to the development and to write this post down.

blog comments powered by Disqus