All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 14m6s
- Het auditlog toonde ruwe id's als 'store', 'reconfigure' en 'credentials_view'. Die hebben nu namen, ook in de filterlijsten, en de kolomkoppen zijn vertaald in plaats van hardgecodeerd Nederlands. Onbekende waarden vallen terug op de ruwe tekst zodat een module niets kapotmaakt - Nieuwe test die de broncode afloopt en elke audit.log() zonder vertaling aanwijst - Een modulepagina heette in de titelbalk letterlijk 'mod:mijnmodule' - De store herkende een installatie aan de mapnaam: een stack telde alleen mee als hij 'sonarr' of 'sonarr-...' heette. Wie zijn stack 'media-tv' noemde kreeg te zien dat Sonarr niet geinstalleerd was. Nu via de source uit .serverup.json, met een duidelijk Geinstalleerd-label per installatie waar je op kunt klikken om ernaartoe te gaan - Twaalf icoonknoppen per stack werden op een telefoon drie rijen plaatjes zonder naam. Daar blijven starten, herstarten en logs staan, plus een menu met de rest, mét namen. Op een breed scherm verandert er niets - De rij van twintig categorieknoppen boven de stacks is hetzelfde compacte menu geworden als in de app store Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
130 lines
5.1 KiB
Python
130 lines
5.1 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}"
|