Out of the box, Wagtail assumes all content will be authored in a single language. This document describes how to configure Wagtail for authoring content in multiple languages.
Note
Wagtail provides the infrastructure for creating and serving content in multiple languages. There are two options for managing translations across different languages in the admin interface: wagtail.contrib.simple_translation or the more advanced wagtail-localize (third-party package).
This document only covers the internationalisation of content managed by Wagtail. For information on how to translate static content in template files, JavaScript code, etc, refer to the Django internationalisation docs. Or, if you are building a headless site, refer to the docs of the frontend framework you are using.
This section provides an explanation of Wagtail’s internationalisation approach. If you’re in a hurry, you can skip to Configuration.
In summary:
Locale
model and all pages are linked to a Locale
with the locale
foreign key fieldtranslation_key
fieldi18n_patterns
and LocaleMiddleware
for language detectionWagtail stores content in a separate page tree for each locale.
For example, if you have two sites in two locales, then you will see four homepages at the top level of the page hierarchy in the explorer.
This approach has some advantages for the editor experience as well:
All pages (and any snippets that have translation enabled) have a locale
and translation_key
field:
locale
is a foreign key to the Locale
modeltranslation_key
is a UUID that’s used to find translations of a piece of content. Translations of the same page/snippet share the same value in this fieldThese two fields have a ‘unique together’ constraint so you can’t have more than one translation in the same locale.
When you set up a site in Wagtail, you select the site’s homepage in the ‘root page’ field and all requests to that site’s root URL will be routed to that page.
Multi-lingual sites have a separate homepage for each locale that exist as siblings in the page tree. Wagtail finds the other homepages by looking for translations of the site’s ‘root page’.
This means that to make a site available in another locale, you just need to translate and publish its homepage in that new locale.
If Wagtail can’t find a homepage that matches the user’s language, it will fall back to the page that is selected as the ‘root page’ on the site record, so you can use this field to specify the default language of your site.
For detecting the user’s language and adding a prefix to the URLs (/en/
, /fr-fr/
, for example), Wagtail is designed to work with Django’s built-in internationalisation utilities such as i18n_patterns
and LocaleMiddleware
. This means that Wagtail should work seamlessly with any other internationalised Django applications on your site.
The locales that are enabled on a site are recorded in the Locale
model in wagtailcore
. This model has just two fields: ID and language_code
which stores the BCP-47 language tag that represents this locale.
The locale records can be set up with an optional management UI or created in the shell. The possible values of the language_code
field are controlled by the WAGTAIL_CONTENT_LANGUAGES
setting.
Note
Read this if you’ve changed LANGUAGE_CODE
before enabling internationalisation
On initial migration, Wagtail creates a Locale
record for the language that was set in the LANGUAGE_CODE
setting at the time the migration was run. All pages will be assigned to this Locale
when Wagtail’s internationalisation is disabled.
If you have changed the LANGUAGE_CODE
setting since updating to Wagtail 2.11, you will need to manually update the record in the Locale
model too before enabling internationalisation, as your existing content will be assigned to the old code.
In this section, we will go through the minimum configuration required to enable content to be authored in multiple languages.
To enable internationalisation in both Django and Wagtail, set the following settings to True
:
# my_project/settings.py USE_I18N = True WAGTAIL_I18N_ENABLED = True
In addition, you might also want to enable Django’s localisation support. This will make dates and numbers display in the user’s local format:
# my_project/settings.py USE_L10N = True
Next we need to configure the available languages. There are two settings for this that are each used for different purposes:
LANGUAGES
- This sets which languages are available on the frontend of the site.WAGTAIL_CONTENT_LANGUAGES
- This sets which the languages Wagtail content can be authored in.You can set both of these settings to the exact same value. For example, to enable English, French, and Spanish:
# my_project/settings.py WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [ ('en', "English"), ('fr', "French"), ('es', "Spanish"), ]
Note
Whenever WAGTAIL_CONTENT_LANGUAGES
is changed, the Locale
model needs to be updated as well to match.
This can either be done with a data migration or with the optional locale management UI described in the next section.
You can also set these to different values. You might want to do this if you want to have some programmatic localisation (like date formatting or currency, for example) but use the same Wagtail content in multiple regions:
# my_project/settings.py LANGUAGES = [ ('en-GB', "English (Great Britain)"), ('en-US', "English (United States)"), ('en-CA', "English (Canada)"), ('fr-FR', "French (France)"), ('fr-CA', "French (Canada)"), ] WAGTAIL_CONTENT_LANGUAGES = [ ('en-GB', "English"), ('fr-FR', "French"), ]
When configured like this, the site will be available in all the different locales in the first list, but there will only be two language trees in Wagtail.
All the en-
locales will use the “English” language tree, and the fr-
locales will use the “French” language tree. The differences between each locale in a language would be programmatic. For example: which date/number format to use, and what currency to display prices in.
An optional locale management app exists to allow a Wagtail administrator to set up the locales from the Wagtail admin interface.
To enable it, add wagtail.locales
into INSTALLED_APPS
:
# my_project/settings.py INSTALLED_APPS = [ # ... 'wagtail.locales', # ... ]
To allow all of the page trees to be served at the same domain, we need to add a URL prefix for each language.
To implement this, we can use Django’s built-in i18n_patterns function, which adds a language prefix to all of the URL patterns passed into it. This activates the language code specified in the URL and Wagtail takes this into account when it decides how to route the request.
In your project’s urls.py
add Wagtail’s core URLs (and any other URLs you want to be translated) into an i18n_patterns
block:
# /my_project/urls.py # ... from django.conf.urls.i18n import i18n_patterns # Non-translatable URLs # Note: if you are using the Wagtail API or sitemaps, # these should not be added to `i18n_patterns` either urlpatterns = [ path('django-admin/', admin.site.urls), path('admin/', include(wagtailadmin_urls)), path('documents/', include(wagtaildocs_urls)), ] # Translatable URLs # These will be available under a language code prefix. For example /en/search/ urlpatterns += i18n_patterns( path('search/', search_views.search, name='search'), path("", include(wagtail_urls)), )
If you want your default language to have URLs that resolve normally without a language prefix, you can set the prefix_default_language
parameter of i18n_patterns
to False
. For example, if you have your languages configured like this:
# myproject/settings.py # ... LANGUAGE_CODE = 'en' WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [ ('en', "English"), ('fr', "French"), ] # ...
And your urls.py
configured like this:
# myproject/urls.py # ... # These URLs will be available under a language code prefix only for languages that # are not set as default in LANGUAGE_CODE. urlpatterns += i18n_patterns( path('search/', search_views.search, name='search'), path("", include(wagtail_urls)), prefix_default_language=False, )
Your URLs will now be prefixed only for the French version of your website, for example:
- /search/ - /fr/search/
After wrapping your URL patterns with i18n_patterns
, your site will now respond on URL prefixes. But now it won’t respond on the root path.
To fix this, we need to detect the user’s browser language and redirect them to the best language prefix. The recommended approach to do this is with Django’s LocaleMiddleware
:
# my_project/settings.py MIDDLEWARE = [ # ... 'django.middleware.locale.LocaleMiddleware', # ... ]
You don’t strictly have to use i18n_patterns
or LocaleMiddleware
for this and you can write your own logic if you need to.
All Wagtail needs is the language to be activated (using Django’s django.utils.translation.activate
function) before the wagtail.views.serve
view is called.
Perhaps the most important bit of internationalisation-related UI you can add to your site is a selector to allow users to switch between different languages.
If you’re not convinced that you need this, have a look at https://www.w3.org/International/questions/qa-site-conneg#yyyshortcomings for some rationale.
Here is a basic example of how to add links between translations of a page.
This example, however, will only include languages defined in WAGTAIL_CONTENT_LANGUAGES
and not any extra languages that might be defined in LANGUAGES
. For more information on what both of these settings mean, see Configuring available languages.
If both settings are set to the same value, this example should work well for you, otherwise skip to the next section that has a more complicated example which takes this into account.
{# make sure these are at the top of the file #} {% load i18n wagtailcore_tags %} {% if page %} {% for translation in page.get_translations.live %} {% get_language_info for translation.locale.language_code as lang %} <a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}"> {{ lang.name_local }} </a> {% endfor %} {% endif %}
Let’s break this down:
{% if page %} ... {% endif %}
If this is part of a shared base template it may be used in situations where no page object is available, such as 404 error responses, so check that we have a page before proceeding.
{% for translation in page.get_translations.live %} ... {% endfor %}
This for
block iterates through all published translations of the current page.
{% get_language_info for translation.locale.language_code as lang %}
This is a Django built-in tag that gets info about the language of the translation. For more information, see get_language_info() in the Django docs.
<a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}"> {{ lang.name_local }} </a>
This adds a link to the translation. We use {{ lang.name_local }}
to display the name of the locale in its own language. We also add rel
and hreflang
attributes to the <a>
tag for SEO.
For headless sites, the Wagtail API supports two extra filters for internationalised sites:
?locale=
Filters pages by the given locale?translation_of=
Filters pages to only include translations of the given page IDFor more information, see Special filters for internationalized sites.
You can make a snippet translatable by making it inherit from wagtail.models.TranslatableMixin
. For example:
# myapp/models.py from django.db import models from wagtail.models import TranslatableMixin from wagtail.snippets.models import register_snippet @register_snippet class Advert(TranslatableMixin, models.Model): name = models.CharField(max_length=255)
The TranslatableMixin
model adds the locale
and translation_key
fields to the model.
For snippets with existing data, it’s not possible to just add TranslatableMixin
, make a migration, and run it. This is because the locale
and translation_key
fields are both required and translation_key
needs a unique value for each instance.
To migrate the existing data properly, we firstly need to use BootstrapTranslatableMixin
, which excludes these constraints, then add a data migration to set the two fields, then switch to TranslatableMixin
.
This is only needed if there are records in the database. So if the model is empty, you can go straight to adding TranslatableMixin
and skip this.
BootstrapTranslatableMixin
to the modelThis will add the two fields without any constraints:
# myapp/models.py from django.db import models from wagtail.models import BootstrapTranslatableMixin from wagtail.snippets.models import register_snippet @register_snippet class Advert(BootstrapTranslatableMixin, models.Model): name = models.CharField(max_length=255) # if the model has a Meta class, ensure it inherits from # BootstrapTranslatableMixin.Meta too class Meta(BootstrapTranslatableMixin.Meta): verbose_name = 'adverts'
Run python manage.py makemigrations myapp
to generate the schema migration.
Create a data migration with the following command:
python manage.py makemigrations myapp --empty
This will generate a new empty migration in the app’s migrations
folder. Edit that migration and add a BootstrapTranslatableModel
for each model to bootstrap in that app:
from django.db import migrations from wagtail.models import BootstrapTranslatableModel class Migration(migrations.Migration): dependencies = [ ('myapp', '0002_bootstraptranslations'), ] # Add one operation for each model to bootstrap here # Note: Only include models that are in the same app! operations = [ BootstrapTranslatableModel('myapp.Advert'), ]
Repeat this for any other apps that contain a model to be bootstrapped.
BootstrapTranslatableMixin
to TranslatableMixin
Now that we have a migration that fills in the required fields, we can swap out BootstrapTranslatableMixin
for TranslatableMixin
that has all the constraints:
# myapp/models.py from wagtail.models import TranslatableMixin # Change this line @register_snippet class Advert(TranslatableMixin, models.Model): # Change this line name = models.CharField(max_length=255) class Meta(TranslatableMixin.Meta): # Change this line, if present verbose_name = 'adverts'
makemigrations
to generate schema migrations, then migrate!Run makemigrations
to generate the schema migration that adds the constraints into the database, then run migrate
to run all of the migrations:
python manage.py makemigrations myapp python manage.py migrate
When prompted to select a fix for the nullable field ‘locale’ being changed to non-nullable, select the option “Ignore for now” (as this has been handled by the data migration).
As mentioned at the beginning, Wagtail does supply wagtail.contrib.simple_translation
.
The simple_translation module provides a user interface that allows users to copy pages and translatable snippets into another language.
Content editors need to translate the content and publish the pages.
To enable add "wagtail.contrib.simple_translation"
to INSTALLED_APPS
and run python manage.py migrate
to create the submit_translation
permissions. In the Wagtail admin, go to settings and give some users or groups the “Can submit translations” permission.
Note
Simple Translation is optional. It can be switched out by third-party packages. Like the more advanced wagtail-localize.
As part of the initial work on implementing internationalisation for Wagtail core, we also created a translation package called wagtail-localize
. This supports translating pages within Wagtail, using PO files, machine translation, and external integration with translation services.
Before official multi-language support was added into Wagtail, site implementors had to use external plugins. These have not been replaced by Wagtail’s own implementation as they use slightly different approaches, one of them might fit your use case better:
For a comparison of these options, see AccordBox’s blog post How to support multi-language in Wagtail CMS.
The Wagtail admin backend has been translated into many different languages. You can find a list of currently available translations on Wagtail’s Transifex page. (Note: if you’re using an old version of Wagtail, this page may not accurately reflect what languages you have available).
If your language isn’t listed on that page, you can easily contribute new languages or correct mistakes. Sign up and submit changes to Transifex. Translation updates are typically merged into an official release within one month of being submitted.
Logged-in users can set their preferred language from /admin/account/
. By default, Wagtail provides a list of languages that have a >= 90% translation coverage. It is possible to override this list via the WAGTAILADMIN_PERMITTED_LANGUAGES setting.
In case there is zero or one language permitted, the form will be hidden.
If there is no language selected by the user, the LANGUAGE_CODE
will be used.
The default language of Wagtail is en-us
(American English). You can change this by tweaking a couple of Django settings:
True
If there is a translation available for your language, the Wagtail admin backend should now be in the language you’ve chosen.
© 2014-present Torchbox Ltd and individual contributors.
All rights are reserved.
Licensed under the BSD License.
https://docs.wagtail.org/en/stable/advanced_topics/i18n.html