feat(l10n): een poort die vertaalsleutels aanwijst die niemand meer opvraagt #1516

Merged
brenno merged 4 commits from feat/l10n-wezenpoort into main 2026-08-17 10:28:09 +00:00
Owner

Sluit #1512.

Waarom

Bij het opruimen van de weesgeraakte sleutel Snijtekens (#1505) bleek een ruwe grep 249 kandidaten te geven. Dat getal was onbruikbaar: sleutels die via een variabele of generator worden opgehaald zien er in zo'n scan uit als wees terwijl ze het niet zijn. Elke wees kost wél 31 vertalingen aan onderhoud.

Hoe de controle bewijst dat een sleutel gebruikt wordt

tool/check_l10n_orphans.dart, Flutter-vrij (dart:io + package:analyzer), ~20 s.

De sleutelvoorraad komt via de AST uit de drie tabellen in en.dart (3.522 sleutels). Bewijs van gebruik in twee lagen: eerst elke Dart-stringliteral in lib/, test/, tool/ — AST-gemeten, dus inclusief aaneengeschakelde literals en de tekstdelen van interpolaties, wat doorgeefluiken (EditorField(label: 'Titel')) en registerregels (labelKey: 'applicationLanguage') meepakt. Alleen over wat dán nog overblijft de dure tekstlaag over assets/ en web/.

Die tweede laag bleek nodig: er is een derde ophaalweg die niemand op papier had staan — AppLocalizations.sourceFor(lang, labelNl) in de canvas-/matrix-/tree-/flow-specs, gevoed uit assets/improvement/templates/. "Bedreigingen" staat in geen enkele Dart-literal en is wél in gebruik.

Wat bewust géén bewijs is

docs/, README en CHANGELOG (proza beschrijft de app — twee van de drie handmatig bevestigde wezen staan in de CHANGELOG en zouden anders wegvallen), de *_l10n_spec.json-bestanden (die hébben de sleutel gemaakt), de vertaaltabellen zelf, en de poort plus haar eigen test. Dat laatste is geen theorie: settingsLogo verdween uit de bevindingen zodra hij in een commentaarzin van de poort stond.

Uitkomst: 181 wezen van 3.522

4 in _stringsEn (t-sleutels), 48 en 129 in de twee Nederlandse tabellen. Grote clusters: ~22 OpenKAT-rapportbeschrijvingen, ~18 opslagteksten, 11 "(ontwerp)"-moduletegels, de teksten van het geschrapte actions-slidetype (#417), en herschreven zinnen waarvan de oude versie bleef staan. Steekproef van tien: allemaal echt wees.

181 is een ondergrens, geen telling: een sleutel die als deeltekst van een andere string voorkomt wordt niet gemeld. De poort mag zwijgen over een wees, maar mag nooit iemand op zoektocht sturen naar een sleutel die wél gebruikt wordt.

In check-full, met een ratchet

Het bewijs is tekstueel, geen typecontrole: de poort weet dat een sleutel ergens vóórkomt, niet dat hij wordt uitgevoerd. Zo'n oordeel hoort niet in de poort die élke commit tegenhoudt. Wat wél tanden heeft is orphanBaseline = 181 — mag dalen, nooit stijgen — plus opname in make ratchets.

Getoetst in twee richtingen

test/l10n_orphan_check_test.dart (13 tests): vijf bouwen een mini-repo met een sleutel die wél gebruikt wordt (via d(), t(), een doorgeefluik, een aaneengeschakelde literal, een asset-label) en eisen nul bevindingen; vier planten een wees en eisen alarm, ook als alleen documentatie of alleen een spec-bestand hem noemt. Vier meten op de echte boom. Zonder die tweede richting commit je een poort die stilzwijgend niets doet — dat is hier eerder bijna gebeurd.

Opruimen van de 181 is bewust een volgende stap.

Poorten

make check groen. make check-secrets groen, make sast groen (0 findings).

Sluit #1512. ## Waarom Bij het opruimen van de weesgeraakte sleutel `Snijtekens` (#1505) bleek een ruwe grep 249 kandidaten te geven. Dat getal was onbruikbaar: sleutels die via een variabele of generator worden opgehaald zien er in zo'n scan uit als wees terwijl ze het niet zijn. Elke wees kost wél 31 vertalingen aan onderhoud. ## Hoe de controle bewijst dat een sleutel gebruikt wordt `tool/check_l10n_orphans.dart`, Flutter-vrij (`dart:io` + `package:analyzer`), ~20 s. De sleutelvoorraad komt via de AST uit de drie tabellen in `en.dart` (3.522 sleutels). Bewijs van gebruik in twee lagen: eerst elke Dart-stringliteral in `lib/`, `test/`, `tool/` — AST-gemeten, dus inclusief aaneengeschakelde literals en de tekstdelen van interpolaties, wat doorgeefluiken (`EditorField(label: 'Titel')`) en registerregels (`labelKey: 'applicationLanguage'`) meepakt. Alleen over wat dán nog overblijft de dure tekstlaag over `assets/` en `web/`. Die tweede laag bleek nodig: er is een **derde ophaalweg** die niemand op papier had staan — `AppLocalizations.sourceFor(lang, labelNl)` in de canvas-/matrix-/tree-/flow-specs, gevoed uit `assets/improvement/templates/`. "Bedreigingen" staat in geen enkele Dart-literal en is wél in gebruik. ## Wat bewust géén bewijs is `docs/`, README en CHANGELOG (proza beschrijft de app — twee van de drie handmatig bevestigde wezen staan in de CHANGELOG en zouden anders wegvallen), de `*_l10n_spec.json`-bestanden (die hébben de sleutel gemaakt), de vertaaltabellen zelf, en de poort plus haar eigen test. Dat laatste is geen theorie: `settingsLogo` verdween uit de bevindingen zodra hij in een commentaarzin van de poort stond. ## Uitkomst: 181 wezen van 3.522 4 in `_stringsEn` (t-sleutels), 48 en 129 in de twee Nederlandse tabellen. Grote clusters: ~22 OpenKAT-rapportbeschrijvingen, ~18 opslagteksten, 11 "(ontwerp)"-moduletegels, de teksten van het geschrapte `actions`-slidetype (#417), en herschreven zinnen waarvan de oude versie bleef staan. Steekproef van tien: allemaal echt wees. 181 is een **ondergrens**, geen telling: een sleutel die als deeltekst van een andere string voorkomt wordt niet gemeld. De poort mag zwijgen over een wees, maar mag nooit iemand op zoektocht sturen naar een sleutel die wél gebruikt wordt. ## In `check-full`, met een ratchet Het bewijs is tekstueel, geen typecontrole: de poort weet dat een sleutel ergens vóórkomt, niet dat hij wordt uitgevoerd. Zo'n oordeel hoort niet in de poort die élke commit tegenhoudt. Wat wél tanden heeft is `orphanBaseline = 181` — mag dalen, nooit stijgen — plus opname in `make ratchets`. ## Getoetst in twee richtingen `test/l10n_orphan_check_test.dart` (13 tests): vijf bouwen een mini-repo met een sleutel die wél gebruikt wordt (via `d()`, `t()`, een doorgeefluik, een aaneengeschakelde literal, een asset-label) en eisen nul bevindingen; vier planten een wees en eisen alarm, ook als alleen documentatie of alleen een spec-bestand hem noemt. Vier meten op de echte boom. Zonder die tweede richting commit je een poort die stilzwijgend niets doet — dat is hier eerder bijna gebeurd. Opruimen van de 181 is bewust een volgende stap. ## Poorten `make check` groen. `make check-secrets` groen, `make sast` groen (0 findings).
De vertaalpoorten van deze repo keken maar één kant op. `make l10n-check` en
`test/app_localizations_test.dart` bewaken dat elke GEBRUIKTE sleutel in alle 32
talen bestaat; of een sleutel nog wordt opgehaald vroeg niets. Elke wees kost
32 regels onderhoud voor tekst die geen mens ooit ziet.

Waarom AST en niet een grep. Een ruwe grep gaf 249 kandidaten, waarvan een groot
deel ruis: een zin die in de bron over twee regels aaneengeschakeld staat is één
sleutel in de tabel maar staat nergens als één stuk tekst, en escapes lopen uit
elkaar. De literals komen daarom uit de AST, net als in check_hardcoded_text.

Waarom er ook tekstueel gezocht wordt. Naast d() en t() bestaat een derde
ophaalweg: `AppLocalizations.sourceFor(lang, labelNl)`, waar de verbetermodule
de labels van haar sjablonen doorheen haalt. Die labels komen uit gegevens
(assets/improvement/templates/, improvement_templates_floor.g.dart) en staan in
geen enkele Dart-literal. "Bedreigingen" is zo in gebruik zonder ergens als
literal voor te komen. De tweede regel — komt de sleutel als tekst voor in lib/,
test/, tool/, assets/ of web/ — vangt die weg, en meteen alles wat via een
generator wordt samengesteld. Grof, en met opzet: hij levert valse negatieven op
("treffer(s)" zit in "meer treffer(s)") en dat is de goede kant om te falen.
Deze poort mag zwijgen over een wees; ze mag nooit iemand op zoektocht sturen
naar een sleutel die wél gebruikt wordt.

Wat níet als bewijs telt: documentatie (proza beschrijft de app, het roept geen
sleutel op — twee van de drie met de hand bevestigde wezen staan in de
CHANGELOG), de l10n-specs die de sleutel juist gemáákt hebben, de vertaaltabellen
zelf, en de poort en haar test: die noemen sleutels bij naam en wasten daarmee
hun eigen bevindingen weg tot dat opviel.

De test toetst twee richtingen. Nul bevindingen op een sleutel die wél gebruikt
wordt (langs vijf routes), en alarm op een geplante wees — met tabel en
regelnummer, en ook wanneer alleen documentatie, alleen een l10n-spec of alleen
de andere taaltabellen hem noemen. Zonder die tweede richting is een poort die
stilvalt altijd groen. Daarnaast één meting op de echte boom, zodat de drie met
de hand bevestigde wezen vastliggen.
Waarom check-full en niet check. Het bewijs van gebruik is tekstueel, geen
typecontrole: de poort weet dat een sleutel ergens vóórkomt, niet dat hij wordt
uitgevoerd. Zo'n oordeel hoort niet in de poort die élke commit tegenhoudt — een
valse melding kost daar iedereen tijd op iets wat geen bug is, want er breekt
niets van een ongebruikte sleutel; er verwatert alleen onderhoud. Bovendien
staat de teller op 181 en niet op nul: de opruiming is bewust een volgende stap,
en een poort in `check` zou dus of rood staan of een uitzonderingslijst nodig
hebben.

Wat wél élke commit raakt is de ratchet. `orphanBaseline` mag dalen en nooit
stijgen, dus een sleutel die je toevoegt en nooit aanroept laat het getal
stijgen. Daarom staat hij ook in check_ratchet_trend: een basislijn die niemand
in de trendmeting ziet, staat stil zonder dat het opvalt.
CHECKS.md is de plek waar een poort uitlegt wat ze meet en waar haar grens ligt;
zonder die twee laatste alinea's (wat níet als bewijs telt, en waarover ze
bewust zwijgt) leest een lezer de lijst als volledig. Dat is precies de
overschatting die een poort gevaarlijk maakt.
test(ratchets): geef de trendmeting brontekst voor de nieuwe basislijn (#1512)
All checks were successful
scans / scans (pull_request) Successful in 2m39s
static-gate / static-gate (pull_request) Successful in 6m2s
acbd81c6f6
De vergelijkingstests van check_ratchet_trend draaien op vaste brontekst, één
stuk per bestand waar een basislijn woont. Een nieuwe ratchet zonder bron telt
daar als "onvindbaar" en zet vier tellingen op scherp die niets met dit werk te
maken hebben. De wezenratchet staat op nul in de fixture, net als de gemengde
commentaarblokken: dan doet hij mee als "al af" en blijven die tellingen over
hun eigen onderwerp gaan.
brenno merged commit 6e84accfae into main 2026-08-17 10:28:09 +00:00
Sign in to join this conversation.
No description provided.