Set correct lang-picker options when different locales for a given path may be served by the CMS or the Django fallback view

* Expand prefer_cms to take args that tell us what locales are available in the Django fallback version of the page
* In prefer_cms annotate the request with lists of locales available in a CMS-backed version of the page and a Django-backed version
* Add a new jinja helper to select the most appropriate list of locales to pass to the lang picker UI element
This commit is contained in:
Steve Jalim 2024-11-07 10:34:15 +00:00
Родитель d78a2c4110
Коммит 6e2a8a51dd
6 изменённых файлов: 161 добавлений и 27 удалений

Просмотреть файл

@ -4,12 +4,16 @@
file, You can obtain one at https://mozilla.org/MPL/2.0/.
#}
{% if translations|length > 1 %}
{%- set available_languages = get_lang_switcher_options(request, translations) -%}
{% if available_languages|length > 1 %}
<form id="lang_form" class="mzp-c-language-switcher" method="get" action="#">
<a class="mzp-c-language-switcher-link" href="{{ url('mozorg.locales') }}">{{ ftl('footer-language') }}</a>
<label for="page-language-select">{{ ftl('footer-language') }}</label>
<select id="page-language-select" class="mzp-js-language-switcher-select" name="lang" dir="ltr" data-testid="footer-language-select">
{% for code, label in translations|dictsort -%}
{% for code, label in available_languages|dictsort -%}
{# Don't escape the label as it may include entity references
# like "Português (do&nbsp;Brasil)" (Bug 861149) #}
<option lang="{{ code }}" value="{{ code }}"{{ code|ifeq(LANG, " selected") }}>{{ label|safe|replace('English (US)', 'English') }}</option>

Просмотреть файл

@ -17,6 +17,7 @@ from markupsafe import Markup
from bedrock.base import waffle
from bedrock.utils import expand_locale_groups
from lib.l10n_utils import get_translations_native_names
from ..urlresolvers import reverse
@ -153,3 +154,22 @@ def alternate_url(path, locale):
return alt_paths[path][locale]
return None
@library.global_function
def get_lang_switcher_options(request, translations):
# For purely Django-rendered pages, or purely CMS-backed pages, we can just
# rely on the `translations` var in the render context to know what locales
# are viable for the page being rendered. Great! \o/
available_locales = translations
# However, if a URL route is decorated with bedrock.cms.decorators.prefer_cms
# that means that a page could come from the CMS or from Django depending on
# the locale being requested. In this situation _locales_available_via_cms
# and _locales_for_django_fallback_view are annotated onto the request.
# We need to use these to create a more accurate view of what locales are
# available
if hasattr(request, "_locales_available_via_cms") and hasattr(request, "_locales_for_django_fallback_view"):
available_locales = get_translations_native_names(sorted(set(request._locales_available_via_cms + request._locales_for_django_fallback_view)))
return available_locales

Просмотреть файл

@ -9,19 +9,41 @@ from django.http import Http404
from wagtail.views import serve as wagtail_serve
from bedrock.base.i18n import remove_lang_prefix
from lib.l10n_utils.fluent import get_active_locales
from .utils import get_cms_locales_for_path
HTTP_200_OK = 200
def prefer_cms(view_func):
def prefer_cms(view_func=None, fallback_ftl_files=None, fallback_lang_codes=None):
"""
A decorator that helps us migrate from pure Django-based views
to CMS views.
to CMS views, or support having _some_ locales served from the CMS and
other / evergreen content in other locales coming from Django views .
It will try to see if `wagtail.views.serve` can find a live CMS page for the
URL path that matches the current Django view's path, and if so, will
return that. If not, it will let the regular Django view run.
Args:
view_func - the function to wrap
fallback_ftl_files (optional) - a list of the names of the Fluent files used by
the Django view that's being wrapped. It's a little repetitive, but
from those we can work out what locales the page is availble in
across the CMS and Django views
fallback_lang_codes (optional) - a list of strings of language codes that
show what locales are available for the Django view being wrapped.
This is useful if, for some reason, the Fluent files are not a reliable
way to determine the available locales for a page
(e.g. the Fluent files cover 20 locales for some strings which appear
on the page, but the main localized content is only in two languages,
because the contnet doesn't come from Fluent - such as Legal Docs,
which comes from a git repo)
Note that setting both fallback_lang_codes and fallback_ftl_files will cause
an exception to be raised - only one should be set, not both.
Workflow:
1. This decorator is added to the target view that will be replaced with CMS content
@ -33,42 +55,100 @@ def prefer_cms(view_func):
Example for a function-based view:
@prefer_cms
@prefer_cms(fallback_ftl_files=[...]) # or fallback_lang_codes=["fr", "es-MX",]
def some_path(request):
...
Or, in a URLconf for a regular Django view:
...
path("some/path/", prefer_cms(views.some_view)),
path("some/path/", prefer_cms(views.some_view, fallback_ftl_files=[...])),
# or
path("some/path/", prefer_cms(views.some_view, fallback_lang_codes=["fr", "pt-BR",])),
...
Or, in a URLconf with Bedrock's page() helper:
page(
"about/leadership/",
"mozorg/about/leadership/index.html",
ftl_files=["some/path/to/fluent/data"],
decorators=[prefer_cms],
)
(Note that no fallback_ftl_files is needed here - we'll just pick up the regular
ftl_files kwarg that's already used for page(). ALSO: fallback_ftl_files is NOT
supported for the page() helper, as we shouldn't need it in real use)
"""
@wraps(view_func)
def wrapped_view(request, *args, **kwargs):
path = remove_lang_prefix(request.path_info)
try:
# Does Wagtail have a route that matches this? If so, show that page
wagtail_response = wagtail_serve(request, path)
if wagtail_response.status_code == HTTP_200_OK:
return wagtail_response
except Http404:
pass
if fallback_ftl_files and fallback_lang_codes:
raise RuntimeError("The prefer_cms decorator can be configured with either fallback_ftl_files or fallback_lang_codes but not both.")
# If not, call the original view function, but remember to
# un-mark the request as being for a CMS page. This is so to avoid
# lib.l10n_utils.render() incorrectly looking for available translations
# based on CMS data - we're falling back to non-CMS pages, so we want
# the Fluent translations that are normal for a Django-rendered page
request.is_cms_page = False
return view_func(request, *args, **kwargs)
fallback_ftl_files = fallback_ftl_files or []
fallback_lang_codes = fallback_lang_codes or []
return wrapped_view
def _get_django_locales_available(
fallback_ftl_files,
fallback_lang_codes,
kwargs,
):
# Prefer explicit list of lang codes over everything else
if fallback_lang_codes:
return fallback_lang_codes
# If we're looking at Fluent files, prefer the ftl_files kwarg (as used
# with page()) but also accept an explicitly added fallback_ftl_files param
_ftl_files = kwargs.get("ftl_files", fallback_ftl_files)
return get_active_locales(_ftl_files, force=True)
def decorator(func):
@wraps(func)
def wrapped_view(request, *args, **kwargs):
path = remove_lang_prefix(request.path_info)
# Annotate the request with the Django/fallback locales, as we'll
# need them for the language picket in the footer when rendering
# the Wagtail response IF there is a Wagtail match
request._locales_for_django_fallback_view = _get_django_locales_available(
fallback_ftl_files=fallback_ftl_files,
fallback_lang_codes=fallback_lang_codes,
kwargs=kwargs,
)
try:
# Does Wagtail have a route that matches this? If so, show that page
wagtail_response = wagtail_serve(request, path)
if wagtail_response.status_code == HTTP_200_OK:
return wagtail_response
except Http404:
pass
# If the page does not exist in Wagtail, call the original view function and...
#
# 1) Un-mark this request as being for a CMS page (which happened
# via wagtail_serve()) to avoid lib.l10n_utils.render() incorrectly
# looking for available translations based on CMS data, rather than
# Fluent files
request.is_cms_page = False
# 2) Make extra sure this request is still annotated with any CMS-backed
# locale versions that are available, so that we can populate the
# language picker appropriately. (The annotation also happened via
# wagtail_serve() thanks to AbstractBedrockCMSPage._patch_request_for_bedrock
request._locales_available_via_cms = getattr(
request,
"_locales_available_via_cms",
get_cms_locales_for_path(request),
)
return func(request, *args, **kwargs)
return wrapped_view
# If view_func is None, the decorator was called with parameters
if view_func is None:
return decorator
else:
# Otherwise, apply the decorator directly to view_func
return decorator(view_func)

Просмотреть файл

@ -59,7 +59,7 @@ class AbstractBedrockCMSPage(WagtailBasePage):
# Quick annotation to help us track the origin of the page
request.is_cms_page = True
# Patch in a list of CMS-available locales for pages that are translations, not just aliases
# Patch in a list of available locales for pages that are translations, not just aliases
request._locales_available_via_cms = get_locales_for_cms_page(self)
return request

Просмотреть файл

@ -1,11 +1,26 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
from django.http import Http404
def get_page_for_path(request, path):
from wagtail.models import Site
site = Site.find_for_request(request)
try:
page, args, kwargs = site.root_page.specific.route(request, path)
return page
except Http404:
pass
return None
def get_locales_for_cms_page(page):
# Patch in a list of CMS-available locales for pages that are
# translations, not just aliases
locales_available_via_cms = [page.locale.language_code]
try:
_actual_translations = (
@ -21,3 +36,12 @@ def get_locales_for_cms_page(page):
pass
return locales_available_via_cms
def get_cms_locales_for_path(request):
locales = []
if page := get_page_for_path(request=request, path=request.path):
locales = get_locales_for_cms_page(page)
return locales

Просмотреть файл

@ -39,12 +39,18 @@ urlpatterns = (
# VPN Resource Center
path(
"vpn/resource-center/",
prefer_cms(views.resource_center_landing_view),
prefer_cms(
views.resource_center_landing_view,
fallback_lang_codes=["de", "en-US", "es-ES", "fr", "it", "ja", "nl", "pl", "pt-BR", "ru", "zh-CN"],
),
name="products.vpn.resource-center.landing",
),
path(
"vpn/resource-center/<slug:slug>/",
prefer_cms(views.resource_center_article_view),
prefer_cms(
views.resource_center_article_view,
fallback_lang_codes=["de", "en-US", "es-ES", "fr", "it", "ja", "nl", "pl", "pt-BR", "ru", "zh-CN"],
),
name="products.vpn.resource-center.article",
),
path("monitor/waitlist-plus/", views.monitor_waitlist_plus_page, name="products.monitor.waitlist-plus"),