All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 17m4s
- De poortcontrole werd aangesloten op vertaalsleutels die er al waren, en twee
daarvan verwachten een plaatshouder die ik niet meegaf: port_free wil {port},
port_in_use wil {port} en {by}
- Nieuwe test die elke t('sleutel', {...}) in de interface vergelijkt met de
plaatshouders in de Nederlandse tekst, in beide richtingen en ook voor
aanroepen zonder variabelen. Die laatste toevoeging was nodig: de eerste
versie van de test liet juist de fout passeren waar hij voor gemaakt was
- De test kent nu ook de verkorte objectnotatie van JavaScript ({name} betekent
{name: name}); zonder dat meldde hij vier bestaande aanroepen ten onrechte
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
213 lines
8.2 KiB
Python
213 lines
8.2 KiB
Python
"""Vertalingen: nl en en moeten dezelfde sleutels hebben, en de UI mag geen
|
|
sleutels gebruiken die nergens gedefinieerd zijn.
|
|
|
|
Zonder deze test loopt de vertaling stilletjes scheef zodra iemand een scherm
|
|
toevoegt — precies wat er bij de auth-, netwerk- en update-schermen gebeurde.
|
|
"""
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
WORTEL = Path(__file__).resolve().parent.parent / "server-up"
|
|
VERTALINGEN = WORTEL / "translations"
|
|
TEMPLATE = WORTEL / "templates" / "index.html"
|
|
|
|
|
|
def _laad(code: str) -> dict:
|
|
data = json.loads((VERTALINGEN / f"{code}.json").read_text(encoding="utf-8"))
|
|
return {k: v for k, v in data.items() if not k.startswith("_")}
|
|
|
|
|
|
def test_nl_en_en_hebben_dezelfde_sleutels():
|
|
nl, en = _laad("nl"), _laad("en")
|
|
alleen_nl = sorted(set(nl) - set(en))
|
|
alleen_en = sorted(set(en) - set(nl))
|
|
assert not alleen_nl, f"Ontbreekt in en.json: {alleen_nl}"
|
|
assert not alleen_en, f"Ontbreekt in nl.json: {alleen_en}"
|
|
|
|
|
|
@pytest.mark.parametrize("code", ["nl", "en"])
|
|
def test_geen_lege_vertalingen(code):
|
|
leeg = [k for k, v in _laad(code).items() if not str(v).strip()]
|
|
assert not leeg, f"Lege waarden in {code}.json: {leeg}"
|
|
|
|
|
|
@pytest.mark.parametrize("code", ["nl", "en"])
|
|
def test_meta_is_compleet(code):
|
|
data = json.loads((VERTALINGEN / f"{code}.json").read_text(encoding="utf-8"))
|
|
meta = data.get("_meta", {})
|
|
assert meta.get("code") == code
|
|
assert meta.get("name")
|
|
|
|
|
|
def test_plaatshouders_komen_overeen():
|
|
"""`{user}` in het Nederlands moet ook in het Engels staan, anders valt er
|
|
bij het vertalen een variabele weg."""
|
|
nl, en = _laad("nl"), _laad("en")
|
|
for sleutel, tekst in nl.items():
|
|
vars_nl = set(re.findall(r"\{(\w+)\}", str(tekst)))
|
|
vars_en = set(re.findall(r"\{(\w+)\}", str(en.get(sleutel, ""))))
|
|
assert vars_nl == vars_en, f"{sleutel}: nl={vars_nl} en={vars_en}"
|
|
|
|
|
|
def test_ui_gebruikt_alleen_bestaande_sleutels():
|
|
regels = TEMPLATE.read_text(encoding="utf-8").splitlines()
|
|
# Commentaarregels overslaan: die noemen t('key') als voorbeeld.
|
|
html = "\n".join(r for r in regels if not r.lstrip().startswith(("//", "*", "/*")))
|
|
# Alleen letterlijke sleutels: `t('foo')` en `t('foo', {...})`. Dynamisch
|
|
# samengestelde sleutels zoals t('role_'+r) vallen hierbuiten en worden
|
|
# afzonderlijk gecontroleerd in test_dynamische_sleutels_bestaan.
|
|
gebruikt = set(re.findall(r"\bt\('([a-z0-9_]+)'\s*[,)]", html))
|
|
bekend = set(_laad("nl"))
|
|
onbekend = sorted(gebruikt - bekend)
|
|
assert not onbekend, f"t() verwijst naar niet-bestaande sleutels: {onbekend}"
|
|
|
|
|
|
def test_dynamische_sleutels_bestaan():
|
|
"""Sleutels die de UI opbouwt uit een voorvoegsel plus een waarde."""
|
|
nl = _laad("nl")
|
|
for rol in ("admin", "operator", "viewer"):
|
|
assert f"role_{rol}" in nl
|
|
for plan in ("off", "daily", "weekly"):
|
|
assert f"backup_schedule_{plan}" in nl
|
|
|
|
|
|
def test_nieuwe_schermen_zijn_vertaald():
|
|
"""Steekproef op de schermen die in v0.5.x zijn toegevoegd."""
|
|
nl = _laad("nl")
|
|
for sleutel in ("login", "setup_title", "security", "auth_mode",
|
|
"trusted_proxies", "networks", "own_ip_hint", "channel",
|
|
"update_now", "rollback", "port_taken", "loading"):
|
|
assert sleutel in nl, f"vertaalsleutel ontbreekt: {sleutel}"
|
|
|
|
|
|
def test_i18n_module_leest_beide_talen(env):
|
|
from core import i18n
|
|
i18n.load()
|
|
codes = {t["code"] for t in i18n.available()}
|
|
assert {"nl", "en"} <= codes
|
|
# strings() valt terug op het Engels voor ontbrekende sleutels.
|
|
assert i18n.strings("nl")["login"]
|
|
assert i18n.strings("en")["login"]
|
|
|
|
|
|
def test_elke_auditcategorie_heeft_een_nederlandse_naam():
|
|
"""Het auditlog toonde ruwe id's als 'store' en 'reconfigure'.
|
|
|
|
Die worden nu vertaald met `audit_cat_<x>` en `audit_act_<x>`. Voegt iemand
|
|
een nieuwe `audit.log(...)` toe zonder vertaling, dan staat er weer een ruwe
|
|
id in beeld — vandaar dat deze test de broncode afloopt.
|
|
"""
|
|
import re
|
|
nl = _laad("nl")
|
|
bron = ""
|
|
for pad in WORTEL.rglob("*.py"):
|
|
bron += pad.read_text(encoding="utf-8")
|
|
|
|
# audit.log("categorie", "actie", ...) — alleen de letterlijke waarden;
|
|
# aanroepen met een variabele (containers/stacks) staan hieronder apart.
|
|
paren = re.findall(r'audit\.log\(\s*"([a-z0-9_]+)"\s*,\s*"([a-z0-9_]+)"', bron)
|
|
assert paren, "geen audit.log-aanroepen gevonden — is de vorm veranderd?"
|
|
|
|
ontbreekt = []
|
|
for cat, act in set(paren):
|
|
if f"audit_cat_{cat}" not in nl:
|
|
ontbreekt.append(f"audit_cat_{cat}")
|
|
if f"audit_act_{act}" not in nl:
|
|
ontbreekt.append(f"audit_act_{act}")
|
|
# De acties die als variabele worden meegegeven: stack- en containerknoppen.
|
|
for act in ("start", "stop", "restart", "up", "down", "update", "backup"):
|
|
if f"audit_act_{act}" not in nl:
|
|
ontbreekt.append(f"audit_act_{act}")
|
|
assert not ontbreekt, f"geen vertaling voor: {sorted(set(ontbreekt))}"
|
|
|
|
|
|
def test_auditnamen_bestaan_in_beide_talen():
|
|
nl, en = _laad("nl"), _laad("en")
|
|
alleen_nl = sorted(k for k in nl if k.startswith("audit_") and k not in en)
|
|
assert not alleen_nl, f"ontbreken in en.json: {alleen_nl}"
|
|
|
|
|
|
def _meegegeven(blok: str) -> set[str]:
|
|
"""De sleutels uit een `{…}` in een t()-aanroep.
|
|
|
|
JavaScript kent twee vormen: `{port: c.port}` en de verkorte `{name}`, die
|
|
hetzelfde betekent als `{name: name}`. Alleen op een dubbele punt zoeken
|
|
mist die tweede, en dan meldt de test fouten die er niet zijn.
|
|
"""
|
|
uit, huidig, diepte = set(), "", 0
|
|
for teken in blok + ",":
|
|
if teken in "([{":
|
|
diepte += 1
|
|
elif teken in ")]}":
|
|
diepte -= 1
|
|
if teken == "," and diepte == 0:
|
|
deel = huidig.strip()
|
|
if deel:
|
|
naam = deel.split(":", 1)[0].strip()
|
|
if re.fullmatch(r"\w+", naam):
|
|
uit.add(naam)
|
|
huidig = ""
|
|
else:
|
|
huidig += teken
|
|
return uit
|
|
|
|
|
|
def _t_aanroepen(html: str):
|
|
"""Elke `t('sleutel', { … })` met de meegegeven sleutels."""
|
|
for m in re.finditer(r"\bt\(\s*'([a-z0-9_]+)'\s*,\s*\{([^{}]*)\}", html):
|
|
yield m.group(1), _meegegeven(m.group(2))
|
|
|
|
|
|
def test_elke_plaatshouder_wordt_ook_meegegeven():
|
|
"""`t('port_free')` op een tekst met `{port}` erin zet letterlijk "{port}"
|
|
in beeld.
|
|
|
|
Dat is precies wat er gebeurde toen de poortcontrole werd aangesloten op
|
|
sleutels die er al waren: `port_free` verwacht `{port}`, `port_in_use`
|
|
verwacht `{port}` én `{by}`, en die werden niet meegegeven.
|
|
"""
|
|
nl = _laad("nl")
|
|
html = (WORTEL / "templates" / "index.html").read_text(encoding="utf-8")
|
|
aanroepen = list(_t_aanroepen(html))
|
|
assert aanroepen, "geen t()-aanroepen met variabelen gevonden — vorm veranderd?"
|
|
|
|
fouten = []
|
|
for sleutel, gegeven in aanroepen:
|
|
tekst = nl.get(sleutel)
|
|
if tekst is None:
|
|
continue # andere test dekt onbekende sleutels
|
|
ontbreekt = set(re.findall(r"\{(\w+)\}", tekst)) - gegeven
|
|
if ontbreekt:
|
|
fouten.append(f"{sleutel}: mist {sorted(ontbreekt)}")
|
|
|
|
# En het geval dat de eerste versie van deze test miste: een aanroep zónder
|
|
# variabelen op een tekst die er wél verwacht. Dat was precies de fout —
|
|
# `t('port_free')` op "Poort {port} is vrij".
|
|
for m in re.finditer(r"\bt\(\s*'([a-z0-9_]+)'\s*\)", html):
|
|
sleutel = m.group(1)
|
|
tekst = nl.get(sleutel)
|
|
if tekst is None:
|
|
continue
|
|
nodig = sorted(set(re.findall(r"\{(\w+)\}", tekst)))
|
|
if nodig:
|
|
fouten.append(f"{sleutel}: aangeroepen zonder {nodig}")
|
|
assert not fouten, fouten
|
|
|
|
|
|
def test_geen_plaatshouder_zonder_tekst():
|
|
"""Andersom: een sleutel meegeven die niet in de tekst staat is meestal een
|
|
hernoeming die half is doorgevoerd."""
|
|
nl = _laad("nl")
|
|
html = (WORTEL / "templates" / "index.html").read_text(encoding="utf-8")
|
|
fouten = []
|
|
for sleutel, gegeven in _t_aanroepen(html):
|
|
tekst = nl.get(sleutel)
|
|
if tekst is None:
|
|
continue
|
|
over = gegeven - set(re.findall(r"\{(\w+)\}", tekst))
|
|
if over:
|
|
fouten.append(f"{sleutel}: geeft {sorted(over)} mee, staat niet in de tekst")
|
|
assert not fouten, fouten
|