diff --git a/locale/de/LC_MESSAGES/messages.mo b/locale/de/LC_MESSAGES/messages.mo index a60a64e..9470828 100644 Binary files a/locale/de/LC_MESSAGES/messages.mo and b/locale/de/LC_MESSAGES/messages.mo differ diff --git a/locale/de/LC_MESSAGES/messages.po b/locale/de/LC_MESSAGES/messages.po index 823c51d..268f5ed 100644 --- a/locale/de/LC_MESSAGES/messages.po +++ b/locale/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: FileMorph VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-05-08 14:43+0200\n" +"POT-Creation-Date: 2026-05-08 20:50+0200\n" "PO-Revision-Date: 2026-05-08 11:00+0200\n" "Last-Translator: FileMorph \n" "Language: de\n" @@ -706,6 +706,8 @@ msgid "" "I waive my 14-day right of withdrawal and consent to immediate contract " "execution pursuant to §356 (5) BGB." msgstr "" +"Ich verzichte auf mein 14-tägiges Widerrufsrecht und stimme der " +"sofortigen Vertragsausführung gemäß § 356 Abs. 5 BGB zu." msgid "If this email exists, you'll receive a reset link shortly." msgstr "" diff --git a/locale/en/LC_MESSAGES/messages.mo b/locale/en/LC_MESSAGES/messages.mo index 01c8740..73adf18 100644 Binary files a/locale/en/LC_MESSAGES/messages.mo and b/locale/en/LC_MESSAGES/messages.mo differ diff --git a/locale/en/LC_MESSAGES/messages.po b/locale/en/LC_MESSAGES/messages.po index 6790ec5..0ef55ed 100644 --- a/locale/en/LC_MESSAGES/messages.po +++ b/locale/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: FileMorph VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-05-08 14:43+0200\n" +"POT-Creation-Date: 2026-05-08 20:50+0200\n" "PO-Revision-Date: 2026-05-07 13:43+0200\n" "Last-Translator: FULL NAME \n" "Language: en\n" diff --git a/locale/messages.pot b/locale/messages.pot index 5d88063..bcd83cb 100644 --- a/locale/messages.pot +++ b/locale/messages.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: FileMorph VERSION\n" +"Project-Id-Version: FileMorph 1.0.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-05-08 15:18+0200\n" +"POT-Creation-Date: 2026-05-08 20:50+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/scripts/i18n.py b/scripts/i18n.py index f770fe7..a750bf7 100644 --- a/scripts/i18n.py +++ b/scripts/i18n.py @@ -40,9 +40,30 @@ def _pybabel(args: list[str]) -> int: return _run([sys.executable, "-m", "babel.messages.frontend"] + args) +def _project_version() -> str: + """Read project version from pyproject.toml (single source of truth). + + Falling back to "0.0.0" if pyproject is unparseable lets the extract + succeed in degenerate environments rather than blocking the whole + i18n pipeline on a header field. + """ + pyproject = ROOT / "pyproject.toml" + if not pyproject.exists(): + return "0.0.0" + try: + for line in pyproject.read_text(encoding="utf-8").splitlines(): + stripped = line.strip() + if stripped.startswith("version") and "=" in stripped: + return stripped.split("=", 1)[1].strip().strip('"').strip("'") + except OSError: + pass + return "0.0.0" + + def cmd_extract() -> int: """Scan all sources → locale/messages.pot.""" LOCALE_DIR.mkdir(parents=True, exist_ok=True) + version = _project_version() return _pybabel( [ "extract", @@ -51,6 +72,7 @@ def cmd_extract() -> int: "-o", str(POT_FILE), "--project=FileMorph", + f"--version={version}", "--copyright-holder=FileMorph", "--no-location", "--sort-output", @@ -113,6 +135,7 @@ def cmd_drift_check() -> int: fd, tmp_name = tempfile.mkstemp(prefix="messages_drift_", suffix=".pot") os.close(fd) tmp_path = Path(tmp_name) + version = _project_version() try: rc = _pybabel( [ @@ -122,6 +145,7 @@ def cmd_drift_check() -> int: "-o", str(tmp_path), "--project=FileMorph", + f"--version={version}", "--copyright-holder=FileMorph", "--no-location", "--sort-output", diff --git a/tests/test_seo_foundation.py b/tests/test_seo_foundation.py index ba4b0ba..f7a6608 100644 --- a/tests/test_seo_foundation.py +++ b/tests/test_seo_foundation.py @@ -288,30 +288,46 @@ def test_pricing_renders_live_buttons_with_waiver_gate_when_stripe_enabled(clien waiver checkbox. The button starts as ``disabled`` and ``pricing.js`` unlocks it once the user actively ticks the matching waiver checkbox. Without this two-step consent the §356 (5) BGB waiver in ``terms.html`` - §9 cannot be enforced. We assert the structural anchors here so a future - refactor that removes the gate fails loudly. + §9 cannot be enforced. We assert the structural anchors AND the + legally load-bearing label text here so a future refactor that removes + either the gate or the §356/14-day disclosure fails loudly. """ from app.core.config import settings + from app.main import templates monkeypatch.setattr(settings, "pricing_page_enabled", True) monkeypatch.setattr(settings, "stripe_secret_key", "sk_test_dummy") # gitleaks:allow - from app.main import templates - + original_stripe_enabled = templates.env.globals.get("stripe_enabled") templates.env.globals["stripe_enabled"] = True - - r = client.get("/pricing") - assert r.status_code == 200 - body = r.text - # Each upgrade tier exposes a waiver checkbox that is wired to its button - # via the `data-target` attribute. - assert 'id="pro-waiver"' in body - assert 'data-target="pro-btn"' in body - assert 'id="business-waiver"' in body - assert 'data-target="business-btn"' in body - # The buttons themselves carry the `disabled` attribute on render — JS - # toggles it off once the checkbox is ticked. - assert 'id="pro-btn" disabled' in body - assert 'id="business-btn" disabled' in body + try: + r = client.get("/pricing") + assert r.status_code == 200 + body = r.text + # Each upgrade tier exposes a waiver checkbox that is wired to its button + # via the `data-target` attribute. + assert 'id="pro-waiver"' in body + assert 'data-target="pro-btn"' in body + assert 'id="business-waiver"' in body + assert 'data-target="business-btn"' in body + # The buttons themselves carry the `disabled` attribute on render — JS + # toggles it off once the checkbox is ticked. + assert 'id="pro-btn" disabled' in body + assert 'id="business-btn" disabled' in body + # Legal load-bearing disclosure: the visible label MUST cite §356 BGB + # and the 14-day window. A copy-edit that drops either weakens the + # consent record and breaks dispute reproducibility — pin both. + body_lower = body.lower() + assert "§356" in body or "§ 356" in body, "BGB §356 (5) reference missing from waiver label" + assert "14-day" in body_lower or "14-tägig" in body_lower or "14 day" in body_lower, ( + "14-day withdrawal-window disclosure missing from waiver label" + ) + finally: + # Jinja globals are session-shared — revert so later tests see the + # original (unset / False) state. + if original_stripe_enabled is None: + templates.env.globals.pop("stripe_enabled", None) + else: + templates.env.globals["stripe_enabled"] = original_stripe_enabled def test_navbar_omits_pricing_link_when_pricing_disabled(client, monkeypatch):