UX-13 P5 review: every STYLE rule has a mutation check, fuller jargon lists, Dutch wording, overlap partner test
checks / unit (pull_request) Successful in 10m44s
infra-pin / fresh (pull_request) Successful in 1s

- test_ui_text.py: key parity, placeholder parity, English-left-in-Dutch,
  semicolon, 25-word description and 2-sentence line each now fail on a
  spoiled input (the English-left check was not a real mutation).
- Jargon lists now cover the whole 'Not' column of STYLE.md section 4, in
  English, with the Dutch equivalents.
- nl.json: 'gewijzigd worden' where 'wijzigen' read as intransitive,
  'Bezig met bewaren' for the saving state, 'Metingen kloppen niet'.
- test_uimodel.py: an overlap that changes partners is named piece by piece
  (a surviving mutant of _tiling's partner check).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
review-agent
2026-09-28 16:54:45 +02:00
parent 667b8e9145
commit ede01b8a04
3 changed files with 71 additions and 23 deletions
+6 -6
View File
@@ -119,12 +119,12 @@
"periods": {"window": "Tijdvak", "from": "Van", "to": "Tot", "days": "Dagen", "price": "Prijs", "add": "Tijdvak toevoegen"}, "periods": {"window": "Tijdvak", "from": "Van", "to": "Tot", "days": "Dagen", "price": "Prijs", "add": "Tijdvak toevoegen"},
"reversed": "Meet negatief", "reversed": "Meet negatief",
"phase": "Fase", "phase": "Fase",
"raw_roster": {"description": "Deze lijst met omvormers kan alleen op het tabblad Configuratie wijzigen."}, "raw_roster": {"description": "Deze lijst met omvormers kan alleen op het tabblad Configuratie gewijzigd worden."},
"region_year": "cijfers van {year}", "region_year": "cijfers van {year}",
"hour": "{hour}:00 UTC", "hour": "{hour}:00 UTC",
"preset": {"description": "Ingevuld door de regio hierboven, dus kies een eigen bedrag om het te wijzigen."}, "preset": {"description": "Ingevuld door de regio hierboven, dus kies een eigen bedrag om het te wijzigen."},
"cycle": {"name": "Een cyclus met de hand starten", "description": "Dezelfde acties als op Start, die elk eerst om bevestiging vragen."}, "cycle": {"name": "Een cyclus met de hand starten", "description": "Dezelfde acties als op Start, die elk eerst om bevestiging vragen."},
"savebar": {"label": "Niet bewaarde wijzigingen", "one": "wijziging niet bewaard", "many": "wijzigingen niet bewaard", "discard": "Verwerpen", "save": "Bewaren", "saving": "Bewaren"}, "savebar": {"label": "Niet bewaarde wijzigingen", "one": "wijziging niet bewaard", "many": "wijzigingen niet bewaard", "discard": "Verwerpen", "save": "Bewaren", "saving": "Bezig met bewaren"},
"saved": {"name": "Bewaard, nog niet actief", "description": "De add-on werkt met de instellingen waarmee hij startte tot u hem herstart."}, "saved": {"name": "Bewaard, nog niet actief", "description": "De add-on werkt met de instellingen waarmee hij startte tot u hem herstart."},
"differs": {"name": "Bewaarde instellingen zijn niet actief", "description": "Sommige bewaarde instellingen verschillen van wat nu draait, dus herstart de add-on om ze toe te passen."}, "differs": {"name": "Bewaarde instellingen zijn niet actief", "description": "Sommige bewaarde instellingen verschillen van wat nu draait, dus herstart de add-on om ze toe te passen."},
"stale": {"name": "Elders gewijzigd", "description": "Deze instellingen zijn elders gewijzigd terwijl u bewerkte, dus bewaar opnieuw om de uwe te houden of verwerp om de andere te laden."}, "stale": {"name": "Elders gewijzigd", "description": "Deze instellingen zijn elders gewijzigd terwijl u bewerkte, dus bewaar opnieuw om de uwe te houden of verwerp om de andere te laden."},
@@ -134,11 +134,11 @@
"toast": {"saved": "Bewaard. Herstart om toe te passen.", "discarded": "Wijzigingen verworpen.", "failed": "Dat lukte niet. Probeer opnieuw.", "blocked": {"description": "Los de rood gemarkeerde instellingen op en bewaar opnieuw."}, "restarting": {"description": "Herstart gevraagd, dus deze pagina antwoordt even niet."}}, "toast": {"saved": "Bewaard. Herstart om toe te passen.", "discarded": "Wijzigingen verworpen.", "failed": "Dat lukte niet. Probeer opnieuw.", "blocked": {"description": "Los de rood gemarkeerde instellingen op en bewaar opnieuw."}, "restarting": {"description": "Herstart gevraagd, dus deze pagina antwoordt even niet."}},
"tier": {"bad": "Oplossen", "warn": "Nakijken", "info": "Opmerking", "ok": "In orde"}, "tier": {"bad": "Oplossen", "warn": "Nakijken", "info": "Opmerking", "ok": "In orde"},
"derive": { "derive": {
"raw_roster": {"description": "Deze lijst met omvormers kan alleen op het tabblad Configuratie wijzigen."}, "raw_roster": {"description": "Deze lijst met omvormers kan alleen op het tabblad Configuratie gewijzigd worden."},
"no_inverter": {"description": "Voeg minstens één omvormer toe."}, "no_inverter": {"description": "Voeg minstens één omvormer toe."},
"flat_named": {"description": "Voeg een tweede omvormer toe voor u deze een naam geeft."}, "flat_named": {"description": "Voeg een tweede omvormer toe voor u deze een naam geeft."},
"no_source": {"description": "Kies waar de dynamische prijzen vandaan komen."}, "no_source": {"description": "Kies waar de dynamische prijzen vandaan komen."},
"custom_profile": {"description": "Deze tijdvakken kunnen alleen op het tabblad Configuratie wijzigen."}, "custom_profile": {"description": "Deze tijdvakken kunnen alleen op het tabblad Configuratie gewijzigd worden."},
"no_price": {"description": "Vul een prijs per kWh in."}, "no_price": {"description": "Vul een prijs per kWh in."},
"no_periods": {"description": "Voeg minstens één tijdvak toe."}, "no_periods": {"description": "Voeg minstens één tijdvak toe."},
"period_time": {"description": "Tijdvak {row}: schrijf elk tijdstip als uren en minuten, zoals 07:00 of 24:00."}, "period_time": {"description": "Tijdvak {row}: schrijf elk tijdstip als uren en minuten, zoals 07:00 of 24:00."},
@@ -151,7 +151,7 @@
}, },
"setup": { "setup": {
"name": "Installatie", "name": "Installatie",
"description": "Voor installateurs: zes stappen naar een werkende installatie, en elke stap kan later wijzigen onder Instellingen.", "description": "Voor installateurs: zes stappen naar een werkende installatie, en elke stap kan later gewijzigd worden onder Instellingen.",
"done_before": {"description": "Deze installatie is al ingesteld, dus een wijziging hier vervangt de bewaarde instellingen."}, "done_before": {"description": "Deze installatie is al ingesteld, dus een wijziging hier vervangt de bewaarde instellingen."},
"step": "Stap {n} van {total}", "step": "Stap {n} van {total}",
"back": "Terug", "back": "Terug",
@@ -209,7 +209,7 @@
"verdict": {"held": "Op 0 W gehouden", "stopped": "Gestopt", "cycle": "Vandaag maandelijkse cyclus", "trade": "Handelt op prijs", "house": "Volgt het huis"}, "verdict": {"held": "Op 0 W gehouden", "stopped": "Gestopt", "cycle": "Vandaag maandelijkse cyclus", "trade": "Handelt op prijs", "house": "Volgt het huis"},
"pill": {"running": "Actief", "shadow": "Schaduwmodus", "stopped": "Gestopt", "alarm": "Heeft u nu nodig", "warning": "Actief, met een waarschuwing"}, "pill": {"running": "Actief", "shadow": "Schaduwmodus", "stopped": "Gestopt", "alarm": "Heeft u nu nodig", "warning": "Actief, met een waarschuwing"},
"banner_sub": {"running": "De batterijen volgen het plan hieronder.", "shadow": "De plannen hieronder tonen wat de add-on zou doen.", "stopped": "Start de regeling hieronder als u klaar bent.", "alarm": "Zie Vraagt aandacht hieronder voor wat u moet doen.", "warning": "Zie Vraagt aandacht hieronder."}, "banner_sub": {"running": "De batterijen volgen het plan hieronder.", "shadow": "De plannen hieronder tonen wat de add-on zou doen.", "stopped": "Start de regeling hieronder als u klaar bent.", "alarm": "Zie Vraagt aandacht hieronder voor wat u moet doen.", "warning": "Zie Vraagt aandacht hieronder."},
"tile": {"not_set_up": "Niet ingesteld", "no_reading": "Geen meting", "producing": "Produceert", "not_producing": "Produceert niet", "needs_solar": "Zonnemeting nodig", "not_adding_up": "Metingen kloppen niet samen", "using": "Verbruikt", "taking": "Neemt af van het net", "feeding": "Injecteert", "balanced": "In evenwicht", "giving": "Levert stroom", "charging": "Laadt", "resting": "Rust"}, "tile": {"not_set_up": "Niet ingesteld", "no_reading": "Geen meting", "producing": "Produceert", "not_producing": "Produceert niet", "needs_solar": "Zonnemeting nodig", "not_adding_up": "Metingen kloppen niet", "using": "Verbruikt", "taking": "Neemt af van het net", "feeding": "Injecteert", "balanced": "In evenwicht", "giving": "Levert stroom", "charging": "Laadt", "resting": "Rust"},
"now": { "now": {
"no_reading": "Geen meting van deze batterij.", "no_reading": "Geen meting van deze batterij.",
"would_rest": "{zero}. Zou nu rusten.", "would_rest": "{zero}. Zou nu rusten.",
+58 -17
View File
@@ -68,18 +68,30 @@ def where(lang, path):
# ============================================================================ # # ============================================================================ #
print("1. English and Dutch have the same keys and placeholders") print("1. English and Dutch have the same keys and placeholders")
_en, _nl = dict(leaves(EN)), dict(leaves(NL)) _en, _nl = dict(leaves(EN)), dict(leaves(NL))
_only_en = sorted(where("en", p) for p in _en if p not in _nl)
_only_nl = sorted(where("nl", p) for p in _nl if p not in _en)
def missing(en, nl):
"""The keys only one language has."""
return (sorted(where("en", p) for p in en if p not in nl),
sorted(where("nl", p) for p in nl if p not in en))
def holes(en, nl):
"""The Dutch strings whose {placeholders} differ from the English."""
return sorted(
f"{where('nl', p)}: {sorted(PLACEHOLDER.findall(str(en[p])))} vs "
f"{sorted(PLACEHOLDER.findall(str(t)))}"
for p, t in nl.items() if p in en
and sorted(PLACEHOLDER.findall(str(t)))
!= sorted(PLACEHOLDER.findall(str(en[p]))))
_only_en, _only_nl = missing(_en, _nl)
for x in _only_en + _only_nl: for x in _only_en + _only_nl:
print(f" MISSING in the other language: {x}") print(f" MISSING in the other language: {x}")
check(f"every one of the {len(_en)} English strings has a Dutch one, and no " check(f"every one of the {len(_en)} English strings has a Dutch one, and no "
"Dutch string is extra", not _only_en and not _only_nl and len(_en) > 300) "Dutch string is extra", not _only_en and not _only_nl and len(_en) > 300)
_holes = sorted( _holes = holes(_en, _nl)
f"{where('nl', p)}: {sorted(PLACEHOLDER.findall(str(_en[p])))} vs "
f"{sorted(PLACEHOLDER.findall(str(t)))}"
for p, t in _nl.items() if p in _en
and sorted(PLACEHOLDER.findall(str(t)))
!= sorted(PLACEHOLDER.findall(str(_en[p]))))
for x in _holes: for x in _holes:
print(f" PLACEHOLDERS differ: {x}") print(f" PLACEHOLDERS differ: {x}")
check("every Dutch string fills the same {placeholders} as its English one", check("every Dutch string fills the same {placeholders} as its English one",
@@ -94,10 +106,14 @@ print("\n2. every string follows STYLE.md, in both languages")
JARGON = { JARGON = {
"en": r"controllers?|control loop|setpoints?|SoC|state of charge|BMS|PV|" "en": r"controllers?|control loop|setpoints?|SoC|state of charge|BMS|PV|"
r"failsafe|trip(?:ped)?|parked|discharg\w*|P1 feed|quarter-hour|" r"failsafe|trip(?:ped)?|parked|discharg\w*|P1 feed|quarter-hour|"
r"payload|endpoint|JSON|API|timeout|boolean|config", r"payload|endpoint|JSON|API|timeout|boolean|config|commands?|"
"nl": r"setpoints?|SoC|BMS|PV|failsafe|regelaar|regellus|" r"charge level|ceiling|billed quarter|quarters?|banked|soft limit|"
r"latch|ontlad\w*|payload|endpoint|JSON|API|time-?out|boolean|" r"open block|actuator|ESP32|master|meter source|circulating|"
r"config|kwartuur\w*", r"sign disagreement|rail",
"nl": r"setpoints?|setpunt\w*|SoC|laadtoestand|BMS|PV|failsafe|regelaar|"
r"regellus|controller|latch|ontlad\w*|payload|endpoint|JSON|API|"
r"time-?out|boolean|config|kwartuur\w*|commando\w*|geparkeerd|"
r"ESP32|master",
} }
JARGON = {k: re.compile(r"\b(?:" + v + r")\b", re.I) for k, v in JARGON.items()} JARGON = {k: re.compile(r"\b(?:" + v + r")\b", re.I) for k, v in JARGON.items()}
# Details belong behind the page's DOCS link, never in the string itself. # Details belong behind the page's DOCS link, never in the string itself.
@@ -167,6 +183,22 @@ check("a link or section in a string is caught",
"See https://example.org for the rest.")) "See https://example.org for the rest."))
check("a snake_case option key is caught", check("a snake_case option key is caught",
problems("nl", ("x",), "Zet peak_cap_w hoger")) problems("nl", ("x",), "Zet peak_cap_w hoger"))
check("a semicolon is caught",
problems("en", ("x",), "Saved; restart to apply"))
check("a 26-word description is caught, and 25 words pass",
problems("en", ("x", "description"), " ".join(["word"] * 26) + ".")
and not problems("en", ("x", "description"),
" ".join(["word"] * 25) + "."))
check("a 3-sentence line is caught",
problems("en", ("x",), "One. Two. Three."))
check("a key only one language has is caught, both ways",
missing({("a",): "x", ("b",): "y"}, {("a",): "x"}) == (["en.json:b"], [])
and missing({("a",): "x"}, {("a",): "x", ("c",): "z"})
== ([], ["nl.json:c"]))
check("a placeholder dropped or renamed in Dutch is caught",
holes({("a",): "Window {row}"}, {("a",): "Tijdvak"})
and holes({("a",): "{w} now"}, {("a",): "nu {d}"})
and not holes({("a",): "{a} {b}"}, {("a",): "{b} {a}"}))
# ============================================================================ # # ============================================================================ #
@@ -219,14 +251,23 @@ print("\n5. no Dutch string is left in English")
# Words that are the same in both, or names. Anything else equal is untranslated. # Words that are the same in both, or names. Anything else equal is untranslated.
SAME = {"Meter", "Timing", "Support", "Details", "Alarm", "Info", "per kWh", SAME = {"Meter", "Timing", "Support", "Details", "Alarm", "Info", "per kWh",
"{hour}:00 UTC", "Flanders, Fluvius Antwerpen", "Modus", "Status"} "{hour}:00 UTC", "Flanders, Fluvius Antwerpen", "Modus", "Status"}
_same = sorted(f"{where('nl', p)} = {t!r}" for p, t in _nl.items()
if p in _en and t == _en[p] and t not in SAME
and PLACEHOLDER.sub("", t).strip(" .:,")) def english(en, nl):
return sorted(f"{where('nl', p)} = {t!r}" for p, t in nl.items()
if p in en and t == en[p] and t not in SAME
and PLACEHOLDER.sub("", t).strip(" .:,"))
_same = english(_en, _nl)
for x in _same: for x in _same:
print(f" ENGLISH {x}") print(f" ENGLISH {x}")
check(f"every Dutch string is Dutch ({len(_same)} still English)", not _same) check(f"every Dutch string is Dutch ({len(_same)} still English)", not _same)
check("mutation: an English string in nl.json is caught", _spoiled = dict(_nl)
"Save" != NL["settings"]["savebar"]["save"] and "Save" not in SAME) _spoiled[("settings", "savebar", "save")] = _en[("settings", "savebar",
"save")]
check("mutation: an English string left in nl.json is caught",
english(_en, _spoiled) == ["nl.json:settings.savebar.save = 'Save'"])
# ============================================================================ # # ============================================================================ #
+7
View File
@@ -283,6 +283,13 @@ check("a gap is named: the day and the times",
refused(rows(("07:00", "22:00", "mon-sun", 0.3), refused(rows(("07:00", "22:00", "mon-sun", 0.3),
("22:00", "24:00", "mon-sun", 0.2))) ("22:00", "24:00", "mon-sun", 0.2)))
== ("period_gap", {"day": "mon", "from": "00:00", "to": "07:00"})) == ("period_gap", {"day": "mon", "from": "00:00", "to": "07:00"}))
check("an overlap that changes partners is named piece by piece: windows 1 "
"and 2 first, over their own times only",
refused(rows(("00:00", "24:00", "mon-sun", 0.3),
("06:00", "08:00", "mon-sun", 0.2),
("08:00", "10:00", "mon-sun", 0.2)))
== ("period_overlap", {"day": "mon", "from": "06:00", "to": "08:00",
"a": 1, "b": 2}))
check("a gap on one day only is found on that day", check("a gap on one day only is found on that day",
refused(rows(("00:00", "24:00", "mon-fri", 0.3), refused(rows(("00:00", "24:00", "mon-fri", 0.3),
("00:00", "24:00", "sat", 0.2))) ("00:00", "24:00", "sat", 0.2)))