The hub has written every customer e-mail in Hungarian whatever the box was set to. The box has published its language since controller v0.247.0; nothing read it. Now it does. Nothing an operator reads changes. The Hungarian mails are byte-identical, and that is a diff rather than a reading: 56 goldens per language captured from v0.117.0 BEFORE any string moved, and all 56 Hungarian ones pass unchanged after every sentence was routed through the new bundle. - internal/i18n: flat bundle, 79 keys, hu authoritative + hu fallback, ceiling 0. - customerMessages/severityLabels are DERIVED from the bundle, so a sentence is written in one place and all 40+ tests that read those maps still work. - Language order: last reported -> created-with -> hu. reports.language defaults to EMPTY, never hu: "never told us" is not "chose Hungarian". - message_customer on POST /api/v1/event, additive and optional forever, for the sentences the box composes and the hub cannot translate. - The bind page is per-language, and its `expired` state stays Hungarian: it is the state an unknown token lands in, so rendering a real English customer's token in English would make the LANGUAGE answer what the TEXT refuses to. Two defects found inside the release: - R-581: the newest report was picked by received_at, which has SECOND granularity, so same-second reports tied and the winner was arbitrary. Ordered by the autoincrement id now. GetCustomers() still has the shape - row open. - R-582: the English copy-guard stems, ported word for word from Hungarian, convicted 141 honest sentences. The English claim is a phrase with a modal. R-555 closed: the language allowlist entry is out of wire_contract_gate.py. hub_copy_gate.py follows the sentences into the bundle - without that it would have scanned four files that no longer hold any customer text and reported success. Three new decoys incl. an innocent control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
6.0 KiB
REPORT — localisation slice 3 Part A: the hub's e-mails follow the household's language
hub v0.118.0 · felhom.eu base 20aafc3dec20 · 2026-09-18 · R-558 (Part A), R-555 closed
Claims in the task that turned out wrong, named first
- "
customerMessages40 entries (L70)" — 39 entries, at L69. - "
customers.language,appliances.reported_language" — neither table exists. There is nocustomerstable and noappliancestable. The real ones arecustomer_configs(the customer record) andreports(the box's heartbeat, one row per report). The columns added arecustomer_configs.languageandreports.language. - "no
ALTER TABLE customers ADDfound by grep — the migration pattern is something else; if it is not obvious, stop and report." The pattern is completely obvious and there are ~20 instances:s.db.Exec("ALTER TABLE <t> ADD COLUMN <c> <type> NOT NULL DEFAULT <v>"), error deliberately ignored so it is idempotent. The grep failed only because it named a table that does not exist. - "
renderBackupRunFailuresgains alangparameter" — it is operator-only and already English. Its single caller isFormatOperatorEmail. Untouched. - "
message_customer's length is capped likemessage" —messagehas no length cap. Both are bounded only by the 1 MBLimitReaderon the request body. I wrote a cap, then removed it: a byte-count truncation would also cut a UTF-8 sequence in half. - "
python3 scripts/hub_gates.py(or the runner the hub uses — name it)" — there is nohub_gates.py. The hub's gates run fromscripts/repo_gates.py, this repo's single runner. - Line numbers were mostly off by one or two (
customerMessages69 not 70,severityLabels158 not 159,FormatCustomerEmail166 not 167).
Claims that were right: no mail goldens existed; the hub read no language anywhere outside a
comment; 05-hub-architecture.md had no customer-mail section; the box has published the field since
v0.247.0. I briefly believed Report.Language was never assigned and was wrong — cmd/ is
gitignored, so rg skips main.go, where all four assignments live.
What shipped
The hub reads the language the box reports and writes the household's e-mail in it. Nothing an operator reads changed. Hungarian is byte-identical, measured.
internal/i18n— flat bundle, 79 keys, hu authoritative + hu fallback, missing ceiling 0.- 56 mail goldens per language, captured from v0.117.0 before any string moved. All 56
Hungarian ones pass unchanged after the rewrite. A
nowFnseam makes them byte-stable. customerMessages/severityLabelsare derived from the bundle — one place per sentence, and all 40+ existing tests and comments that read them still work.- Order: last reported → created-with →
hu.reports.languagedefaults to empty, neverhu: "never told us" is not "chose Hungarian". message_customeraccepted onPOST /api/v1/event(additive, optional forever).- Per-language bind page, built the controller's way (substitute markers, then parse).
- Customer form
languageselect;customer.languageincontroller.yaml(diff: one line).
Live evidence, read off the running hub
The wire already worked and I measured it before writing anything (read-only copy of hub.db + WAL):
demo-felhom 2026-09-18 13:42:56 language='hu' controller 0.255.0
demo-hp 2026-09-18 13:45:24 language='en' controller 0.255.0
drill-r50 / peti-felhom / tester-1 language=<ABSENT> (0.213.0 / 0.115.0 / 0.245.0)
That is the positive and the negative control in one read: boxes ≥ 0.247.0 report the field, older ones send nothing at all — which is exactly the case the empty default exists for.
Two defects found inside this release
- R-581 (P2, partly still open).
CustomerLanguagepicked the newest report byreceived_at, which has second granularity — same-second reports tie and the winner is arbitrary. A household that had just switched would get the old language back at random. Fixed by ordering on the autoincrementid; caught by a test that failed on the first version. Still open:GetCustomers()has the same shape and feeds the whole operator dashboard. - R-582 (closed, kept for the lesson). The English copy-guard stems, ported word-for-word from the
Hungarian, convicted 141 honest sentences. In Hungarian the stem is the claim
(
visszaállíthat= can restore); English splits the modal from the verb, so the claim is a phrase. Then the decoy suite caught the fix being too narrow — "can still be restored" walked through a pattern written for "can be restored".
Gate work
wire_contract_gate.py: the R-555 language allowlist entry deleted — the field is genuinely
decoded now and the gate checks it (202 tags, was 201). hub_copy_gate.py: the sentences moved into
the bundle, so CUSTOMER_SURFACES had to move with them — without that the four declared Go files
would all still exist, the gate would still report success, and it would be scanning nothing. It
learned English, and found one real occurrence (operator-tier, registered with its reason). Three new
decoys including an innocent control; the hub-copy exemption is removed from
decoy_coverage_gate.py.
The mails
| Group | Count | Golden | English | Live-tested |
|---|---|---|---|---|
Event types (mail.event.*) |
39 | ✅ hu + en | ✅ | pending (§13.1) |
| Severity labels | 4 | ✅ | ✅ | — |
| Customer wrapper (subject, body, 2 lines, sign-off) | 5 | ✅ | ✅ | — |
| Claim arc (claim / reset / reenroll / claimed) | 4×2 | ✅ | ✅ | pending (§13.2) |
| Self-bind mail | 2 | ✅ | ✅ | pending |
| Bind page | 21 | — (render tests) | ✅ | pending |
| Operator mails | 3 | ✅ (unchanged) | n/a — never localised | — |
Green
go build / go vet / go test ./... clean. All 14 felhom.eu gates OK. 15/15 decoys behave.
Not yet done: deploy + live proof (§13), and Part B (controller v0.256.0, the box's own sentence).