Christian Haintz
Co-founder and CTO of Carrot & Company. Writes about tech and business topics.
37 Minutes

Migrate your Wagtail Website from wagtailtrans to the new wagtail-localize

37 Minutes
Article posted on January 10, 2021
How to migrate your multi-language wagtail website from wagtailtrans to the new native wagtail translation system introduced in version 2.11 and wagtail-localize, the new translation frontend.

This article describes one migration strategy on how to upgrade existing Wagtail websites (version <2.11) with wagtailtrans to the new version 2.11 and how to migrate the translations from wagtailtrans to the new internal translation system by wagtail. Because the internal translation system doesn't come with a frontend we will also install wagtail-localize , a modern translation frontend which can automatically translate your existing content using (for example) the DeepL translator.

In this article we will focus on migrating existing content to the new system, adding wagtail -localize and removing wagtailtrans from your wagtail system.

Disclamer : Make sure you have a backup of your DB and Code. And we advise you to test the complete migration steps in a development environment beforehand. Some parts may vary depending on how you integrated wagtailtrans to your system. This approach has worked for our setup but might need adaption for yours.

The approach

The idea is based on Karl Hobley's suggestion from the Github Issue #297 . We needed to adjust it.

  1. Backup your DB and code.
  2. Install the migration fork from Github
  3. Upgrade wagtail to 2.11+ and install wagtail-localize
  4. Migrate the database schemas and data
  5. Remove wagtailtrans

Preconditions

The existing setup which needs to be upgraded in this example is wagtail version 2.10.2 and wagtailtrans version 2.2.1 with django 3.1.2. We assume that we have a running website with wagtailtrans configured and working.

Make a Backup of your DB and code

You probably already have a decent backup strategy and your code in a version system, so we are done with that step. If you don't have one we can recommend Django DB Backup and git for source code versioning.

Install the migration fork of wagtailtrans

To be able to upgrade to wagtail 2.11 we need to install a forked version of wagtailtrans (based on 2.2.1). We do this because some of the features of wagtailtrans are now done natively in wagtail. Therefore we need to patch some code to make them working in parallel so that we can do the migration.

pip git+https://github.com/carrotandcompany/wagtailtrans@master#egg=wagtailtrans

Install the new wagtail and wagtail-localize

After we have installed the forked version we can upgrade wagtail and wagtailtrans. For that change requirements.txt

wagtail==2.11.3
# wagtailtrans==2.2.1  <-- remove this line as we manually installed the forked version for migration
wagtail-localize==0.9.4

Install it

pip install -r requirements.txt

Setup the internationalization with wagtail

This is based on the Wagtail docs and wagtail-localize Readme .

Changes to the django settings.py

We need to verify some settings in order to get wagtail to activate and use the internal translation system

LANGUAGE_CODE = 'en'

Add wagtail_localize.

INSTALLED_APPS = [
    ....
    'wagtailtrans' # leave it in for now, as we need it for migration
    'wagtail_localize',
    'wagtail_localize.locales'
]

Replace the wagtailtrans middleware with the wagtail one.

MIDDLEWARE = [
    ...,
    # 'wagtailtrans.middleware.TranslationMiddleware',  # <-- remove this line 
    'django.middleware.locale.LocaleMiddleware', #  <-- add this line instead 
    ...,
]

Activate i18n with similar settings.

USE_I18N = True
USE_L10N = True
WAGTAIL_I18N_ENABLED = True
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
    ('en', "English"),
    ('de', "German"),
]

Changes to urls.py

As wagtail now uses django's own i18 system we need to embed the wagtail urls in a i18_patterns block. This is based on the wagtail-localize Readme

from django.conf.urls.i18n import i18n_patterns
...

urlpatterns += i18n_patterns(
    ...,
    url(r"", include(wagtail_urls)),
)

Migrate db

After installing and the setup we need to execute the migrations of the installed packages. In the forked version of wagtailtrans there is one additional data migration that:

  • migrates wagtailtrans language model --> wagtail Local model
  • sets wagtail new translation_key and locale page property based on the wagtailtrans data

If you want to check out what the migration does in detail, have a look at 0010_migrate_info_to_wagtail.py

To actually run the migrations after installing the wagtailtrans migration fork execute

python manage.py migrate

Move your translation sites

Wagtailtrans uses the approach of a root page for each multilingual site you want to have. The approach in wagtail is different. All your site's languages are directly below the wagtail root page. This means, we need to move all language subpages from the translation root page to the wagtail root.

Change the site root site

This is done in the Wagtail admin 'Settings' --> 'Sites'. Edit your sites and change the root site to your previously moved sites (choose the default language version).

Remove the wagtailtrans Translation Root Page.

In the Wagtail admin you should now see your moved sites and the Translation Root Sites. They shouldn't have any children pages left, as we moved them all to root previously. Now we can delete all the empty Translation Root Sites. You can recognize them by the type which is Translatable site root page .

At this stage your website should be working again. Only one thing is missing: cleanly removing wagtailtrans from our system.

Cleanly remove Wagtailtrans

Wagtailtrans is usually very deeply integrated with our website because every translated custom page inherits from the 'TranslatablePage'. To cleanly remove this relation we need to to the following steps:

  1. Add Page as additional Parent model to all your custom page models which inherits from TranslatablePage
  2. Make the migration file for that change
  3. Modify the migration
  4. Run the migration
  5. Remove TranslatablePage from all your custom page models.
  6. Make migrations and migrate again
  7. Remove wagtailtrans from old migrations
  8. Remove from source
  9. Drop the wagtailtrans tables

Add Page model as Parent

To all our model pages which inherit from the TranslatablePage we need to add an additional parent Page .

YourPage(TranslatablePage, Page):
    ...

Generate DB Migration

We are now generating the db migrations, and because there is no default value for page_ptr we need to set one. We set it to -1 (no worries we change it in the next step)

python manage.py makemigrations

You are trying to add a non-nullable field 'page_ptr' to flexpage without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> -1
Migrations for 'website':
  webapp/website/migrations/0002_yourpage_page_ptr.py
    - Add field page_ptr to yourpage

Modify the migration file

We need to modify the previously created migration file 0002_yourpage_page_ptr.py (the actual name might vary on your system) because the -1 value is simply wrong. What we really need there is the id of the parent page record (because of Django Table Inheritance). The id of that page is already stored, but in a different column translatable_page_ptr . So we need to edit the migration file to transfer that info to the page_ptr field.

We accomplish this by changing the migration file:

  1. Remove the default value of the AddField entry (you know, the -1 from last step.)
  2. Duplicate the AddField and rename the latest one to AlterField .
  3. Add blank=True and null=True arguments to the AddField Entry. Because we need the new column but don't have values yet for the page_ptr
  4. We need to make sure that we have the ids transfered from the old TranslatablePage to the new Page between the AddField and AlterField entries . So we add a RunPython entry

Here we provided the final modified migration script for one custom page called 'yourpage'. You need a similar migration for each of your custom pages.

def transfer_ids(apps, schema_editor):
    YourPage = apps.get_model('website', 'YourPage')
    for page in YourPage.objects.all():
        # copy the page pointer from wagtailtrans to the new wagtail page
        page.page_ptr = page.translatable_page_ptr
        page.save()


class Migration(migrations.Migration):

    dependencies = [
        ('wagtailcore', '0059_apply_collection_ordering'),
        ('website', '0001_gen'),
    ]

    operations = [
        migrations.AddField(
            model_name='yourpage',
            name='page_ptr',
            field=models.OneToOneField(auto_created=True, blank=True, null=True,
                                       on_delete=django.db.models.deletion.CASCADE,
                                       parent_link=False, to='wagtailcore.page'),
            preserve_default=False,
        ),
        migrations.RunPython(transfer_ids),
        migrations.AlterField(
            model_name='yourpage',
            name='page_ptr',
            field=models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='wagtailcore.page'),
            preserve_default=False,
        ),
    ]

Run the migration

Run the modified migration:

python manage.py migrate

Remove TranslatablePage

Now that we have migrated the db we can finally remove the TranslatablePage from the custom page classes

Change from:

YourPage(TranslatablePage, Page):
    ...

to the new model signature

YourPage(Page):
    ...

Don't forget to also remove the imports from wagtailtrans if you don't need them anymore.

Make migrations again

Now we need to make the migrations so that the translatable_page_ptr will get removed from the db table of our own page models:

python manage.py makemigrations

Then migrate the changes

python manage.py migrate

Remove wagtailtrans from the old migrations

wagtailtrans is part of the existing migration files of our custom page models. We need to remove them before we can deactivate the wagtailtransapp and uninstall it. There are different approaches to remove it from the migration files. We want to explain one possible solution which doesn't negatively affect old db states before wagtailtrans nor db versions with existing wagtailtrans migrations applied. For the first situation we mock wagtailtrans and for the latter our migrating logic applies.

To achieve this behavior lets have a look at a typical existing migration file which creates a custom page. (Untouched) This should look very similar to the following:

class Migration(migrations.Migration):

    initial = True

    dependencies = [
       ('wagtailtrans', '0009_create_initial_language'),
    ]

    operations = [
        migrations.CreateModel(
            name='YourPage',
            fields=[
                ('translatablepage_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailtrans.translatablepage')),
                ('body', wagtail.core.fields.StreamField([...])),
            ],
            options={
                'abstract': False,
            },
            bases=('wagtailtrans.translatablepage',),
        ),
    ]

Our approach is to remove all occurences of wagtailtrans in such migrations using the following steps.

  1. replace wagtailtrans dependencies with wagtailcore
  2. mock translatablepage_ptr fields with an IntegerField
  3. remove the bases dependent on translatablepage

Remove from dependencies

In the dependencies section we can remove wagtailtrans and make the migration dependent on a different previous migration. For example we can use the wagtail migration ('wagtailcore', '0059_apply_collection_ordering'),

dependencies = [
   ('wagtailtrans', '0009_create_initial_language'),
]

Mock translatablepage_ptr fields with an IntegerField

The next part with wagtailtrans used is the translatablepage_ptr in the CreateModel operation. We simply mock it with an IntegerField. Why this works? When we have this migration file already applied in a DB it won't run anymore so no existing translatablepage_ptrs are overwriten. When we don't have this migration applied we can be sure that we don't have translated content for these pages, so we can simply create an integer field which is blank, as it will be removed in the migration files we created beforehand right after this migration is executed.

...
operations = [
    migrations.RemoveField( # <-- this is were we are removing the translatablepage_ptr field again
        model_name='yourpage',
        name='translatablepage_ptr',
    ),
    migrations.AlterField(
        model_name='yourpage',
        name='page_ptr',
        field=models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page'),
    ),
]
...

This way providing just a mock field instead of the wagtailtrans is save.

Remove the bases dependent on translatablepage

The last part where we have a reference to wagtailtrans is the base class of our page. We can simply remove that inheritance as we don't need it because we will remove wagtailtrans and during the migration this inheritance isn't used anyway.

migrations.CreateModel(
            name='YourPage',
            fields=[
                ('translatablepage_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailtrans.translatablepage')),
                ('body', wagtail.core.fields.StreamField([...])),
            ],
            options={
                'abstract': False,
            },
            bases=('wagtailtrans.translatablepage',), # <-- simply remove this whole line 
        ),

Now there shouldn't be any reference to wagtailtrans left in our source code.

Remove wagtailtrans from source

This is a good time to scan through the source and remove/replace any reference to wagtailtrans. In the settings we can now savely remove wagtailtrans from the INSTALLED_APPS .

INSTALLED_APPS = [
    ....
    'wagtailtrans'   # <-- remove this line
    'wagtail_localize',
    'wagtail_localize.locales'
]

Uninstall wagtailtrans

Now that we migrated everything we can savely uninstall our migration version of wagtailtrans.

pip uninstall wagtailtrans

Drop the wagtailtrans tables

Finally we can remove the db tables of wagtailtrans. Drop the following tables in your prefered DB frontend.

drop table wagtailtrans_translatablepage;
drop table wagtailtrans_translatablesiterootpage;
drop table wagtailtrans_sitelanguages_other_languages;
drop table wagtailtrans_sitelanguages;
drop table wagtailtrans_language;

That's it. We are using wagtail also as part of our SaaS Development Kit Carrot Seed .

We use Cookies 🍪