In an increasingly connected world, your content needs to speak your audience's language — literally. Wagtail CMS provides a robust internationalization (i18n) system that lets you author, manage, and serve content in multiple languages from a single codebase. In this lesson, you'll learn how to enable multi-language support, configure locales, set up translation workflows, and build a language selector — transforming your Wagtail site into a truly global content platform.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Enable Wagtail's internationalization framework and configure available languages
  • Understand how Wagtail stores multi-lingual content using the Locale model and translation_key
  • Set up URL routing with Django's i18n_patterns and language prefixes
  • Create and manage translations of pages and snippets using Wagtail Localize
  • Build a language/region selector UI for site visitors
  • Filter content by locale via the Wagtail REST API for headless sites
  • Implement best practices for multi-language content governance and workflow

2. Why This Matters (Real-World Scenario)

Imagine your SaaS platform's knowledge base — tutorials, release notes, API docs — is available only in English. Your fastest-growing user segment is in Latin America, where Spanish and Portuguese speakers make up 60% of new signups. Manual copy-pasting into a separate site is error-prone, and maintaining two codebases is unsustainable.

Wagtail's i18n system solves this elegantly: a single Wagtail instance serves content in every locale, editors work in the admin panel with translation tools, and each language gets its own page tree with shared URL structure. A French editor sees only French pages; an English editor sees only English ones. Translations are linked by a shared UUID, so you always know which pages correspond. When a reader visits /es/tutorials/, Wagtail automatically serves the Spanish version. No separate deployments. No duplicated infrastructure. Just clean, maintainable multi-language content.

3. Core Concepts

3.1 What is Internationalization (i18n)?

Internationalization is the process of designing software so it can adapt to different languages and regions without code changes. In Wagtail, i18n covers three layers:

  • Content i18n — Your Wagtail pages and snippets exist in multiple languages
  • Interface i18n — The Wagtail admin panel itself can be translated (it ships with 50+ language packs)
  • Django i18n — Static template strings, date formats, and numbers use Django's built-in translation utilities

3.2 Wagtail's Approach to Multi-lingual Content

Wagtail stores content in a separate page tree for each locale. This is fundamentally different from a single tree where each page has multiple language fields. The separate-tree approach offers key advantages:

  • No default language — content can be authored in any language first, then translated to any other
  • Translations are separate pages, so they publish at different times (no blocking on a slow translator)
  • Editors can be granted permission to edit only specific locales
  • Each locale's tree can have different structure — French pages might have sections English ones lack

3.3 Key Database Fields: locale and translation_key

Every page and translatable snippet has two special fields:

  • locale — A foreign key to the Locale model (which stores a BCP-47 language tag like en, fr, es)
  • translation_key — A UUID shared across all translations of the same piece of content. English page A and its French translation share the same translation_key

The locale and translation_key fields have a unique together constraint, meaning you cannot create two pages with the same translation_key in the same locale.

3.4 Translated Homepages

When you create a Site in Wagtail, you select its homepage. For multi-lingual sites, each locale has its own homepage as a sibling in the page tree. Wagtail finds the correct homepage by looking for translations of the site's root page. If no matching locale homepage exists, Wagtail falls back to the site's configured root_page — so you always have a default language.

3.5 Language Detection and Routing

Wagtail relies on Django's standard i18n utilities: i18n_patterns and LocaleMiddleware. This means language detection works through URL prefixes (/en/, /fr/), browser Accept-Language headers, or custom logic — just like any Django i18n app. Wagtail seamlessly integrates with these because it reads the currently activated language from Django's framework.

4. Hands-On Practice — Step by Step

4.1 Enable Internationalization in Settings

Start by enabling Django and Wagtail i18n in your project's settings.py:

# my_project/settings.py

USE_I18N = True
WAGTAIL_I18N_ENABLED = True

# Optional: enable locale-aware number/date formatting
USE_L10N = True

4.2 Configure Available Languages

Define which languages your site supports. There are two settings with different purposes:

  • LANGUAGES — Controls which languages the frontend is available in
  • WAGTAIL_CONTENT_LANGUAGES — Controls which languages Wagtail content can be authored in

For most sites, set both to the same value:

# my_project/settings.py

WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
    ('en', 'English'),
    ('fr', 'French'),
    ('es', 'Spanish'),
]

You can also decouple them. For example, you might have four frontend locales (en-GB, en-US, fr-FR, fr-CA) but only two content trees (English and French):

# my_project/settings.py

LANGUAGES = [
    ('en-GB', 'English (Great Britain)'),
    ('en-US', 'English (United States)'),
    ('fr-FR', 'French (France)'),
    ('fr-CA', 'French (Canada)'),
]

WAGTAIL_CONTENT_LANGUAGES = [
    ('en-GB', 'English'),
    ('fr-FR', 'French'),
]

With this setup, all en-* locales share the English content tree, and fr-* share the French tree. Locale-specific differences (date format, currency) are handled programmatically by Django.

4.3 Enable the Locale Management UI (Optional)

Add wagtail.locales to your INSTALLED_APPS so admins can manage locales directly from the Wagtail admin panel:

# my_project/settings.py

INSTALLED_APPS = [
    # ...
    'wagtail.locales',
    # ...
]

4.4 Add Language Prefixes to URLs

Use Django's i18n_patterns() to add language code prefixes (/en/, /fr/) to your Wagtail URLs. Non-translatable URLs (admin, documents, API) stay outside the i18n_patterns block:

# my_project/urls.py

from django.conf.urls.i18n import i18n_patterns
from django.urls import include, path

urlpatterns = [
    path('django-admin/', admin.site.urls),
    path('admin/', include(wagtailadmin_urls)),
    path('documents/', include(wagtaildocs_urls)),
]

urlpatterns += i18n_patterns(
    path('search/', search_views.search, name='search'),
    path('', include(wagtail_urls)),
)

To remove the language prefix for your default language, set prefix_default_language=False:

urlpatterns += i18n_patterns(
    path('', include(wagtail_urls)),
    prefix_default_language=False,
)

Now https://example.com/about/ serves the default language, while https://example.com/fr/about/ serves the French version.

4.5 Set Up Translation Workflow with Wagtail Localize

Wagtail provides two translation options:

  • wagtail.contrib.simple_translation — Built-in, copies pages and uses Django's machine translation hooks
  • wagtail-localize — Third-party package, supports segment-level translations, sync, and human translation workflows

Install wagtail-localize for production use:

pip install wagtail-localize

Add it to INSTALLED_APPS:

# my_project/settings.py

INSTALLED_APPS = [
    # ...
    'wagtail.locales',
    'wagtail_localize',
    'wagtail_localize.locales',  # Optional: enhanced locale management
    # ...
]

Run migrations and create your locales. With wagtail-localize, editors can:

  • Submit pages for translation from the Wagtail admin
  • See a side-by-side translation editor with segment-level source text
  • Sync translations when the source page is updated (only changed segments need retranslation)
  • Use machine translation providers (DeepL, Google Translate) as a starting point

4.6 Build a Language Selector

A language selector lets visitors switch between locales. Wagtail provides the locale property on pages and a Locale.get_active() helper. Here's a Django template snippet for a language switcher:

{% load wagtailcore_tags %}
<ul class="language-selector">
  {% for locale in page.get_translations.live %}
    <li>
      <a href="{% pageurl locale %}" hreflang="{{ locale.locale.language_code }}">
        {{ locale.locale.language_code|language_name_local }}
      </a>
    </li>
  {% endfor %}
</ul>

This iterates over all live translations of the current page and renders links in each language. The language_name_local filter displays the language name in its own script (e.g., Español, Français).

For a site-wide selector (not tied to the current page), use the homepage translations:

{% load wagtailcore_tags %}
<ul class="language-selector">
  {% for locale in page.get_site.root_page.get_translations.live %}
    <li>
      <a href="{% pageurl locale %}" hreflang="{{ locale.locale.language_code }}">
        {{ locale.locale.language_code|language_name_local }}
      </a>
    </li>
  {% endfor %}
</ul>

4.7 API Filters for Headless Multi-language Sites

If you're using Wagtail as a headless CMS, the REST API supports locale filtering. Filter pages by language code:

kubectl get pods -w
$ curl https://example.com/api/v2/pages/?locale=fr

{
"meta": {"total_count": 12},
"items": [
{
"id": 42,
"locale": {
"language_code": "fr"
},
"title": "À propos"
}
]
}
Filtering the Wagtail API by locale — returns only French pages

You can also search for translations of a specific page using the translation_of filter:

# Get all translations of page ID 10
curl https://example.com/api/v2/pages/?translation_of=10

5. Common Errors & Solutions

Error 1: Pages not showing in the correct locale

If a page tree is serving content in the wrong language, verify that your i18n_patterns is configured correctly and that WAGTAIL_I18N_ENABLED = True. Also check that each page has the correct locale assigned in the database.

Error 2: 404 errors after adding language prefixes

All Wagtail URLs must be inside the i18n_patterns() block. If include(wagtail_urls) is outside it, requests to /en/about/ won't match and will return 404. Move Wagtail's URL patterns inside i18n_patterns.

Error 3: Translation submission fails silently

Ensure wagtail.locales is in INSTALLED_APPS and migrations have been run. The Locale model must contain entries matching all values in WAGTAIL_CONTENT_LANGUAGES. Missing locale records cause translation submission errors in the admin.

Error 4: Wagtail admin language doesn't change

Wagtail admin uses its own language settings. Users can set their preferred admin language at /admin/account/. For a site-wide admin language change, set WAGTAILADMIN_NOTIFICATION_LANGUAGE_CODE and LANGUAGE_CODE in settings.

Error 5: LANGUAGES or WAGTAIL_CONTENT_LANGUAGES change not reflected

Locale records in the database must match WAGTAIL_CONTENT_LANGUAGES. If you add a new language code to the settings, also create the corresponding Locale record — either through the locale management UI or with a data migration. Run python manage.py shell and create missing locales:

from wagtail.models import Locale
from django.conf import settings

for code, name in settings.WAGTAIL_CONTENT_LANGUAGES:
    Locale.objects.get_or_create(language_code=code)

6. Summary Checklist

  • Enabled USE_I18N = True and WAGTAIL_I18N_ENABLED = True in settings.py
  • Configured LANGUAGES and WAGTAIL_CONTENT_LANGUAGES with your target locales
  • Added wagtail.locales to INSTALLED_APPS for locale management UI
  • Updated urls.py with i18n_patterns and language URL prefixes
  • Installed and configured wagtail-localize for production translation workflows
  • Built a language selector template using get_translations and pageurl
  • Tested API locale filters for headless frontend consumption
  • Created Locale records via the admin panel or shell for each enabled language
  • Verified default language routing works with and without URL prefixes

7. Practice Exercise

Challenge: Build a bilingual documentation site

Take the following steps to complete this exercise:

  1. Create a new Wagtail project or use an existing one
  2. Enable i18n and configure English (en) and German (de) as content languages
  3. Set up i18n_patterns so English URLs have no prefix and German URLs use /de/
  4. Create an English homepage with an About child page containing at least two StreamField sections
  5. Using Wagtail's admin translation tools, submit the About page for translation to German
  6. In the German translation, replace one paragraph with region-specific content (e.g., local contact information)
  7. Build a language selector that lives in the site header and shows both languages
  8. Verify that /about/ serves English, /de/about/ serves German, and non-default pages return 404 for missing locales

Bonus challenge: Add a third locale (ja for Japanese) and verify that the API correctly filters by ?locale=ja while also supporting ?translation_of= queries.

8. Next Steps

Congratulations! You've transformed your Wagtail site from a single-language CMS into a global content platform. Your editors can now create and manage content across multiple locales, your site automatically serves the right language based on URL prefixes, and translation workflows keep everything synchronized.

What's next? In the next lesson, Wagtail Permissions & User Management: Roles, Groups, and Content Governance, you'll learn how to control who can create, edit, approve, and publish content — essential for teams where different editors manage different locales.

Further resources:

Common pitfalls to avoid:

  • Don't forget to create Locale database records when adding a new language to WAGTAIL_CONTENT_LANGUAGES
  • Avoid putting Wagtail admin URLs inside i18n_patterns — they must remain at fixed paths like /admin/
  • Test all locales in both the Wagtail admin and the frontend before going live
  • If using a headless frontend, ensure your frontend reads the locale field from the API response to set the correct HTML lang attribute

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen July 10, 2026

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment