v0.8.82-beta - "Poort {port} is vrij" stond er letterlijk
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
This commit is contained in:
Ramon 2026-08-02 12:57:14 +02:00
parent d7c4682cc7
commit b6319e8fdd
4 changed files with 103 additions and 3 deletions

View file

@ -1,3 +1,16 @@
# v0.8.82-beta — "Poort {port} is vrij" stond er letterlijk
De poortcontrole werd aangesloten op vertaalsleutels die er al waren, en twee
daarvan verwachten een plaatshouder die niet werd meegegeven: `port_free` wil
`{port}`, `port_in_use` wil `{port}` én `{by}`. In beeld stond dus letterlijk
"Poort {port} is vrij".
Er draait nu een test die elke `t('sleutel', { … })` in de interface naloopt en
vergelijkt met de plaatshouders in de Nederlandse tekst — in beide richtingen,
en ook voor aanroepen zónder variabelen. Dat laatste was nodig: de eerste versie
van die test keek alleen naar aanroepen die wél iets meegaven, en liet de fout
waar hij voor gemaakt was gewoon passeren.
# v0.8.81-beta — Poorten kiezen zonder gokken, en installeren op een telefoon
**De poortcontrole zat er wel, maar werd nooit gevraagd.** `/api/ports/check`

View file

@ -1 +1 @@
0.8.81-beta
0.8.82-beta

View file

@ -3282,11 +3282,15 @@ function app() {
if (!c.free) {
return c.used_by
? this.t('port_used_by', { port: c.port, container: c.used_by })
: this.t('port_in_use', { port: c.port });
// `by` staat in de tekst voor het geval we wél weten waardoor; hier
// weten we dat niet, dus leeg. Weglaten zou '{by}' in beeld zetten.
: this.t('port_in_use', { port: c.port, by: '' });
}
// Zonder de hulpcontainer zien we alleen Docker; dan is "vrij" een
// aanname en dat hoort de melding te zeggen.
return c.host_checked ? this.t('port_free') : this.t('port_free_docker_only');
return c.host_checked
? this.t('port_free', { port: c.port })
: this.t('port_free_docker_only');
},
// Bij het openen van een stap meteen controleren wat er in de poortvelden
// staat, in plaats van te wachten tot iemand iets typt.

View file

@ -128,3 +128,86 @@ 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