Migrate your Wagtail Website from wagtailtrans to the new wagtail-localize
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.
- Backup your DB and code.
- Install the migration fork from Github
- Upgrade wagtail to 2.11+ and install wagtail-localize
- Migrate the database schemas and data
- 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
andlocale
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:
-
Add Page as additional Parent model to all your custom page models which inherits from
TranslatablePage
- Make the migration file for that change
- Modify the migration
- Run the migration
-
Remove
TranslatablePage
from all your custom page models. - Make migrations and migrate again
- Remove wagtailtrans from old migrations
- Remove from source
- 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:
-
Remove the default value of the
AddField
entry (you know, the -1 from last step.) -
Duplicate the
AddField
and rename the latest one toAlterField
. -
Add
blank=True
andnull=True
arguments to the AddField Entry. Because we need the new column but don't have values yet for thepage_ptr
-
We need to make sure that we have the ids transfered from the old
TranslatablePage
to the newPage
between theAddField
andAlterField
entries . So we add aRunPython
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.
- replace wagtailtrans dependencies with wagtailcore
-
mock
translatablepage_ptr
fields with an IntegerField -
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 .