123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669 |
- ====================
- Internationalisation
- ====================
- .. contents::
- :local:
- :depth: 3
- .. _multi_language_content:
- Multi-language content
- ======================
- .. versionadded:: 2.11
- Overview
- --------
- 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,
- but does not itself provide an admin interface for managing translations of the same content
- across different languages. For this, the `wagtail-localize <https://github.com/wagtail/wagtail-localize>`_
- app must be installed separately.
- 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 <https://docs.djangoproject.com/en/3.1/topics/i18n/translation/>`_.
- Or, if you are building a headless site, refer to the docs of the frontend framework you are using.
- Wagtail's approach to multi-lingual content
- -------------------------------------------
- This section provides an explanation of Wagtail's internationalisation approach.
- If you're in a hurry, you can skip to `Configuration`_.
- In summary:
- - Wagtail stores content in a separate page tree for each locale
- - It has a built-in ``Locale`` model and all pages are linked to a ``Locale`` with the ``locale`` foreign key field
- - It records which pages are translations of each other using a shared UUID stored in the ``translation_key`` field
- - It automatically routes requests through translations of the site's homepage
- - It uses Django's ``i18n_patterns`` and ``LocaleMiddleware`` for language detection
- Page structure
- ^^^^^^^^^^^^^^
- Wagtail 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:
- - There is no default language for editing, so content can be authored in any
- language and then translated to any other.
- - Translations of a page are separate pages so they can be published at
- different times.
- - Editors can be given permission to edit content in one locale and not others.
- How locales and translations are recorded in the database
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- All pages (and any snippets that have translation enabled) have a ``locale`` and
- ``translation_key`` field:
- - ``locale`` is a foreign key to the ``Locale`` model
- - ``translation_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 field
- These two fields have a 'unique together' constraint so you can't have more than
- one translation in the same locale.
- Translated homepages
- ^^^^^^^^^^^^^^^^^^^^
- 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.
- Language detection and routing
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 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
- builtin internationalisation utilities such as ``i18n_patterns`` and
- `LocaleMiddleware`. This means that Wagtail should work seamlessly with any
- other internationalised Django applications on your site.
- Locales
- ~~~~~~~
- 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 <https://en.wikipedia.org/wiki/IETF_language_tag>`_
- that represents this locale.
- The locale records can be set up with an :ref:`optional management UI <enabling_locale_management>` 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.
- Configuration
- -------------
- In this section, we will go through the minimum configuration required to enable
- content to be authored in multiple languages.
- .. contents::
- :local:
- :depth: 1
- .. _enabling_internationalisation:
- Enabling internationalisation
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- To enable internationalisation in both Django and Wagtail, set the following
- settings to ``True``:
- .. code-block:: python
- # 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:
- .. code-block:: python
- # my_project/settings.py
- USE_L10N = True
- Configuring available languages
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 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:
- .. code-block:: python
- # 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:
- .. code-block:: python
- # 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.
- .. _enabling_locale_management:
- Enabling the locale management UI (optional)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 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``:
- .. code-block:: python
- # my_project/settings.py
- INSTALLED_APPS = [
- # ...
- 'wagtail.locales',
- # ...
- ]
- Adding a language prefix to URLs
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 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 <https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#language-prefix-in-url-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:
- .. code-block:: python
- # /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)),
- )
- User language auto-detection
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 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``:
- .. code-block:: python
- # my_project/settings.py
- MIDDLEWARE = [
- # ...
- 'django.middleware.locale.LocaleMiddleware',
- # ...
- ]
- Custom routing/language detection
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 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.core.views.serve`` view is called.
- Recipes for internationalised sites
- -----------------------------------
- Language/region selector
- ^^^^^^^^^^^^^^^^^^^^^^^^
- 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.
- Basic example
- ~~~~~~~~~~~~~
- 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.
- .. code-block:: html+Django
- {# 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:
- .. code-block:: html+Django
- {% 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.
- .. code-block:: html+Django
- {% for translation in page.get_translations.live %}
- ...
- {% endfor %}
- This ``for`` block iterates through all published translations of the current page.
- .. code-block:: html+Django
- {% 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 <https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#django.utils.translation.get_language_info>`_.
- .. code-block:: html+Django
- <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.
- Handling locales that share content
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Rather than iterating over pages, this example iterates over all of the configured
- languages and finds the page for each one. This works better than the `Basic example`_
- above on sites that have extra Django ``LANGUAGES`` that share the same Wagtail content.
- For this example to work, you firstly need to add Django's
- `django.template.context_processors.i18n <https://docs.djangoproject.com/en/3.1/ref/templates/api/#django-template-context-processors-i18n>`_
- context processor to your ``TEMPLATES`` setting:
- .. code-block:: python
- # myproject/settings.py
- TEMPLATES = [
- {
- # ...
- 'OPTIONS': {
- 'context_processors': [
- # ...
- 'django.template.context_processors.i18n',
- ],
- },
- },
- ]
- Now for the example itself:
- .. code-block:: html+Django
- {% for language_code, language_name in LANGUAGES %}
- {% get_language_info for language_code as lang %}
- {% language language_code %}
- <a href="{% pageurl page.localized %}" rel="alternate" hreflang="{{ language_code }}">
- {{ lang.name_local }}
- </a>
- {% endlanguage %}
- {% endfor %}
- Let's break this down too:
- .. code-block:: html+Django
- {% for language_code, language_name in LANGUAGES %}
- ...
- {% endfor %}
- This ``for`` block iterates through all of the configured languages on the site.
- The ``LANGUAGES`` variable comes from the ``django.template.context_processors.i18n``
- context processor.
- .. code-block:: html+Django
- {% get_language_info for language_code as lang %}
- Does exactly the same as the previous example.
- .. code-block:: html+Django
- {% language language_code %}
- ...
- {% endlanguage %}
- This ``language`` tag comes from Django's ``i18n`` tag library. It changes the
- active language for just the code contained within it.
- .. code-block:: html+Django
- <a href="{% pageurl page.localized %}" rel="alternate" hreflang="{{ language_code }}">
- {{ lang.name_local }}
- </a>
- The only difference with the ``<a>`` tag here from the ``<a>`` tag in the previous example
- is how we're getting the page's URL: ``{% pageurl page.localized %}``.
- All page instances in Wagtail have a ``.localized`` attribute which fetches the translation
- of the page in the current active language. This is why we activated the language previously.
- Another difference here is that if the same translated page is shared in two locales, Wagtail
- will generate the correct URL for the page based on the current active locale. This is the
- key difference between this example and the previous one as the previous one can only get the
- URL of the page in its default locale.
- API filters for headless sites
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 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 ID
- For more information, see :ref:`apiv2_i18n_filters`.
- Translatable snippets
- ^^^^^^^^^^^^^^^^^^^^^
- You can make a snippet translatable by making it inherit from ``wagtail.core.models.TranslatableMixin``.
- For example:
- .. code-block:: python
- # myapp/models.py
- from django.db import models
- from wagtail.core.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.
- Making snippets with existing data translatable
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 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.
- Step 1: Add ``BootstrapTranslatableMixin`` to the model
- *******************************************************
- This will add the two fields without any constraints:
- .. code-block:: python
- # myapp/models.py
- from django.db import models
- from wagtail.core.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.
- Step 2: Create a data migration
- *******************************
- Create a data migration with the following command:
- .. code-block:: bash
- 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:
- .. code-block:: python
- from django.db import migrations
- from wagtail.core.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.
- Step 3: Change ``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:
- .. code-block:: python
- # myapp/models.py
- from wagtail.core.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'
- Step 4: Run ``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:
- .. code-block:: bash
- 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).
- Translation workflow
- --------------------
- As mentioned at the beginning, Wagtail does not supply any built-in user interface
- or external integration that provides a translation workflow. This has been left
- for third-party packages to solve.
- 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.
- Github: https://github.com/wagtail/wagtail-localize
- Alternative internationalisation plugins
- ========================================
- 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:
- - `Wagtailtrans <https://github.com/wagtail/wagtailtrans>`_
- - `wagtail-modeltranslation <https://github.com/infoportugal/wagtail-modeltranslation>`_
- For a comparison of these options, see AccordBox's blog post
- `How to support multi-language in Wagtail CMS <https://www.accordbox.com/blog/how-support-multi-language-wagtail-cms/>`_.
- Wagtail admin translations
- ==========================
- 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 <https://www.transifex.com/torchbox/wagtail/>`_. (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 <https://www.transifex.com/torchbox/wagtail/>`_. Translation updates are typically merged into an official release within one month of being submitted.
- Change Wagtail admin language on a per-user basis
- =================================================
- 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 :ref:`WAGTAILADMIN_PERMITTED_LANGUAGES <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.
- Changing the primary language of your Wagtail installation
- ==========================================================
- The default language of Wagtail is ``en-us`` (American English). You can change this by tweaking a couple of Django settings:
- - Make sure `USE_I18N <https://docs.djangoproject.com/en/stable/ref/settings/#use-i18n>`_ is set to ``True``
- - Set `LANGUAGE_CODE <https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LANGUAGE_CODE>`_ to your websites' primary language
- If there is a translation available for your language, the Wagtail admin backend should now be in the language you've chosen.
|