Skip to content
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ac8d8b3
add a snippet to store pre-translated text
dchukhin Apr 10, 2026
099160d
migrate download firefox button blocks to use new Snippet
dchukhin Apr 13, 2026
35855d6
display ButtonLabelSnippet in Wagtail
dchukhin Apr 13, 2026
5cbebf4
update fixtures based on changes to download firefox buttons
dchukhin Apr 13, 2026
a0548e8
reorder migrations based on main branch
dchukhin Apr 13, 2026
0a984c8
add helper functions for FTL parsing
dchukhin Apr 2, 2026
109f6d3
add tests for new DownloadFirefoxButtonBlock methods
dchukhin Apr 13, 2026
50a3364
add test to assert download button for fallback scenario
dchukhin Apr 13, 2026
5ca37ce
Merge branch 'main' into WT-856-get-firefox-download-firefox-text-in-cms
dchukhin Apr 13, 2026
9989f10
configure translations dashboard to show snippets dashboard with Butt…
dchukhin Apr 17, 2026
608345d
Merge branch 'main' into WT-856-get-firefox-download-firefox-text-in-cms
dchukhin Apr 21, 2026
a8fb001
reorder migrations after merge
dchukhin Apr 21, 2026
b0d58e2
Merge branch 'main' into WT-856-get-firefox-download-firefox-text-in-cms
dchukhin Apr 29, 2026
e40c756
rename ButtonLabelSnippet to PretranslatedPhrase
dchukhin May 1, 2026
82e473f
change PretranslatedPhrase key field to a PretranslatedPhraseCategory…
dchukhin May 1, 2026
6034c65
update logic to create PretranslatedPhrase programmatically
dchukhin May 1, 2026
ba454df
update tests based on changes to snippet models
dchukhin May 1, 2026
cd045bc
add a button label for DownloadFirefoxButtonBlock
dchukhin May 1, 2026
fe2f6e0
undo unnecessary indentation in download-firefox-button.html
dchukhin May 1, 2026
38d9e9d
show PretranslatedPhrases with other snippets in Wagtail
dchukhin May 1, 2026
999d679
show PretranslatedPhrases with other snippets in Wagtail
dchukhin May 1, 2026
7f48dbb
update import order in fixtures
dchukhin May 1, 2026
8008742
Merge branch 'main' into WT-856-get-firefox-download-firefox-text-in-cms
dchukhin May 1, 2026
c94ad5c
add template used by PhraseCategoryIndexView
dchukhin May 4, 2026
26d0e33
Merge branch 'main' into WT-856-get-firefox-download-firefox-text-in-cms
dchukhin May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions springfield/cms/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from django.utils.translation import gettext_lazy as _

from wagtail import blocks
from wagtail.blocks import StructBlockValidationError
from wagtail.images.blocks import ImageChooserBlock
from wagtail.models import Page
from wagtail.snippets.blocks import SnippetChooserBlock
Expand Down Expand Up @@ -931,11 +932,42 @@ class Meta:
def DownloadFirefoxButtonBlock(themes=None, **kwargs):
class _DownloadFirefoxButtonBlock(blocks.StructBlock):
settings = DownloadFirefoxButtonSettings(themes=themes)
label = blocks.CharBlock(label="Button Text", default="Get Firefox")
pretranslated_label = LocalizedLiveSnippetChooserBlock(
"cms.ButtonLabelSnippet",
required=False,
label="Pre-translated Text",
help_text="Select a pre-translated label. Takes precedence over Custom Text.",
)
custom_label = blocks.CharBlock(
required=False,
label="Custom Text",
help_text="Use only if no pre-translated option fits. Will be sent to Smartling for translation as part of the page.",
)

def clean(self, value):
errors = {}
has_pretranslated = bool(value.get("pretranslated_label"))
has_custom = bool((value.get("custom_label") or "").strip())
if not has_pretranslated and not has_custom:
errors["pretranslated_label"] = ValidationError("Either a pre-translated text or custom text is required.")
if has_pretranslated and has_custom:
errors["custom_label"] = ValidationError("Provide either a pre-translated text or custom text, not both.")
if errors:
raise StructBlockValidationError(block_errors=errors)
return super().clean(value)

def get_context(self, value, parent_context=None):
context = super().get_context(value, parent_context)
pretranslated = value.get("pretranslated_label")
if pretranslated:
localized = pretranslated.get_localized()
context["button_label"] = (localized or pretranslated).label
elif value.get("custom_label"):
context["button_label"] = value.get("custom_label", "")
return context

class Meta:
label = "Download Firefox Button"
label_format = "Download Firefox Button - {label}"
template = "cms/blocks/download-firefox-button.html"
value_class = BaseButtonValue

Expand Down
8 changes: 6 additions & 2 deletions springfield/cms/fixtures/button_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# file, You can obtain one at https://mozilla.org/MPL/2.0/.


from django.conf import settings

from springfield.cms.fixtures.base_fixtures import get_2026_test_index_page, get_test_document, get_test_index_page
from springfield.cms.models import FreeFormPage, FreeFormPage2026

Expand Down Expand Up @@ -236,7 +238,8 @@ def get_button_variants(full=False) -> dict[str, dict]:
"download": {
"type": "download_button",
"value": {
"label": "Get Firefox",
"pretranslated_label": settings.BUTTON_LABEL_GET_FIREFOX_SNIPPET_ID,
"custom_label": "",
"settings": {
"theme": "",
"icon": "downloads",
Expand All @@ -250,7 +253,8 @@ def get_button_variants(full=False) -> dict[str, dict]:
"download_default_browser": {
"type": "download_button",
"value": {
"label": "Get Firefox",
"pretranslated_label": settings.BUTTON_LABEL_GET_FIREFOX_SNIPPET_ID,
"custom_label": "",
"settings": {
"theme": "secondary",
"icon": "downloads",
Expand Down
5 changes: 3 additions & 2 deletions springfield/cms/fixtures/homepage_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from springfield.cms.fixtures.base_fixtures import get_2026_test_index_page
from springfield.cms.fixtures.button_fixtures import get_button_variants
from springfield.cms.fixtures.snippet_fixtures import get_pre_footer_cta_snippet
from springfield.cms.fixtures.snippet_fixtures import get_button_label_snippets, get_pre_footer_cta_snippet
from springfield.cms.models import HomePage

SHOW_TO_ALL = {"platforms": [], "firefox": "", "auth_state": ""}
Expand Down Expand Up @@ -316,8 +316,9 @@ def get_kit_banner():
def get_home_test_page() -> HomePage:
index_page = get_2026_test_index_page()

# Make sure the Pre-Footer CTA Snippet is created
# Make sure required snippets exist
get_pre_footer_cta_snippet()
get_button_label_snippets()

page = HomePage.objects.filter(slug="test-home-page").first()
if not page:
Expand Down
35 changes: 34 additions & 1 deletion springfield/cms/fixtures/snippet_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@
from wagtail.models import Locale

from springfield.cms.fixtures.base_fixtures import get_placeholder_images
from springfield.cms.models import BannerSnippet, DownloadFirefoxCallToActionSnippet, PreFooterCTAFormSnippet, PreFooterCTASnippet, QRCodeSnippet, Tag
from springfield.cms.models import (
BannerSnippet,
ButtonLabelSnippet,
DownloadFirefoxCallToActionSnippet,
PreFooterCTAFormSnippet,
PreFooterCTASnippet,
QRCodeSnippet,
Tag,
)
from springfield.cms.models.snippets import QRCodeFloatingSnippet


Expand Down Expand Up @@ -84,6 +92,31 @@ def get_qr_code_snippet() -> QRCodeSnippet:
return snippet


def get_button_label_snippets() -> tuple[ButtonLabelSnippet, ButtonLabelSnippet]:
locale = Locale.get_default()
get_firefox, _ = ButtonLabelSnippet.objects.update_or_create(
id=settings.BUTTON_LABEL_GET_FIREFOX_SNIPPET_ID,
defaults={
"locale": locale,
"key": "get_firefox",
"label": "Get Firefox",
"live": True,
"translation_key": "f25078fd-50e4-4a73-acbc-6355bfa7de6e",
},
)
download_firefox, _ = ButtonLabelSnippet.objects.update_or_create(
id=settings.BUTTON_LABEL_DOWNLOAD_FIREFOX_SNIPPET_ID,
defaults={
"locale": locale,
"key": "download_firefox",
"label": "Download Firefox",
"live": True,
"translation_key": "e13dc0ed-aa51-4077-b011-fb20958ffefd",
},
)
return get_firefox, download_firefox


def get_floating_qr_code_snippet() -> QRCodeFloatingSnippet:
locale = Locale.get_default()
snippet, _ = QRCodeFloatingSnippet.objects.update_or_create(
Expand Down
162 changes: 155 additions & 7 deletions springfield/cms/ftl_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,105 @@

from django.conf import settings

import polib
from fluent.syntax import parse
from fluent.syntax.ast import Message, Placeable, TextElement
from wagtail_localize.models import StringSegment

# Brand terms to expand in FTL strings.
# These match the terms defined in l10n/en/brands.ftl
BRAND_TERMS = {
# Company names
"-brand-name-apple": "Apple",
"-brand-name-creative-commons": "Creative Commons",
"-brand-name-facebook": "Facebook",
"-brand-name-google": "Google",
"-brand-name-microsoft": "Microsoft",
"-brand-name-mozilla": "Mozilla",
"-brand-name-mozilla-corporation": "Mozilla Corporation",
"-brand-name-mozilla-foundation": "Mozilla Foundation",
"-brand-name-netscape": "Netscape",
"-brand-name-linkedin": "LinkedIn",
"-brand-name-spotify": "Spotify",
# Firefox browsers
"-brand-name-firefox": "Firefox",
"-brand-name-firefox-beta": "Firefox Beta",
"-brand-name-firefox-browser": "Firefox Browser",
"-brand-name-mozilla": "Mozilla",
"-brand-name-firefox-browsers": "Firefox Browsers",
"-brand-name-firefox-developer-edition": "Firefox Developer Edition",
"-brand-name-firefox-enterprise": "Firefox Enterprise",
"-brand-name-firefox-esr": "Firefox ESR",
"-brand-name-firefox-extended-support-release": "Firefox Extended Support Release",
"-brand-name-firefox-focus": "Firefox Focus",
"-brand-name-firefox-nightly": "Firefox Nightly",
# Firefox browsers (short names)
"-brand-name-beta": "Beta",
"-brand-name-developer-edition": "Developer Edition",
"-brand-name-enterprise": "Enterprise",
"-brand-name-esr": "ESR",
"-brand-name-focus": "Focus",
"-brand-name-nightly": "Nightly",
# Firefox browsers (legacy)
"-brand-name-firefox-aurora": "Firefox Aurora",
# Firefox products
"-brand-name-facebook-container": "Facebook Container",
"-brand-name-firefox-devtools": "Firefox DevTools",
"-brand-name-firefox-relay": "Firefox Relay",
"-brand-name-firefox-translations": "Firefox Translations",
# Firefox products (short names)
"-brand-name-relay": "Relay",
# Firefox projects
"-brand-name-firefox-labs": "Firefox Labs",
# Pocket
"-brand-name-pocket": "Pocket",
# Fakespot
"-brand-name-fakespot": "Fakespot",
# Mozilla projects
"-brand-name-bugzilla": "Bugzilla",
"-brand-name-gecko": "Gecko",
"-brand-name-mdn-plus": "MDN Plus",
"-brand-name-mdn-web-docs": "MDN Web Docs",
"-brand-name-mozilla-monitor": "Mozilla Monitor",
"-brand-name-mozilla-vpn": "Mozilla VPN",
"-brand-name-mozilla-account": "Mozilla account",
"-brand-name-mozilla-accounts": "Mozilla accounts",
"-brand-name-gecko": "Gecko",
"-brand-name-ipad": "iPad",
"-brand-name-iphone": "iPhone",
"-brand-name-firefox-translations": "Firefox Translations",
"-brand-name-mozilla-ai-v2": "Mozilla.ai",
"-brand-name-mozilla-ventures": "Mozilla Ventures",
"-brand-name-thunderbird": "Thunderbird",
# Mozilla projects (short names)
"-brand-name-common-voice": "Common Voice",
"-brand-name-mdn": "MDN",
"-brand-name-monitor": "Monitor",
# Other browsers
"-brand-name-brave": "Brave",
"-brand-name-chrome": "Chrome",
"-brand-name-edge": "Edge",
"-brand-name-ie": "Internet Explorer",
"-brand-name-opera": "Opera",
"-brand-name-safari": "Safari",
# Platforms
"-brand-name-android": "Android",
"-brand-name-chromeos": "Chrome OS",
"-brand-name-ios": "iOS",
"-brand-name-windows": "Windows",
"-brand-name-macos": "macOS",
"-brand-name-linux": "Linux",
"-brand-name-mac": "macOS",
"-brand-name-mac-short": "Mac",
"-brand-name-macos": "macOS",
"-brand-name-windows": "Windows",
# Apple products
"-brand-name-app-store": "App Store",
"-brand-name-ipad": "iPad",
"-brand-name-iphone": "iPhone",
"-brand-name-test-flight": "TestFlight",
# Facebook products
"-brand-name-instagram": "Instagram",
# Google products
"-brand-name-chromebook": "Chromebook",
"-brand-name-chromium": "Chromium",
"-brand-name-google-play": "Google Play",
"-brand-name-youtube": "YouTube",
# Enterprise
"-brand-name-support-for-organizations": "Support for Organizations",
}


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


def get_ftl_path_for_locale_at_subpath(locale: str, ftl_relative_path: str) -> Path | None:
"""Like get_ftl_path_for_locale but accepts a full relative path.

Args:
locale: e.g. "de"
ftl_relative_path: e.g. "firefox/browsers/compare/brave.ftl"
"""
external_path = settings.FLUENT_REPO_PATH / locale / ftl_relative_path
if external_path.exists():
return external_path
local_path = settings.FLUENT_LOCAL_PATH / locale / ftl_relative_path
if local_path.exists():
return local_path
return None


def get_ftl_translations_at_subpath(locale: str, ftl_relative_path: str) -> dict[str, str]:
"""Get translations for a specific locale using a full relative FTL path."""
ftl_path = get_ftl_path_for_locale_at_subpath(locale, ftl_relative_path)
if ftl_path:
return parse_ftl_file(ftl_path)
return {}


def get_english_ftl_strings_at_subpath(ftl_relative_path: str) -> dict[str, str]:
"""Get English source strings using a full relative FTL path."""
en_path = settings.FLUENT_LOCAL_PATH / "en" / ftl_relative_path
return parse_ftl_file(en_path)


def get_ui_ftl_path_for_locale(locale: str) -> Path | None:
"""
Get the path to ui.ftl for a specific locale.
Expand Down Expand Up @@ -362,6 +468,48 @@ def convert_ftl_links_to_wagtail(translated: str, source: str) -> str:
return result


def build_po_from_ftl(translation, en_text_to_msgid: dict[str, str], translations: dict[str, str]):
"""Build a PO file by matching Wagtail segments to FTL translations.

Args:
translation: A wagtail_localize Translation instance.
en_text_to_msgid: Mapping of normalized English text to FTL message IDs
(built with build_text_to_msgid_mapping).
translations: Mapping of FTL message IDs to translated text for the target locale.

Returns:
A polib.POFile populated with matched translation entries.
"""
po = polib.POFile(wrapwidth=200)
po.metadata = {
"MIME-Version": "1.0",
"Content-Type": "text/plain; charset=utf-8",
"X-WagtailLocalize-TranslationID": str(translation.uuid),
}

string_segments = StringSegment.objects.filter(source=translation.source).select_related("string", "context")

for segment in string_segments:
source_text = segment.string.data
normalized_source = normalize_text_for_matching(source_text)

msgid = en_text_to_msgid.get(normalized_source)

if msgid and msgid in translations:
translated_text = translations[msgid]
translated_text = convert_ftl_links_to_wagtail(translated_text, source_text)

po.append(
polib.POEntry(
msgid=source_text,
msgctxt=segment.context.path,
msgstr=translated_text,
)
)

return po


def build_text_to_msgid_mapping(ftl_strings: dict[str, str]) -> dict[str, str]:
"""
Build a mapping from normalized text to FTL message ID.
Expand Down
Loading
Loading