From bdfee22e8b180a7fad3b3122436c84570d2da790 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 16 Oct 2020 13:36:19 +0100 Subject: [PATCH] New topic docs for internationalisation (#6436) * New topic docs for internationalisation * Spelling + Grammar fixes * Clarity fixes * Fix formatting issues --- docs/advanced_topics/i18n.rst | 588 +++++++++++++++++++++++++++++++++- 1 file changed, 578 insertions(+), 10 deletions(-) diff --git a/docs/advanced_topics/i18n.rst b/docs/advanced_topics/i18n.rst index 55e8441d473..986c6a8785f 100644 --- a/docs/advanced_topics/i18n.rst +++ b/docs/advanced_topics/i18n.rst @@ -2,22 +2,590 @@ Internationalisation ==================== +.. contents:: + :local: + :depth: 3 + Multi-language content ====================== -In its basic configuration, Wagtail does not provide specific support for multi-language content. This is because there is no single preferred approach to handling translations that works for all scenarios - various approaches are possible, depending on factors such as: +.. 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 and, optionally, set up a workflow that allows users to +translate content between languages. + +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. + +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 `_ +that represents this locale. + +The locale records can be set up with an :ref:`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. + +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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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"), + ] + +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_internationalisation: + +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 `_ +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 %} + + {% for translation in page.get_translations.live %} + {% get_language_info for translation.locale.language_code as lang %} + + {{ lang.name_local }} + + {% endfor %} + +Let's break this down: + +.. 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 `_. + +.. code-block:: html+Django + + + {{ lang.name_local }} + + +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 ```` 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 `_ +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 %} + + {{ lang.name_local }} + + {% 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 + + + {{ lang.name_local }} + + +The only difference with the ```` tag here from the ```` 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. + +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) + +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_localize.bootstrap 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) + +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 --empty + python manage.py migrate + +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. -* The number of languages you intend to support -* Whether the available site content can differ from one language to another, or is the same for all languages -* Whether the type of content you're working with is most naturally modelled as one language per item (e.g. a blog post), or multiple languages per item (e.g. descriptions of a product in a web store) +Github: https://github.com/wagtail/wagtail-localize -Several add-on packages for Wagtail providing multi-language support are available: +Alternative internationalisation plugins +======================================== -* `Wagtailtrans `_ -* `wagtail-modeltranslation `_ +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 `_. +- `Wagtailtrans `_ +- `wagtail-modeltranslation `_ +For a comparison of these options, see AccordBox's blog post +`How to support multi-language in Wagtail CMS `_. Wagtail admin translations ========================== @@ -26,7 +594,7 @@ The Wagtail admin backend has been translated into many different languages. You 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. -Change Wagtail admin language on a per user basis +Change Wagtail admin language on a per-user basis ================================================= Logged-in users can set their preferred language from ``/admin/account/``. @@ -35,7 +603,7 @@ It is possible to override this list via the :ref:`WAGTAILADMIN_PERMITTED_LANGUA 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`` wil be used. +If there is no language selected by the user, the ``LANGUAGE_CODE`` will be used. Changing the primary language of your Wagtail installation