Wagtailtrans and editing page siblings

I'm currently working on the initial development phase of my latest project, a redesign and site migration for the charity Girls Not Brides.

The previous site is a pretty solid Wordpress build, and as it's multilingual (in English, French, and Spanish), it's using WPML for the translations.

Having been involved with a project recently that uses Wagtail Model Translation, we decided to go with the other major alternative for multilingual Wagtail sites, Wagtailtrans.

For me, this follows a better UX model of having an individual page per language (using synchronised page trees), rather than the model translation method of duplicating multiple fields on the same page.

It's also similar to the way that WPML deals with post translations - so that's a UX win, as the transition from Wordpress to Wagtail will retain some familiarity for the CMS users.

Nothing is perfect

There are some drawbacks to Wagtailtrans, notably its current inability to handle non-page items such as settings or snippets.

As such, these are being handled in the build by Django Model Translation (I would have used Wagtail Model Translation, but there's no option to disable its page translation handling, so that just causes a big fight with Wagtailtrans).

Another feature that's missing is the ability to edit translated sibling pages directly from the page editor. You can do so from the parent listing view using the Edit in action buttons, but these aren't available when editing a page directly.

"Edit in" buttons in the Wagtail page listing view. If the page isn't immediately visible in the listing, it can hard to find and edit sibling pages

"Edit in" buttons in the Wagtail page listing view. If the page isn't immediately visible in the listing, it can hard to find and edit sibling pages

This means finding and editing a sibling page can be a challenge, especially if the CMS user has not recently edited the page that they are looking for, as it won't necessarily be immediately visible in the parent page listing.

Wagtail hooks to rescue

We can, however, use the register_page_action_menu_item Wagtail hook combined with the Wagtailtrans helper function edit_in_language_items to add action buttons to the page editor itself.

The default page edit action button menu. Here's where we can add edit buttons for sibling languages

The default page edit action button menu. Here's where we can add edit buttons for sibling languages

This hook needs an instance of wagtail.admin.action_menu.ActionMenuItem returned from the function, so we'll need a separate class per language. Within that class, we'll need to:

  • Check the page has a language
  • Don't show if it's the current page language
  • Return the edit URL if we're showing the button

As we're going to need to do that for multiple languages, I created the helper functions get_is_shown and get_edit_url_for_lang so that they could be reused and keep everything DRY.

Here's how that shapes up for adding an Edit English action button to translated pages:

from wagtailtrans.wagtail_hooks import edit_in_language_items
from wagtail.admin.action_menu import ActionMenuItem
from wagtail.core import hooks 


def get_is_shown(context, lang):
    page = context['page']
    language = getattr(page, 'language', None)
    if not language:
        return False
    elif lang == language.code:
        return False
    return True


def get_edit_url_for_lang(context, lang):
    items = edit_in_language_items(context['page'], context['user_page_permissions'])
    for item in items:
            if self.lang_name == item.label:
                return item.url


class EditEnMenuItem(ActionMenuItem):
    name = 'action-edit-en'
    label = 'Edit English'
    icon_name = 'site'

    def get_url(self, request, context):
        return get_edit_url_for_lang(context, 'English')

    def is_shown(self, request, context):
        return get_is_shown(context, 'en')


@hooks.register('register_page_action_menu_item')
def register_en_menu_item():
    return EditEnMenuItem(order=0)

So that's not too bad.

We get our action button on the page which is the most important thing, but I'm sure you'll see how this is going to become very boilerplatey once we add more languages, with each language needing its own class and register function:

from wagtailtrans.wagtail_hooks import edit_in_language_items
from wagtail.admin.action_menu import ActionMenuItem
from wagtail.core import hooks


def get_edit_url_for_lang(context, lang):
    items = edit_in_language_items(context['page'], context['user_page_permissions'])
    for item in items:
            if self.lang_name == item.label:
                return item.url


def get_is_shown(context, lang):
    page = context['page']
    language = getattr(page, 'language')
    if not language:
        return False
    elif lang == language.code:
        return False
    return True


class EditEnMenuItem(ActionMenuItem):
    name = 'action-edit-en'
    label = 'Edit English'
    icon_name = 'site'

    def get_url(self, request, context):
        return get_edit_url_for_lang(context, 'English')

    def is_shown(self, request, context):
        return get_is_shown(context, 'en')


class EditFrMenuItem(ActionMenuItem):
    name = 'action-edit-fr'
    label = "Edit French"
    icon_name = 'site'

    def get_url(self, request, context):
        return get_edit_url_for_lang(context, 'French')

    def is_shown(self, request, context):
        return get_is_shown(context, 'fr')


class EditEsMenuItem(ActionMenuItem):
    name = 'action-edit-es'
    label = "Edit Spanish"
    icon_name = 'site'

    def get_url(self, request, context):
        return get_edit_url_for_lang(context, 'Spanish')

    def is_shown(self, request, context):
        return get_is_shown(context, 'es')


@hooks.register('register_page_action_menu_item')
def register_en_menu_item():
    return EditEnMenuItem(order=0)


@hooks.register('register_page_action_menu_item')
def register_fr_menu_item():
    return EditFrMenuItem(order=1)


@hooks.register('register_page_action_menu_item')
def register_es_menu_item():
    return EditEsMenuItem(order=2)

You gotta love a loop

So, let's refactor the above code to use dynamic classes, and keep everything nice and scalable.

Here's the finished version of the code:

from django.conf import settings
from wagtailtrans.wagtail_hooks import edit_in_language_items
from wagtail.admin.action_menu import ActionMenuItem
from wagtail.core.hooks import register


class EditLangMenuItem(ActionMenuItem):
    name = 'action-edit-lang'
    icon_name = 'site'
    lang_code = ''
    lang_name = ''

    def get_url(self, request, context):
        items = edit_in_language_items(context['page'], context['user_page_permissions'])
        for item in items:
            if self.lang_name == item.label:
                return item.url

    def is_shown(self, request, context):
        page = context['page']
        language = getattr(page, 'language', None)
        if not language:
            return False
        elif self.lang_code == language.code:
            return False
        return True


# dynamically create and regisetr action menu classes for each language
for i, item in enumerate(settings.LANGUAGES):
    cls = type(
        'EditLangMenuItem' + item[0],
        (EditLangMenuItem, ),
        {
            'lang_code': str(item[0]),
            'lang_name': str(item[1]),
            'label': 'Edit ' + str(item[1]),
            'order': i,
        }
    )
    register('register_page_action_menu_item', lambda cls=cls: cls())

And here's how our modified menu looks on an English version of a translated page:

Modified page editor action button menu, with additional sibling page edit buttons

Modified page editor action button menu, with additional sibling page edit buttons