Wagtail 国际化实战指南:多语言内容架构、配置全流程与源码解析
【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail
本文以 Wagtail 官方文档 docs/advanced_topics/i18n.md 为主线,系统讲解 Wagtail 多语言内容的完整落地方案:从Locale模型与translation_key的数据模型设计,到WAGTAIL_I18N_ENABLED、WAGTAIL_CONTENT_LANGUAGES等关键配置、i18n_patterns语言前缀路由,再到模板中的语言切换器、Headless API 过滤器、可翻译 Snippet 的四步迁移法,并深入 wagtail/models/i18n.py、wagtail/models/sites.py 等源码印证每个配置项背后的真实行为。读完后你将能够独立为一个 Django + Wagtail 项目完成多语言站点的配置、路由、模板与数据迁移。
1. 总体思路:每个语言一棵独立的页面树
Wagtail 默认假设所有内容只以单一语言编写。要为站点启用多语言,需要先理解 Wagtail 的国际化模型,官方文档总结为五点:
- Wagtail 为每个语言环境(locale)维护一棵独立的页面树;
- 内置
Locale模型,所有页面都通过locale外键字段关联到某个Locale; - 通过
translation_key字段存储的共享 UUID记录哪些页面互为翻译; - 通过站点首页(root page)的翻译版本自动路由请求;
- 语言检测基于 Django 的
i18n_patterns与LocaleMiddleware。
需要明确边界:本文档只覆盖Wagtail 管理的内容的国际化。模板、JavaScript 等静态内容的翻译应参考 Django 官方国际化文档;Headless 站点则参考前端框架自身的 i18n 方案。
在管理后台,这种“每语言一棵树”的架构有直接的用户体验收益:
- 编辑没有“默认语言”的束缚,任何语言都可以先写再翻译成其他语言;
- 同一页面的各语言版本是相互独立的页面,可以各自在任意时间发布;
- 可以按 locale 粒度给编辑者分配权限(例如只允许编辑法语树)。
1.1 数据库层面:locale 与 translation_key
所有页面(以及开启翻译的 snippet)都带有locale和translation_key两个字段:
locale是指向Locale模型的外键;translation_key是一个 UUID,同一内容的各语言版本共享同一个值,据此查询翻译。
这两个字段带有 unique-together 约束,保证同一语言环境下同一内容最多只有一个版本。这一点可以在源码中得到印证,TranslatableMixin的定义见 wagtail/models/i18n.py:
class TranslatableMixin(models.Model): translation_key = models.UUIDField(default=uuid.uuid4, editable=False) locale = models.ForeignKey( Locale, on_delete=models.PROTECT, related_name="+", editable=False, verbose_name=_("locale"), ) ... class Meta: abstract = True unique_together = [("translation_key", "locale")]值得注意的是源码中还有一套系统检查(check()方法,wagtail/models/i18n.py):如果你在自己的模型里移除了这个唯一约束,manage.py check会直接报错wagtailcore.E003,提示必须补回UniqueConstraint。从源码结构看,Wagtail 把这个约束视为多语言数据一致性的硬前提,而不只是数据库层面的可选项。
Locale模型本身极其简洁,见 wagtail/models/i18n.py:
class Locale(models.Model): language_code = models.CharField(max_length=100, unique=True) objects = LocaleManager() all_objects = models.Manager()language_code存储 BCP-47 语言标签(如en、fr-fr);- 默认的
objects管理器只返回仍然在WAGTAIL_CONTENT_LANGUAGES中登记的语言——把某个语言从配置中移除后,它会自动被“禁用”而不需要删除数据; - 另有一个
all_objects管理器供 Locale 管理界面使用,以便看到已被禁用的记录。
管理器上的get_for_language()方法(wagtail/models/i18n.py)会先调用get_supported_content_language_variant()做语言变体归一化再查询。这个函数定义在 wagtail/coreutils.py,注释明确说明它等价于 Django 的同名函数,但读取的是WAGTAIL_CONTENT_LANGUAGES而非 Django 的LANGUAGES——这正是“内容语言”与“界面/区域语言”可以分离的底层机制:比如fr-ca请求会尝试匹配回fr的内容树。
注意(改过
LANGUAGE_CODE的用户请阅读):首次迁移时,Wagtail 会按运行迁移那一刻LANGUAGE_CODE设置的语言创建一条Locale记录,国际化关闭期间所有页面都归属这条记录。如果你在启用国际化之前修改过LANGUAGE_CODE,必须先手动更新Locale表中的旧记录,否则已有内容会被挂到错误的语言代码下。
2. 配置多语言内容:最小必要步骤
2.1 开启国际化
在 Django 和 Wagtail 两侧同时打开开关:
# my_project/settings.py USE_I18N = True WAGTAIL_I18N_ENABLED = TrueWAGTAIL_I18N_ENABLED在源码中被大量getattr(settings, "WAGTAIL_I18N_ENABLED", False)式地读取,例如TranslatableMixin.localized_draft(wagtail/models/i18n.py)与站点路由逻辑(见下节),关闭时整个多语言机制静默降级为单语言行为。
2.2 配置可用语言:LANGUAGES 与 WAGTAIL_CONTENT_LANGUAGES 的分工
两个设置各司其职:
LANGUAGES—— 决定前端站点上可用哪些语言(URL 前缀、区域化格式等);WAGTAIL_CONTENT_LANGUAGES—— 决定 Wagtail 内容可以被编写成哪些语言(即管理后台中有哪些语言树)。
两者可以设为完全相同的值。例如启用英、法、西三语:
# my_project/settings.py WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [ ("en", "English"), ("fr", "French"), ("es", "Spanish"), ]注意:每次修改
WAGTAIL_CONTENT_LANGUAGES后,都必须同步更新Locale表。这可以通过数据迁移完成,也可以用下一节的 Locale 管理界面。
两者也可以设为不同的值——典型场景是需要做程序化本地化(日期格式、货币等)但内容树共享时:
# 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"), ]这样站点会在全部 5 个 locale 前缀下可用,但 Wagtail 里只有两棵语言树:所有en-前缀共享 “English” 树,所有fr-前缀共享 “French” 树,各 locale 之间的差异(日期/数字格式、显示货币)由程序化处理。这也解释了 2.2 节中get_supported_content_language_variant()按“语言码 → 通用变体”逐级回退的行为。
2.3 (可选)启用 Locale 管理界面
Wagtail 提供了一个可选的 Locale 管理应用,让管理员直接在后台增删语言,而不必写数据迁移。启用方式是把wagtail.locales加入INSTALLED_APPS:
# my_project/settings.py INSTALLED_APPS = [ # ... "wagtail.locales", # ... ]该应用位于 wagtail/locales/,核心实现是LocaleViewSet(wagtail/locales/views.py)与表单LocaleForm(wagtail/locales/forms.py),并注册了后台菜单项(wagtail/locales/wagtail_hooks.py)。
2.4 给 URL 加语言前缀
要让所有语言树服务在同一个域名下,需要为每种语言添加 URL 前缀。推荐直接使用 Django 内置的i18n_patterns(),它会给传入的路由批量加上语言前缀,并激活 URL 中的语言代码——Wagtail 在路由请求时会将此纳入考虑:
# /my_project/urls.py # ... from django.conf.urls.i18n import i18n_patterns # 非翻译 URL # 注意:如果你在用 Wagtail API 或 sitemap, # 这些 URL 同样不应加入 i18n_patterns urlpatterns = [ path("django-admin/", admin.site.urls), path("admin/", include(wagtailadmin_urls)), path("documents/", include(wagtaildocs_urls)), ] # 可翻译 URL # 这些 URL 会挂在语言代码前缀下,例如 /en/search/ urlpatterns += i18n_patterns( path("search/", search_views.search, name="search"), path("", include(wagtail_urls)), )注意文档中的提醒:Wagtail API 与 sitemaps 这类 URL 不应包进i18n_patterns。
默认语言绕过前缀
若希望默认语言的 URL 不带语言前缀(例如/search/而非/en/search/),把i18n_patterns的prefix_default_language参数设为False。假设配置为:
# myproject/settings.py LANGUAGE_CODE = "en" WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [ ("en", "English"), ("fr", "French"), ]# myproject/urls.py # ... # 只有非 LANGUAGE_CODE 默认的语言会带前缀 urlpatterns += i18n_patterns( path("search/", search_views.search, name="search"), path("", include(wagtail_urls)), prefix_default_language=False, )此时 URL 形态为:
- /search/ - /fr/search/2.5 自动检测用户语言:LocaleMiddleware
包上i18n_patterns后站点只在语言前缀路径下响应,根路径会 404。修复方式是检测浏览器语言并 302 重定向到最合适的语言前缀,Django 的LocaleMiddleware即为此设计:
# my_project/settings.py MIDDLEWARE = [ # ... "django.middleware.locale.LocaleMiddleware", # ... ]2.6 自定义路由/语言检测
i18n_patterns与LocaleMiddleware并非硬性要求,也可以自己实现路由逻辑。从源码看,Wagtail 对前端路由的唯一要求是:在调用wagtail.views.serve视图之前,用django.utils.translation.activate激活正确的语言。
这一机制在源码中的落点可以印证“翻译首页自动路由”的说法。Site.get_site_root_paths()(wagtail/models/sites.py)在启用WAGTAIL_I18N_ENABLED时,会对每个站点执行:
for root_page in site.root_page.get_translations(inclusive=True).select_related("locale"): result.append(SiteRootPath(site.id, root_page.url_path, site.root_url, root_page.locale.language_code))即:把 root page 的所有翻译版本都注册为站点根路径,每条记录带上各自的language_code。页面 URL 与语言树的匹配就是基于这张表(结果会被缓存 1 小时,key 由SITE_ROOT_PATHS_CACHE_KEY管理)。由此得到两个实践结论:
- 要让站点在某语言下可用,只需把首页翻译到该语言并发布;
- 如果 Wagtail 找不到与用户语言匹配的首页,会回退到 Site 记录中指定的 root page——所以这个字段实际上就是站点的“默认语言”声明。
3. 国际化站点模板食谱
3.1 语言/区域选择器
多语言站点最重要的 UI 之一是让用户能手动切换语言(W3C 关于站点连接性建议中解释了为什么“粘住用户选择”很重要,官方文档引用了相关说明)。
基础示例
下面是最简单的“页面翻译链接”写法,注意它只会列出WAGTAIL_CONTENT_LANGUAGES中定义的语言,不含LANGUAGES里多出来的区域语言:
{# 确保这两行在文件顶部 #} {% load wagtailcore_tags %} {% if page %} {% for translation in page.get_translations.live %} <a href="{% pageurl translation %}" rel="alternate" hreflang="{{ translation.locale.language_code }}"> {{ translation.locale.language_name_local }} </a> {% endfor %} {% endif %}逐段拆解:
{% if page %}:若这段代码放在共享基础模板中,可能遇到 404 等没有 page 对象的场景,先做防御;{% for translation in page.get_translations.live %}:遍历当前页面所有已发布的翻译。对应源码TranslatableMixin.get_translations()(wagtail/models/i18n.py),它按translation_key过滤同表记录,inclusive=False时排除自身;<a>标签:链接指向翻译页,{{ translation.locale.language_name_local }}以语言本身的名称显示(例如fr显示为français);同时加上rel="alternate"与hreflang属性利于 SEO。translation.locale就是上文介绍的Locale模型实例,language_name_local属性见 wagtail/models/i18n.py,底层调用 Django 的translation.get_language_info()。
也可以改用 Django 内置的{% get_language_info %}标签获取语言信息:
{% load i18n %} {% get_language_info for translation.locale.language_code as lang %}处理共享内容的多 locale 站点
对于LANGUAGES中存在多个 locale 共享同一棵内容树的站点(2.2 节的差异化配置),遍历页面翻译的方式不够用。更好的做法是遍历配置的语言列表,为每个语言找到对应页面。首先需要把 Django 的 i18n 上下文处理器加入TEMPLATES:
# myproject/settings.py TEMPLATES = [ { # ... "OPTIONS": { "context_processors": [ # ... "django.template.context_processors.i18n", ], }, }, ]然后模板这样写:
{% 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 %}拆解:
LANGUAGES变量来自刚添加的django.template.context_processors.i18n上下文处理器;{% language language_code %}...{% endlanguage %}(来自i18n标签库)只在该代码块内临时激活指定语言;- 关键差异在
{% pageurl page.localized %}:Wagtail 的每个页面实例都有.localized属性,返回“当前激活语言”下该页面的翻译版本——所以要先激活语言。源码中localized属性(wagtail/models/i18n.py)的行为细节值得注意:它先取localized_draft,若页面实现了DraftStateMixin且取到的翻译未发布,则回退返回自身;而localized_draft在WAGTAIL_I18N_ENABLED未开启、或找不到对应 locale 时同样返回self。 - 当同一翻译页被多个 locale 共享时,Wagtail 会依据当前激活的 locale 生成正确的 URL(例如
en-GB与en-US前缀各生成各的),这正是它与基础示例的本质区别——基础示例只能取到页面在“默认 locale”下的 URL。
3.2 Headless 站点的 API 过滤器
对于 Headless 架构,Wagtail API 为国际化站点提供两个额外过滤参数:
?locale=—— 按指定 locale 过滤页面;?translation_of=—— 只返回某个页面 ID 的翻译。
两者在 wagtail/api/v2/filters.py 中实现:TranslationOfFilter还支持translation_of=root这种特殊取值(返回根页面各语言的翻译);LocaleFilter则通过get_object_or_404(Locale, language_code=...)将查询参数解析为Locale实例后再过滤 queryset。
4. 可翻译 Snippet
让 snippet 支持翻译只需让它继承wagtail.models.TranslatableMixin:
# 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)TranslatableMixin会为模型添加locale与translation_key两个字段,并自动获得get_translations()、localized、copy_for_translation()等能力。另外从源码可以看到一个信号处理器set_locale_on_new_instance(wagtail/models/i18n.py):新实例保存时若未指定 locale,会自动赋值为默认 locale(若模型通过 ParentalKey 挂在另一个可翻译模型下,则继承父对象的 locale)。
4.1 为已有数据的 Snippet 开启翻译
如果 snippet 表里已有数据,不能直接加TranslatableMixin然后跑迁移——因为locale与translation_key都是必填的,且每条记录的translation_key必须唯一。正确流程是四步:
第 1 步:给模型加BootstrapTranslatableMixin
它添加两个字段但不带约束(对应源码 wagtail/models/i18n.py,两个字段均允许为空且无 unique_together):
# 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) # 如果模型有 Meta 类,确保它同样继承 BootstrapTranslatableMixin.Meta class Meta(BootstrapTranslatableMixin.Meta): verbose_name = "adverts"运行python manage.py makemigrations myapp生成结构迁移。
第 2 步:创建数据迁移
python manage.py makemigrations myapp --empty这会生成一个空迁移。编辑该迁移,为每个需要初始化的模型添加一个BootstrapTranslatableModel操作:
from django.db import migrations from wagtail.models import BootstrapTranslatableModel class Migration(migrations.Migration): dependencies = [ ("myapp", "0002_bootstraptranslations"), ] # 每个要初始化的模型加一个操作 # 注意:只包含同一个 app 里的模型! operations = [ BootstrapTranslatableModel("myapp.Advert"), ]源码实现(wagtail/models/i18n.py)中,BootstrapTranslatableModel是一个migrations.RunPython操作:前向时取LANGUAGE_CODE对应的Locale,再调用bootstrap_translatable_model()为所有translation_key为空的记录逐条写入新 UUID 与该 locale。其他包含可翻译模型的 app 需要重复同样的步骤。
第 3 步:把BootstrapTranslatableMixin换回TranslatableMixin
数据补齐后,换回带全部约束的正式 Mixin:
# myapp/models.py from wagtail.models import TranslatableMixin # 改这一行 @register_snippet class Advert(TranslatableMixin, models.Model): # 改这一行 name = models.CharField(max_length=255) class Meta(TranslatableMixin.Meta): # 如果存在,改这一行 verbose_name = "adverts"第 4 步:makemigrations+migrate
python manage.py makemigrations myapp python manage.py migrate当 Django 提示“nullable 的 locale 字段要改为非空”的修复选项时,选择"Ignore for now"——因为数据迁移已经保证所有记录都有值。
若模型表是空的,可以直接跳到添加
TranslatableMixin,跳过上述流程。
5. 翻译工作流:simple_translation 与第三方方案
Wagtail 官方提供wagtail.contrib.simple_translation作为内容翻译的默认工作流(源码位于 wagtail/contrib/simple_translation/)。它提供一个后台界面,允许用户把页面和可翻译 snippet复制到另一种语言:
- 复制出来的副本仍是源语言内容(并未自动翻译);
- 页面的副本处于草稿状态。
之后由内容编辑者完成实际翻译并手动发布。启用步骤:
- 把
"wagtail.contrib.simple_translation"加入INSTALLED_APPS; - 运行
python manage.py migrate创建submit_translation权限; - 在 Wagtail 后台的权限设置中,给用户或组勾选 “Can submit translations” 权限。
simple_translation 是可选的,可以整体替换为第三方包,例如更成熟的 wagtail-localize,它支持基于 PO 文件、机器翻译和外部翻译服务集成的翻译工作流。
5.1 另一种架构路线:wagtail-modeltranslation
Wagtail 官方方案遵循“每语言一棵页面树”哲学,因此不同语言树的结构可能差异很大(wagtail-localize 在一定程度上提供了同步选项)。如果你需要字段级翻译、所有语言共处一条数据库记录、统一的树结构,wagtail-modeltranslation 基于 django-modeltranslation 提供了另一种稳健的架构模式,值得在多语言站点选型时对比评估。
6. Wagtail 后台界面自身的语言
这一节与内容语言无关,讲的是后台 UI的语言。
6.1 后台翻译与按用户切换语言
Wagtail 后台已被翻译成多种语言,可用翻译列表可以在 Wagtail 的 Transifex 项目页查看(旧版本 Wagtail 的页面信息可能不反映你手上的语言包)。如果你的语言不在列表中,也可以注册 Transifex 提交新语言或纠错,翻译更新通常会在提交后一个月内合并进正式发行版。
后台支持按用户切换语言:登录用户在/admin/account/页面可以设置首选语言。默认情况下 Wagtail 列出翻译覆盖率≥ 90%的语言;可以通过设置WAGTAILADMIN_PERMITTED_LANGUAGES覆盖这个列表(读取逻辑见 wagtail/admin/localization.py)。相关行为在测试中有明确验证(wagtail/admin/tests/test_account_management.py):
- 用
WAGTAILADMIN_PERMITTED_LANGUAGES=[("en", "English"), ("es", "Spanish")]覆盖时,下拉列表只出现这两种语言; - 只允许一种语言时(
len(get_available_admin_languages()) <= 1),整个语言选择表单会被隐藏——对应源码中LocaleSettingsPanel.is_active()的判断逻辑(wagtail/admin/views/account.py); - 用户未选择任何语言时,回退使用
LANGUAGE_CODE。
6.2 修改安装的默认语言
Wagtail 默认语言是en-us(美式英语)。修改方法是调整两个 Django 设置:
- 确保
USE_I18N为True; - 把
LANGUAGE_CODE设为站点的主语言。
如果该语言存在后台翻译,后台界面即会显示为你选择的语言。注意这与内容层面的默认语言(Site 的 root page 所在语言)是两回事:LANGUAGE_CODE影响后台 UI 与首次迁移生成的Locale记录,而站点的“默认内容语言”由 Site 记录的 root page 决定。
7. 小结:一张配置清单
把上文收敛为可执行的 checklist:
| 步骤 | 位置 | 说明 |
|---|---|---|
1.USE_I18N = True | settings.py | Django 侧国际化总开关 |
2.WAGTAIL_I18N_ENABLED = True | settings.py | Wagtail 侧多语言内容总开关 |
3.LANGUAGES/WAGTAIL_CONTENT_LANGUAGES | settings.py | 前者管前端 locale,后者管内容语言树;修改后者后同步Locale表 |
4."wagtail.locales" | INSTALLED_APPS | 可选,启用 Locale 管理 UI |
5.i18n_patterns(...) | urls.py | 页面与可翻译 URL 加语言前缀;API/sitemaps 不入内 |
6.prefix_default_language=False | i18n_patterns参数 | 可选,默认语言免前缀 |
7.django.middleware.locale.LocaleMiddleware | MIDDLEWARE | 根路径按浏览器语言 302 到语言前缀 |
| 8. 模板选择器 | 站点模板 | 基础版遍历page.get_translations.live;共享 locale 站点用{% language %}+page.localized |
9.?locale=/?translation_of= | Wagtail API | Headless 站点的多语言过滤 |
10.TranslatableMixin | 自定义模型 | snippet 可翻译化;已有数据走 Bootstrap 四步法 |
11.wagtail.contrib.simple_translation | INSTALLED_APPS+ 迁移 | 翻译复制工作流与 “Can submit translations” 权限 |
12.WAGTAILADMIN_PERMITTED_LANGUAGES | settings.py | 可选,控制后台/admin/account/可选语言 |
这套方案的核心在于:内容层面用translation_key把各语言树“缝合”起来,路由层面交给 Django 原生的 i18n 工具链,后台界面语言独立可控。三者解耦后,你既可以做单树多区域的程序化本地化,也可以做多树深度定制的国际化站点。更多 API 侧细节可参考 docs/advanced_topics/api/ 中的 API 文档,更多可参考文档入口 docs/advanced_topics/i18n.md。
【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考