Skip to content

Commit 167f4e1

Browse files
dchukhinstevejalimkkellydesignmaribedran
authored
Create "Compare" And "More" Pages In Wagtail (#1241)
* update get_locales_for_cms_page() to also return fallback locale codes * add a utility function to find the fallback page for a locale * add a migration to ensure expected Locales exist in database * update FALLBACK_LOCALES mapping in settings to match expected locale fallback pattern * update CMSLocaleFallbackMiddleware to serve content from a fallback locale when the requested locale page is not found * update SpringfieldLocale.get_active to find fallback locale if needed Note: this logic shouldn't be needed, since all of the locales should be present in the database, but in case one of the locale records is removed from the database, but the locale is still referenced in the code (for example, in the settings), this logic will handle it. * update i18n context processor to return a CANONICAL_LANG CANONICAL_LANG matches LANG, unless a page is served for a fallback locale. In this case, the template can determine which context variable (LANG or CANONICAL_LANG) to use in each relevant place. * update SpringfieldLinkBlock to return expected page URLs when a user requests a page and is given content for a fallback locale, the page links in the content should match the requested URL's locale. * avoid bug of always returning a 200 response by reverting commit 356d331 * add tests for changes to get_locale() and render() * make sure that locales created in migration are translations of root page * update get_locale_options to return alias locales that have fallback locales * make sure canonical href points to appropriate URL for content * add a test that alias locale pages do not appear in the sitemap * for consistency, make sure non-Wagtail pages also serve fallback content from alias locale URL when alias locale has no content * serve fallback locale content even when alias Locale does not exist in the database * make sure page links in link block return alias locale URLs even when alias Locale does not exist in the database * update tests that rely on FALLBACK_LOCALES to explicitly set it * correct an incorrectly defined variable in a test * make sure relative links in link block return alias locale URLs even when alias Locale does not exist in the database * add an indicator for alias locales on the locale list page in Wagtail * make sure root pages in new Locales are properly created this fixes an error where translating a page into the new Locales was causing a server error, because wagtail-localize was not able to correctly find the root page in the new Locale. * make sure page links correctly point to page URLs in other locales * add column filters in the wagtail localize dashboard * use latest version of wagtail-localize-dashboard * make sure canonical and alternate hreflangs are valid do not show hreflang alternates for alias locales that don't have their own content * add template file used in test * avoid leaking test state by modifying jinja2_loader.searchpath, rather than setting template engines * make sure new root pages are created at the correct depth * make sure we correctly render homepage content for alias locales * make logic more readable by separating middleware and URL-routing logic The separation: - makes the logic more readable - avoids a scenario where the middleware intercepts the request and returns a Wagtail page when it should not * reorder migrations after merge * make sure redirects based on Accept-Language header work for non-depth-2 home page sites Previously, the middleware's Accept-Language redirect was hardcoding Wagtail url_path prefixes as /home and /home-{locale}, which only works when the site root page is at depth 2. In environments where Site.root_page is at a different depth (for example at depth 3 with url_path=/home/home/), the hardcoded prefixes were not matching pages, causing 404s instead of redirects for Wagtail Locales that did not have a specific page. * reorder migrations after merge * fix minor typos in tests * add unit tests for different root page scenarios in fallback locale * disable creating alias locale records in db export script * update comment in migration to be accurate based on middleware refactoring * make code more readable by referring to request interception in a consistent way * make code more readable by moving fallback locale check into helper function * add explicit test that a live Page in an alias locale is served as expected * be more specific about the exception when the site root does not exist in a fallback locale * be more efficient when calculating existing locales * be more specific about the exceptions raised in SpringfieldLinkBlockURLValue.get_url() * Remove extra whitespace in comment Co-authored-by: Steve Jalim <stevejalim@mozilla.com> * get locale directly from the URL, rather than an attribute on the request This change makes the code less likely to break in case any changes are made to our code that sets the 'locale' attribute on the request * update 'compare' and 'more' URLs to use prefer_cms() * add helper functions for FTL parsing * add a field to determine if article detail siblings should be visible on article index page * add a management command to create 'compare' and 'more' CMS pages * add migrations to create 'compare' and 'more' pages * reorder migrations after merge * reorder migrations after merge * explicitly set show_sibling_detail_pages in test object * update 'compare' and 'more' URLs to use prefer_cms() * add helper functions for FTL parsing * add a field to determine if article detail siblings should be visible on article index page * add a management command to create 'compare' and 'more' CMS pages * add migrations to create 'compare' and 'more' pages * explicitly set show_sibling_detail_pages in test object * reorder migrations to account for changes in main branch * reorder migrations after merge * reorder migrations after merge * reorder migrations after merge * [reset-db] * [reset-db] * add wagtailsearch migration dependency, to allow migration to run on fresh databases * Organize imports on create compare pages command * ArticleIndexPage supports 3 different card types * update logic to create 'compare' and 'more' pages to set pages' index_card_type * merge migration * updated migration list --------- Co-authored-by: Steve Jalim <stevejalim@mozilla.com> Co-authored-by: Kasey Kelly <kasey@servee.com> Co-authored-by: Mariana Bedran Lesche <maribedran@gmail.com>
1 parent a3c90de commit 167f4e1

11 files changed

Lines changed: 1841 additions & 106 deletions

springfield/cms/fixtures/compare_more_page_fixtures.py

Lines changed: 816 additions & 0 deletions
Large diffs are not rendered by default.

springfield/cms/ftl_parser.py

Lines changed: 155 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,105 @@
1717

1818
from django.conf import settings
1919

20+
import polib
2021
from fluent.syntax import parse
2122
from fluent.syntax.ast import Message, Placeable, TextElement
23+
from wagtail_localize.models import StringSegment
2224

2325
# Brand terms to expand in FTL strings.
2426
# These match the terms defined in l10n/en/brands.ftl
2527
BRAND_TERMS = {
28+
# Company names
29+
"-brand-name-apple": "Apple",
30+
"-brand-name-creative-commons": "Creative Commons",
31+
"-brand-name-facebook": "Facebook",
32+
"-brand-name-google": "Google",
33+
"-brand-name-microsoft": "Microsoft",
34+
"-brand-name-mozilla": "Mozilla",
35+
"-brand-name-mozilla-corporation": "Mozilla Corporation",
36+
"-brand-name-mozilla-foundation": "Mozilla Foundation",
37+
"-brand-name-netscape": "Netscape",
38+
"-brand-name-linkedin": "LinkedIn",
39+
"-brand-name-spotify": "Spotify",
40+
# Firefox browsers
2641
"-brand-name-firefox": "Firefox",
42+
"-brand-name-firefox-beta": "Firefox Beta",
2743
"-brand-name-firefox-browser": "Firefox Browser",
28-
"-brand-name-mozilla": "Mozilla",
44+
"-brand-name-firefox-browsers": "Firefox Browsers",
45+
"-brand-name-firefox-developer-edition": "Firefox Developer Edition",
46+
"-brand-name-firefox-enterprise": "Firefox Enterprise",
47+
"-brand-name-firefox-esr": "Firefox ESR",
48+
"-brand-name-firefox-extended-support-release": "Firefox Extended Support Release",
49+
"-brand-name-firefox-focus": "Firefox Focus",
50+
"-brand-name-firefox-nightly": "Firefox Nightly",
51+
# Firefox browsers (short names)
52+
"-brand-name-beta": "Beta",
53+
"-brand-name-developer-edition": "Developer Edition",
54+
"-brand-name-enterprise": "Enterprise",
55+
"-brand-name-esr": "ESR",
56+
"-brand-name-focus": "Focus",
57+
"-brand-name-nightly": "Nightly",
58+
# Firefox browsers (legacy)
59+
"-brand-name-firefox-aurora": "Firefox Aurora",
60+
# Firefox products
61+
"-brand-name-facebook-container": "Facebook Container",
62+
"-brand-name-firefox-devtools": "Firefox DevTools",
63+
"-brand-name-firefox-relay": "Firefox Relay",
64+
"-brand-name-firefox-translations": "Firefox Translations",
65+
# Firefox products (short names)
66+
"-brand-name-relay": "Relay",
67+
# Firefox projects
68+
"-brand-name-firefox-labs": "Firefox Labs",
69+
# Pocket
70+
"-brand-name-pocket": "Pocket",
71+
# Fakespot
72+
"-brand-name-fakespot": "Fakespot",
73+
# Mozilla projects
74+
"-brand-name-bugzilla": "Bugzilla",
75+
"-brand-name-gecko": "Gecko",
76+
"-brand-name-mdn-plus": "MDN Plus",
77+
"-brand-name-mdn-web-docs": "MDN Web Docs",
78+
"-brand-name-mozilla-monitor": "Mozilla Monitor",
79+
"-brand-name-mozilla-vpn": "Mozilla VPN",
2980
"-brand-name-mozilla-account": "Mozilla account",
3081
"-brand-name-mozilla-accounts": "Mozilla accounts",
31-
"-brand-name-gecko": "Gecko",
32-
"-brand-name-ipad": "iPad",
33-
"-brand-name-iphone": "iPhone",
34-
"-brand-name-firefox-translations": "Firefox Translations",
82+
"-brand-name-mozilla-ai-v2": "Mozilla.ai",
83+
"-brand-name-mozilla-ventures": "Mozilla Ventures",
84+
"-brand-name-thunderbird": "Thunderbird",
85+
# Mozilla projects (short names)
86+
"-brand-name-common-voice": "Common Voice",
87+
"-brand-name-mdn": "MDN",
88+
"-brand-name-monitor": "Monitor",
89+
# Other browsers
90+
"-brand-name-brave": "Brave",
3591
"-brand-name-chrome": "Chrome",
3692
"-brand-name-edge": "Edge",
93+
"-brand-name-ie": "Internet Explorer",
94+
"-brand-name-opera": "Opera",
3795
"-brand-name-safari": "Safari",
96+
# Platforms
3897
"-brand-name-android": "Android",
98+
"-brand-name-chromeos": "Chrome OS",
3999
"-brand-name-ios": "iOS",
40-
"-brand-name-windows": "Windows",
41-
"-brand-name-macos": "macOS",
42100
"-brand-name-linux": "Linux",
101+
"-brand-name-mac": "macOS",
102+
"-brand-name-mac-short": "Mac",
103+
"-brand-name-macos": "macOS",
104+
"-brand-name-windows": "Windows",
105+
# Apple products
106+
"-brand-name-app-store": "App Store",
107+
"-brand-name-ipad": "iPad",
108+
"-brand-name-iphone": "iPhone",
109+
"-brand-name-test-flight": "TestFlight",
110+
# Facebook products
111+
"-brand-name-instagram": "Instagram",
112+
# Google products
113+
"-brand-name-chromebook": "Chromebook",
114+
"-brand-name-chromium": "Chromium",
115+
"-brand-name-google-play": "Google Play",
116+
"-brand-name-youtube": "YouTube",
117+
# Enterprise
118+
"-brand-name-support-for-organizations": "Support for Organizations",
43119
}
44120

45121

@@ -195,6 +271,36 @@ def get_english_ftl_strings(ftl_filename: str) -> dict[str, str]:
195271
return parse_ftl_file(en_path)
196272

197273

274+
def get_ftl_path_for_locale_at_subpath(locale: str, ftl_relative_path: str) -> Path | None:
275+
"""Like get_ftl_path_for_locale but accepts a full relative path.
276+
277+
Args:
278+
locale: e.g. "de"
279+
ftl_relative_path: e.g. "firefox/browsers/compare/brave.ftl"
280+
"""
281+
external_path = settings.FLUENT_REPO_PATH / locale / ftl_relative_path
282+
if external_path.exists():
283+
return external_path
284+
local_path = settings.FLUENT_LOCAL_PATH / locale / ftl_relative_path
285+
if local_path.exists():
286+
return local_path
287+
return None
288+
289+
290+
def get_ftl_translations_at_subpath(locale: str, ftl_relative_path: str) -> dict[str, str]:
291+
"""Get translations for a specific locale using a full relative FTL path."""
292+
ftl_path = get_ftl_path_for_locale_at_subpath(locale, ftl_relative_path)
293+
if ftl_path:
294+
return parse_ftl_file(ftl_path)
295+
return {}
296+
297+
298+
def get_english_ftl_strings_at_subpath(ftl_relative_path: str) -> dict[str, str]:
299+
"""Get English source strings using a full relative FTL path."""
300+
en_path = settings.FLUENT_LOCAL_PATH / "en" / ftl_relative_path
301+
return parse_ftl_file(en_path)
302+
303+
198304
def get_ui_ftl_path_for_locale(locale: str) -> Path | None:
199305
"""
200306
Get the path to ui.ftl for a specific locale.
@@ -362,6 +468,48 @@ def convert_ftl_links_to_wagtail(translated: str, source: str) -> str:
362468
return result
363469

364470

471+
def build_po_from_ftl(translation, en_text_to_msgid: dict[str, str], translations: dict[str, str]):
472+
"""Build a PO file by matching Wagtail segments to FTL translations.
473+
474+
Args:
475+
translation: A wagtail_localize Translation instance.
476+
en_text_to_msgid: Mapping of normalized English text to FTL message IDs
477+
(built with build_text_to_msgid_mapping).
478+
translations: Mapping of FTL message IDs to translated text for the target locale.
479+
480+
Returns:
481+
A polib.POFile populated with matched translation entries.
482+
"""
483+
po = polib.POFile(wrapwidth=200)
484+
po.metadata = {
485+
"MIME-Version": "1.0",
486+
"Content-Type": "text/plain; charset=utf-8",
487+
"X-WagtailLocalize-TranslationID": str(translation.uuid),
488+
}
489+
490+
string_segments = StringSegment.objects.filter(source=translation.source).select_related("string", "context")
491+
492+
for segment in string_segments:
493+
source_text = segment.string.data
494+
normalized_source = normalize_text_for_matching(source_text)
495+
496+
msgid = en_text_to_msgid.get(normalized_source)
497+
498+
if msgid and msgid in translations:
499+
translated_text = translations[msgid]
500+
translated_text = convert_ftl_links_to_wagtail(translated_text, source_text)
501+
502+
po.append(
503+
polib.POEntry(
504+
msgid=source_text,
505+
msgctxt=segment.context.path,
506+
msgstr=translated_text,
507+
)
508+
)
509+
510+
return po
511+
512+
365513
def build_text_to_msgid_mapping(ftl_strings: dict[str, str]) -> dict[str, str]:
366514
"""
367515
Build a mapping from normalized text to FTL message ID.

0 commit comments

Comments
 (0)