Wagtail programmatically create page translation

Kamal Mustafa - Sep 30 - - Dev Community

I can't find any programmatic interface to create page translation. All the logics seems being implemented in SubmitTranslationView in wagtail.contrib.simple_translation.views.

So the only way to access those programmatically is by simulating the request to access the views. I wrapped this in a function named translate_page(). To translate a page, we can call this function as:-

page_ja = translate_page(user, page, "ja")
Enter fullscreen mode Exit fullscreen mode

Or we can pass the optional parameter include_subtree:-

page_ja = translate_page(user, page, "ja", include_subtree=True)
Enter fullscreen mode Exit fullscreen mode

And the function is defined as:-

def translate_page(user, page, lang, include_subtree=False):
    locale, created = Locale.objects.get_or_create(language_code=lang)
    data = {"locales": [locale.id], "include_subtree": include_subtree}
    url = reverse(
        "simple_translation:submit_page_translation", kwargs={"page_id": page.id}
    )
    factory = RequestFactory()
    request = factory.post(url)
    request.POST = data
    request.user = user
    get_response = lambda request: None
    middleware = SessionMiddleware(get_response)
    middleware.process_request(request)
    request.session.save()
    messages = FallbackStorage(request)
    setattr(request, "_messages", messages)

    SubmitPageTranslationView.model = type(page)
    SubmitPageTranslationView.as_view()(request, page_id=page.pk)
    page_translated = page.get_translations()[0].specific
    return page_translated
Enter fullscreen mode Exit fullscreen mode

I have another function called translate_snippet(). The only difference is just the url to submit and no include_subtree parameter.

Page tree gotchas

For all our sites, we have a setup script that will automatically populating some posts and their translation so that developer can start working right away instead of having to create sample pages manually.

Our page structure is like this:-

HomePage ==> BlogIndexPage ==> BlogPage
Enter fullscreen mode Exit fullscreen mode

And the setup script will do the following:-

  • Create the root admin user, let's call it user One.
  • Create instance of HomePage
  • Attach HomePage as new root page
  • Create Japanese translation of HomePage
  • Create instance of BlogIndexPage
  • Populate BlogPage with sample posts and attach them under BlogIndexPage
  • Translate BlogIndexPage with include_subtree=True

This all works well until we're setting new site where the page structure is:-

BlogIndexPage ==> BlogPage
Enter fullscreen mode Exit fullscreen mode

So we omitted HomePage (which I think a bad idea now but let's save it for another topic) since this site is mainly a blog. The first thing I notice after running the setup script is that no translation of the blog posts created, despite we already passed include_subtree when translating BlogIndexPage.

My first gut reaction was it could be some changes in new wagtail versions. Most our sites being created few years ago and still on wagtail 5 but for this new site, we will start with wagtail 6 since it's the latest.

But looking at wagtail's commit logs for simple_translation views.py, the code last changes was three years ago. And we can see the code basically the same between wagtail 5 and 6.

The problem with translate_page function above is that it doesn't check for any errors. Because catching errors mean you have to parse the request's response for some error string. But tracing the code flow lead me to a stage where I can see the code is not executed because the form is not validated.

Printing form.errors showed error messages related to invalid locale. This is strange because we can see in the translate_page function above we're creating the locale if it doesn't exists yet.

And printing the form's self.fields["locales"].choices I can the locale is there in the choice during first call of translate_page() when translating the root page, but the choices was empty when calling it second time to translate BlogIndexPage.

Reading the form's code, locales field's choices is set dynamically in the __init__ method, where locale of the page that already translated will be removed. This could be the reason why the locale was empty in the second call. But the page is not translated yet!

Let's recall back the process:-

  • Create BlogIndexPage
  • Attach BlogIndexPage to root page
  • Translate root page to ja
  • Populate BlogPage and attach them to BlogIndexPage
  • Translate BlogIndexPage to ja

And this is where the light bulb (💡) came in, after hours of debugging. In the original script, we're translating HomePage to ja first, and then BlogIndexPage with all its children. But in this new script, the root page is BlogIndexPage. So BlogIndexPage already translated the second time we call translate_page!

So that's the reason locales choices become empty.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .