How to support multi-language in Wagtail CMS


Michael Yin

Full Stack Developer

Last updated on Feb 22 2020


Table of Contents

Introduction

In this blog post, I will talk about how to add multi-language to Wagtail, after reading, you will learn

  1. Two different solutions to add multi-language to Wagtail CMS.

  2. How to use wagtailtrans to build page trees to implement multi-language feature.

  3. How to use wagtail-modeltranslation to add translate fields to implement multi-language feature.

Some instructions can aslo be found in the relevant project doc but I want this blog can help people learn more quickly.

Preparation

Before we get started, I want to talk about some multi-language settings just in case some reader have no experience with Django

LANGUAGE_CODE = 'en'

from django.utils.translation import gettext_lazy as _

LANGUAGES = [
    ('en', _('English')),
    ('de', _('German')),
    ('fr', _('French')),
    ('it', _('Italian')),
    ('es', _('Spanish')),
]

Above config code should be added to your Django settings file, LANGUAGE_CODE is the default language or the fallback language. LANGUAGES contains the language you want support in your Wagtail project.

Solution 1: Page tree

Workflow

This solution also called duplicate tree, I will show you how it works.

In this solution, we store the english content in one page instance and german content in another page instance, all english content is under a english HomePage which has slug en and all german content is under a german HomePage which has slug de.

So the page tree in Wagtail would seem like this.

/   (RootPage, it would redirect requests)

    en/        (English HomePage)
        english-page-1/        (contains English content)
        english-page-2/
        (other english pages)

    de/        (German HomePage)
        german-page-1/         (contains German content)
        german-page-2/
        (other german pages)

From the above tree structure, we can know.

  1. The RootPage would detect requests and redirect them to the homepages. So if you visit http://www.example.com, then you will be redirected to http://www.example.com/en or http://www.example.com/de.

  2. All language pages have url like http://www.example.com/{language_code}/******, the language prefix is in URL.

  3. We can build relationship between english-page-1 and german-page-1, then user can easily check the translation content. (user can switch the language)

Install wagtailtrans

In previous Wagtail doc, it already talked about how to implement this feature using duplicate tree, but now we can use wagtailtrans to help us do it and do it better.

So here I will talk about how to use wagtailtrans.

First, let's install the package

$ pip install wagtailtrans

Add it to INSTALLED_APPS

INSTALLED_APPS = [
    # ...
    'wagtail.contrib.modeladmin',
    'wagtail.contrib.settings',  # Only required when WAGTAILTRANS_LANGUAGES_PER_SITE=True
    'wagtailtrans',
    # ...
]

Add wagtailtrans.middleware.TranslationMiddleware to your MIDDLEWARE (behind the SiteMiddleware), and make sure django.middleware.locale.LocaleMiddleware is not included in MIDDLEWARE (they do the same job)

Below is recommended order

MIDDLEWARE = [
    'django.contrib.sessions.middleware.SessionMiddleware',
    'wagtail.core.middleware.SiteMiddleware',
    'wagtailtrans.middleware.TranslationMiddleware',
    'django.middleware.common.CommonMiddleware',
    # other Django middleware
    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
]

Page model

Now let's start working on the page model

from wagtailtrans.models import TranslatablePage


class TransHomePage(TranslatablePage):
    body = RichTextField(blank=True, default="")

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


class TransLandingPage(TranslatablePage):

    body = RichTextField(blank=True, default="")

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

Please make all pages are subclass of TranslatablePage.

After you are done, you can migrate the db

$ python manage.py makemigrations
$ python manage.py migrate

# create superuser if need
$ python manage.py createsuperuser
$ python manage.py runserver

Build page tree

First, login Wagtail admin, go to settings/language to config the languages.

Now go to http://127.0.0.1:8000/admin/pages/, add a Translatable site root page, and then go to setting/site, make it the root page or the site.

Then you start add pages under the Translatable site root page.

First, let's create english home pages, please remember to set the slug value to en.

After you create english page, a german page (which has de slug ) would also be created automatically and in draft state, this feature is called synchronized trees.

This means that every change in your ‘canonical’ tree will also be done in the translated trees. To start using this we first need to create a default language (canonical).

You can edit, create pages to build the page tree.

Now the page tree would seem like this

/   (TranslatableSiteRootPage)

    en/        (TransHomePage)
        english-page-1/    (TransLandingPage)

    de/        (TransHomePage)
        german-page-1/     (TransLandingPage)

wagtailtrans would help us manage the relationship between english-page-1 and german-page-1/, please note that landing pages here can also have the same slug value. /de/landing/ and /en/landing/ can also work.

Templates

Because language content are stored in different page instances, so in template you do not need to do anything special.

{% load wagtailcore_tags %}
 <div class="article__body">
     <h2 class="article__title">{{ page.title }}</h2>
     {{ page.body|richtext }}
 </div>

If you want to let user switch language in page, you can use code like this in your template.

{% load wagtailtrans_tags %}

{% get_translations page homepage_fallback=False include_self=False as translations %}

{% for language, page in translations.items %}
<li>
    <a href="{{ page.full_url }}">
        <span>{{ language.code }}</span>
    </a>
</li>
{% endfor %}

Notes

  • wagtailtrans.middleware.TranslationMiddleware would detect your language setting and activate it. The activated language would still work when you visit Wagtail admin. If you visit 127.0.0.1:8000/de/, and then visit Wagtail admin, you will see Wagtail admin in German, this is annoying sometimes. You can fix it by overwriting in wagtail admin/account setting/language preferences

Solution 2: Model Translation

Workflow

In this solution, we do not use page trees, we try to store all language content to one page. Let's assume we need to support en, and de in our project. And now our page has below fields

title
body

Then we can add language suffix to the field so the page can store content of different languages

title
title_en       (contains the English content)
title_de       (contains the German content)

body
body_en
body_de
  1. We can set, get the title_en, title_de, body_en, body_de to make it work for one language.

  2. When we set the title, body field, we check the current language, if we are in en, then self.title = 'test' would set value to title_en, if we are in de, then the self.title = 'test' would set the value to title_de

  3. When we get value from title_de, if the value is None, we should get fallback value from title_en instead of return None value.

Install Wagtail Modeltranslation

Let's first install it

$ pip install wagtail-modeltranslation

Then we change urls.py to make the language prefix can be recognized. i18n_patterns

urlpatterns = [
    url(r'^admin/', include(wagtailadmin_urls)),
    url(r'^documents/', include(wagtaildocs_urls)),
]

from django.conf.urls.i18n import i18n_patterns

urlpatterns += i18n_patterns(
    url(r'', include(wagtail_urls)),
)

Make sure LANGUAGES in settings.py are already config and USE_I18N = True

LANGUAGE_CODE = 'en'

from django.utils.translation import gettext_lazy as _

LANGUAGES = [
    ('en', _('English')),
    ('de', _('German')),
    ('fr', _('French')),
    ('it', _('Italian')),
    ('es', _('Spanish')),
]

USE_I18N = True

Add the package to INSTALLED_APPS, make sure them before all apps that you want to translate

INSTALLED_APPS = (
    'wagtail_modeltranslation',
    'wagtail_modeltranslation.makemigrations',
    'wagtail_modeltranslation.migrate',
    # ...
)

Then modify MIDDLEWARE, LocaleMiddleware should be after SessionMiddleware and before CommonMiddleware

MIDDLEWARE = [
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware', 
    'django.middleware.common.CommonMiddleware',
    # other Django middleware
]

In models.py

class TransHomePage(Page):
    body = RichTextField(blank=True, default="")

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

Then create translation.py besides model.py

from .models import TransHomePage
from modeltranslation.translator import TranslationOptions
from modeltranslation.decorators import register


@register(TransHomePage)
class TransHomePageTR(TranslationOptions):
    fields = (
        'body',
    )

If your page has more fields you want to translate, you can add them to fields

$ python manage.py makemigrations
$ python manage.py migrate --noinput
# you will see some output like this

SQL to synchronize "wagtailcore.page" schema:
   ALTER TABLE "wagtailcore_page" ADD COLUMN "slug_en" varchar(255);
   ALTER TABLE "wagtailcore_page" ADD COLUMN "slug_de" varchar(255);
   ALTER TABLE "wagtailcore_page" ADD COLUMN "slug_fr" varchar(255);
   ALTER TABLE "wagtailcore_page" ADD COLUMN "slug_it" varchar(255);
   ALTER TABLE "wagtailcore_page" ADD COLUMN "slug_es" varchar(255);


# create superuser if need
$ python manage.py createsuperuser
$ python manage.py runserver

As you can see, because page title, slug filed are in wagtailcore_page, so it need to alter the tables to add some columns.

Config in Wagtail

You can create, edit Wagtail page as normal way.

Here we create the homepage and set it as root page of our site.

  1. The top right panel can let you select which translation content to edit or create

  2. You can add, edit many languages at once.

After you publish the page, now we can edit the template.

Template

As I said above, wagtail-modeltranslation can get value based on current language, so in template, you can do it in this way.

{% load wagtailcore_tags %}
 <div class="article__body">
     <h2 class="article__title">{{ page.title }}</h2>
     {{ page.body|richtext }}
 </div>

So when server receive the requests:

  1. django.middleware.locale.LocaleMiddleware would help detect the language setting of user (from URL prefix, session, cookie). It would activate language for every requests.

  2. wagtail-modeltranslation would automatically read value based on the current language. So {{ page.body|richtext }} can work in different languages.

Now if you visit http://127.0.0.1:8000/en/, then english content would show up, if you visit http://127.0.0.1:8000/de/, german content would show up.

You can also check http://127.0.0.1:8000/es/, then you will understand how the fallback work in this case.

If you want user to switch the language, you can use code below

{% load wagtail_modeltranslation %}

{% get_available_languages_wmt as languages %}
{% for language in languages %}
<li>
    <a href="{% change_lang language %}">
        <span>{{ language }}</span>
    </a>
</li>
{% endfor %}

Conclusion

Which one is better? I think most people would have question like this, however, this is not easy to answer, because they solve the problem in different ways.

Here I'd like to give you my thoughts after using them in some of my projects.

Some people like wagtail-modeltranslation because editors want to edit different language content in one place, so if your client really want this then you can choose wagtail-modeltranslation. But you should also know most features of wagtail-modeltranslation is done by patching Wagtail code, so it is not easy if you want do some customization.

wagtailtrans seems not that convenient to use (editors need to edit differnt pages), but I like it more than wagtail-modeltranslation. Because the whole process is clean and simple, what is more, the translation content is stored in page level instead of model field level. So if I want to do some customization in the future, wagtailtrans would be easier for me to do it.

I just shared my thoughts but you can decide by yourself. (no solid answer in this world)

Have fun with Wagtail CMS!

Table of Contents

Subscribe to get notified about new great blog posts about Web Development

Let’s Work on
Contact Us

Subscribe

Get notified about new great Web Development Tutorial