Bi-lingual website using Wagtail CMS.

We’re now using Wagtail for our main website where I work and by law we need to have a Welsh version of the site. So English and Welsh are the two languages we needed. In this example of our approach I’ll outline how to get a simple, easy bi-lingual website going with wagtail. Some of this code and approach is based on Karl from Torchbox’s tutorial, but we didn’t need multiple languages only two, and there is no automatic selection of language based on your locale.

For this to work your second language site must live within the main site, as a parent page with all of the second language pages living from that page. In our example within the top level of the main site, is a page called ‘Hafan’ (Home in Engish) with a slug of ‘cymraeg’. So basically everything that lives of that slug is considered to be a Welsh Page.

For the content editor the process goes something like this:

  • Add an English page where ever in the main site.
  • Add a Welsh page somewhere within the /cymraeg part of the site as a child or grandchild page (Wagtail’s content is in a tree)
  • When editing the Welsh page (using Wagtails useful page browsing interface panel) link to the equivalent English page. (If the pages are the same template type, then you only need to link in one direction, but if you want to form a relationship betweent two different page types, that’s fine but you need to link in both directions, i.e from the English page also choose the Welsh version of the page)
  • In the front end on any page that has a Welsh/English version of itself, a link will appear saying ‘Read this page in English, or the same in Welsh)
  • When a Welsh page is viewed the template elements know to change to show Welsh items, e.g the Navigation, breadcrumbs footer

How to code this approach (the Basics)

Firstly in your add this mix-in class :

class TranslatablePageMixin(models.Model):

 translated_link = models.ForeignKey(Page, null=True, on_delete=models.SET_NULL, blank=True, related_name='+')

 class Meta:
 abstract = True

Now in any pages types that need a Welsh version add the mix-in

class ArticlePage(Page, TranslatablePageMixin):
body = RichTextField(blank=True)

in the content_panels for that page type remember to add a Page Chooser for the translated version of the page (other wise you won’t get the nice Wagtail page chooser UI appearing in the edit view of the page) :


That’s all you need to do in the bit, obviously you need to run your migrations stuff to make the new database changes happen.

Next you’re going to need a template tag which will produce the ‘View this page in….’ link to the correct translated page.

So in your site’s define an inclusion tag :

@register.inclusion_tag('yourapp/tags/translated_link.html', takes_context=True)
def view_this_page_in(context, calling_page=None):

 # First see if calling page has the link set
 if calling_page.translated_link:
 translated_version = calling_page.translated_link
 # next see if a page of same type has relationship to calling page
 if type(calling_page).objects.filter(translated_link=calling_page).count()>0:
 translated_version = type(calling_page).objects.filter(translated_link=calling_page).first().specific
 # else return nothing, dont bother looking through other models.
 translated_version = []
 if welsh_site(context):
 link_text = "View this page in English"
 link_text = "Darllenwch y dudalen hon yn y Gymraeg"
 return {
 'translated_version': translated_version,
 'link_text': link_text,
 # required by the pageurl tag that we want to use within this template
 'request': context['request'],

 except Exception as e:
 return {
 'translated_version': [],
 'link_text': "",
 # required by the pageurl tag that we want to use within this template
 'request': context['request'],

And the HTML bit  (translated_link.html) :

{% load yourapp_tags wagtailcore_tags %}

{% if translated_version %}

 <div class="container-translate">
 <div class="container">
 <div class="row">
 <div class="col-md-12">
 <a href="{% pageurl translated_version %}">{{ link_text }}</a>

{% endif %}

Basically this will look to see if the calling page already has a link to the translated version, if so just use that, but if not check if a page in the tree has a link pointing back to our calling page, if so use that. It could look through all the types of pages, but there might be too many database calls depending on how many page types you’ve setup. So if you’re linking two different page types just link both directions.

This Inclusion tag also needs a function called welsh_site which returns true or false.  It uses this to know if you’re in the Welsh site or not, to then pass back the link title in the correct language.  It’s also used from the template too but more on that in a bit. Here’s the code :

def welsh_site(context):
 request = context['request']
 if "/cymraeg" in request.get_full_path():
 return True
 return False

Place this in your base template where you’d like the ‘View this page in’ link to appear (remember to load your :

{% view_this_page_in calling_page=self %}

The last thing you need to do is to make any elements in the templates/includes change based on if they’re being used in the Welsh site or not, so for example in my footer include, I use the assignment tag again to change the footer statement.

 {% welsh_site as welsh %}
 {% if welsh %}
 <p class="reg">&copy; Prifysgol De Cymru. Mae Prifysgol De Cymru yn elusen gofrestredig. Rhif yr elusen 1140312.</p>
 {% else %}
 <p class="reg">&copy; University of South Wales. The University of South Wales is a registered charity. Registration No.1140312</p>
 {% endif %}

If your site generates it’s top navigation from the pages in the tree, like the Demo wagtail site, you’ll need to modify the inclusion tag responsible for the top nav items. You’ll only want the main Welsh pages as your navigation if you’re in the Welsh site. So basically if you’re in the welsh site, and a welsh homepage exists, then create an instance of the Welsh homepage, then get it’s children. If you’re not in the Welsh site then use the original wagtaildemo bit.

@register.inclusion_tag('yourapp/tags/top_menu.html', takes_context=True)
def top_menu(context, parent, calling_page=None):

 if welsh_site(context) and has_welsh_homepage():
 welsh_home = welsh_homepage()
 menuitems = welsh_home.get_children().filter(live=True,show_in_menus=True)
 menuitems = parent.get_children().filter(live=True,show_in_menus=True)

 for menuitem in menuitems:
 menuitem.show_dropdown = has_menu_children(menuitem)
 return {
 'calling_page': calling_page,
 'menuitems': menuitems,
 # required by the pageurl tag that we want to use within this template
 'request': context['request'],

This inclusion tag needs two other functions:

def has_welsh_homepage():
 if Page.objects.filter(slug='cymraeg').count() > 0:
 return True
 return False

def welsh_homepage():
 return Page.objects.filter(slug='cymraeg').first().specific
 return []

These functions require your second language home page (in our case Welsh) to have a set slug, ours is ‘cymraeg’.

We also made our footer use different types of snippets if the Welsh site’s being served by the template, and also our breadcrumbs checks if it’s being used in the Welsh site, so it can have a different first base crumb.

But basically it’s one set of templates and base that delivers both English and Welsh pages, through one site. It’s far from a perfect approach, but it’s flexible and was quick and easy to get going under a tight deadline.

Please let me know if you spot any problems with the code, or have any suggestions or comments.



Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.