Files
ping-mee-self-service-platform/portal/tests/test_translations.py
ebbe.bassandClaude Sonnet 5.5 49c7fd4a0b feat: add password change
Password page with the AD policy listed first, ordered service (throttle limit below the lockout threshold, verification, local hints, directory change, ended sessions, audit and confirmation mail in one transaction), plain-language errors, German text and tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
2026-10-18 10:08:17 +02:00

121 lines
4.6 KiB
Python

"""Every user-visible string has German text (AD-15); the informal catalogue only differs."""
import ast
import re
from pathlib import Path
import polib
import pytest
PORTAL = Path(__file__).resolve().parent.parent
LOCALE = PORTAL / "locale"
TRANS_TAG = re.compile(r"""\{%\s*trans\s+(?:"([^"]+)"|'([^']+)')""")
BLOCKTRANS = re.compile(r"\{%\s*blocktrans[^%]*%\}(.*?)\{%\s*endblocktrans\s*%\}", re.S)
VARIABLE = re.compile(r"\{\{\s*(\w+)\s*\}\}")
SINGLE = {"_", "gettext", "gettext_lazy"}
PLURAL = {"ngettext", "ngettext_lazy"}
def _call_name(node: ast.Call) -> str:
func = node.func
return func.id if isinstance(func, ast.Name) else getattr(func, "attr", "")
def _strings(node: ast.Call, count: int) -> tuple[str, ...] | None:
args = node.args[:count]
if len(args) == count and all(
isinstance(a, ast.Constant) and isinstance(a.value, str) for a in args
):
return tuple(a.value for a in args)
return None
def messages_in_code() -> set[tuple[str, str | None]]:
"""(msgid, msgid_plural) for every translatable string in templates and Python code."""
found: set[tuple[str, str | None]] = set()
templates = [*(PORTAL / "templates").rglob("*.html"), *(PORTAL / "templates").rglob("*.txt")]
for template in templates:
text = template.read_text(encoding="utf-8")
found.update((a or b, None) for a, b in TRANS_TAG.findall(text))
found.update(
(VARIABLE.sub(r"%(\1)s", body).strip(), None) for body in BLOCKTRANS.findall(text)
)
for source in PORTAL.rglob("*.py"):
if "tests" in source.parts or "migrations" in source.parts:
continue
for node in ast.walk(ast.parse(source.read_text(encoding="utf-8"))):
if not isinstance(node, ast.Call):
continue
name = _call_name(node)
if name in SINGLE and (strings := _strings(node, 1)):
found.add((strings[0], None))
elif name in PLURAL and (strings := _strings(node, 2)):
found.add((strings[0], strings[1]))
return found
def msgids_in_code() -> set[str]:
return {msgid for msgid, _ in messages_in_code()}
def catalogue(name: str) -> polib.POFile:
return polib.pofile(str(LOCALE / name / "LC_MESSAGES" / "django.po"))
def texts(entry: polib.POEntry) -> list[str]:
"""Every translated text of an entry: one, or one per plural form."""
return list(entry.msgstr_plural.values()) if entry.msgid_plural else [entry.msgstr]
def test_the_scan_finds_the_strings():
messages = messages_in_code()
assert {"Sign out", "Home", "Hello, %(name)s", "Audit log"} <= msgids_in_code()
assert ("At least %(n)s character", "At least %(n)s characters") in messages
def test_every_string_in_the_code_has_german_text():
german = {e.msgid: e for e in catalogue("de")}
missing = sorted(msgids_in_code() - german.keys())
assert not missing, f"no German entry for: {missing}"
for entry in german.values():
assert all(t.strip() for t in texts(entry)), f"empty German text for {entry.msgid!r}"
assert "fuzzy" not in entry.flags, f"fuzzy entry {entry.msgid!r}"
def test_plural_strings_are_plural_entries_with_both_forms():
german = {e.msgid: e for e in catalogue("de")}
for msgid, plural in messages_in_code():
if plural:
entry = german[msgid]
assert entry.msgid_plural == plural and len(entry.msgstr_plural) == 2, msgid
def test_the_german_catalogue_has_no_stale_entries():
stale = sorted({e.msgid for e in catalogue("de")} - msgids_in_code())
assert not stale, f"entries no longer used: {stale}"
def test_the_informal_catalogue_only_holds_differences_to_known_strings():
german = {e.msgid: texts(e) for e in catalogue("de")}
for entry in catalogue("de_DU"):
assert entry.msgid in german, f"unknown string {entry.msgid!r}"
assert all(t.strip() for t in texts(entry)) and "fuzzy" not in entry.flags
assert texts(entry) != german[entry.msgid], f"{entry.msgid!r} does not differ"
@pytest.mark.parametrize("name", ["de", "de_DU"])
def test_placeholders_survive_translation(name):
pattern = re.compile(r"%\(\w+\)s")
for entry in catalogue(name):
wanted = set(pattern.findall(entry.msgid))
for text in texts(entry):
assert set(pattern.findall(text)) == wanted, entry.msgid
def test_the_formal_text_does_not_use_du_and_the_informal_does_not_use_sie():
formal = " ".join(t for e in catalogue("de") for t in texts(e))
assert not re.search(r"\b(du|dir|dich|dein|deine)\b", formal, re.I)
informal = " ".join(t for e in catalogue("de_DU") for t in texts(e))
assert not re.search(r"\b(Sie|Ihnen|Ihr|Ihre)\b", informal)