Wagtail 国际化实战指南:多语言内容架构、配置全流程与源码解析
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.pyclass TranslatableMixin(models.Model): translation_key models.UUIDField(defaultuuid.uuid4, editableFalse) locale models.ForeignKey( Locale, on_deletemodels.PROTECT, related_name, editableFalse, 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.pyclass Locale(models.Model): language_code models.CharField(max_length100, uniqueTrue) 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_draftwagtail/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/核心实现是LocaleViewSetwagtail/locales/views.py与表单LocaleFormwagtail/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, namesearch), 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, namesearch), path(, include(wagtail_urls)), prefix_default_languageFalse, )此时 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(inclusiveTrue).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 %} relalternate 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过滤同表记录inclusiveFalse时排除自身a标签链接指向翻译页{{ translation.locale.language_name_local }}以语言本身的名称显示例如fr显示为français同时加上relalternate与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 %} relalternate 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_ofroot这种特殊取值返回根页面各语言的翻译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_length255)TranslatableMixin会为模型添加locale与translation_key两个字段并自动获得get_translations()、localized、copy_for_translation()等能力。另外从源码可以看到一个信号处理器set_locale_on_new_instancewagtail/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_length255) # 如果模型有 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_length255) class Meta(TranslatableMixin.Meta): # 如果存在改这一行 verbose_name adverts第 4 步makemigrationsmigratepython 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-modeltranslationWagtail 官方方案遵循“每语言一棵页面树”哲学因此不同语言树的结构可能差异很大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 Truesettings.pyDjango 侧国际化总开关2.WAGTAIL_I18N_ENABLED Truesettings.pyWagtail 侧多语言内容总开关3.LANGUAGES/WAGTAIL_CONTENT_LANGUAGESsettings.py前者管前端 locale后者管内容语言树修改后者后同步Locale表4.wagtail.localesINSTALLED_APPS可选启用 Locale 管理 UI5.i18n_patterns(...)urls.py页面与可翻译 URL 加语言前缀API/sitemaps 不入内6.prefix_default_languageFalsei18n_patterns参数可选默认语言免前缀7.django.middleware.locale.LocaleMiddlewareMIDDLEWARE根路径按浏览器语言 302 到语言前缀8. 模板选择器站点模板基础版遍历page.get_translations.live共享 locale 站点用{% language %}page.localized9.?locale/?translation_ofWagtail APIHeadless 站点的多语言过滤10.TranslatableMixin自定义模型snippet 可翻译化已有数据走 Bootstrap 四步法11.wagtail.contrib.simple_translationINSTALLED_APPS 迁移翻译复制工作流与 “Can submit translations” 权限12.WAGTAILADMIN_PERMITTED_LANGUAGESsettings.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),仅供参考