## v0.254.0 — The saved notes follow the language, and the switch becomes a globe (2026-09-18, R-557 slice 2 release C — SLICE 2 CLOSED) **MinAgent: 0.131.0** (unchanged). **No hub release needed.** Nothing on the wire moved. **Hungarian changes in exactly TWO places, both declared and both measured** — the sidebar footer of every dashboard page, and the three pages a visitor meets. Everything else is byte-identical. **1. The notes a background run SAVES now follow the language.** The line under last night's backup, the last error, the proof result, the restore outcome — written in the BOX's language at the moment they are written (`util.Text(lang, key, …)`, `m.note`/`s.note`). The consequence, stated rather than hidden: **a household that switches sees the note from the run before in the old language until the next run rewrites it.** That is the operator's §16 option 1; storing a code and rendering live would need a dozen new persisted fields and a legacy path for each — the R-570 shape a dozen times over. Converted: the off-site failure classes and quota notes, the tier-2 reasons and warnings, the undo phrases, the reconstitute hold, the restore record, and the whole restore-outcome family. **`EndRestoreOp` no longer receives a Hungarian literal from anywhere.** **2. The language switch is a globe.** Two text links wrapped in the sidebar footer and asked the reader to recognise „Magyar"/„English" as links; a globe is the one symbol every web user already reads as "language", so nobody has to read Hungarian to find their way out of Hungarian. It is `
`/`` — a menu with no script, which opens on click and on Enter and which a screen reader announces. Language names inside are shown in their own language and are never translated. **3. The sign-in, claim and recovery pages get the same globe — and a visitor's choice stays theirs.** Those pages are met by someone who has not signed in. They have no setting to read, and they must not write the household's: a box's sign-in page is reachable by anyone who can reach the box. So the choice lives in the visitor's own browser (`felhom_lang`, display-only, one of two values), and `langFor` reads it **only when there is no session** — a signed-in household can never inherit a language a previous visitor picked in the same browser. `POST /lang` is exempt from CSRF for a narrow, checkable reason, written down where it is made: the only thing a forged request can achieve is to change the language of the page the victim's own browser shows them. It writes one cookie, reads nothing, touches no setting, and `safeBackPath` keeps its redirect on this box (a protocol-relative `//evil.example` is refused — "starts with /" alone is not the test). **If that handler ever gains a second effect, it needs CSRF that day.** **§16, the operator's default, taken: a claim CARRIES the visitor's language.** Someone who switched the claim page to English and then claimed the box chose English deliberately. Only on success, and only there — the one moment an anonymous visitor becomes the household. **No globe on the guest share pages or the not-found page.** A share visitor is a stranger and the household's setting is the wrong default for them; that is a promise about the share feature and an operator decision (**R-577**). Pinned by `TestGuestSharePagesHaveNoGlobe` so it stays a decision. **The two parity exceptions, measured rather than asserted.** 106 fixtures rendered and compared with a real (LCS) diff — **exactly two change shapes**: the dashboard footer (89 fixtures) and the globe in the shells (12), with 5 byte-identical, which are precisely the three pages that must not change. The whole diff is in `felhom.eu/documentation/audits/i18n-slice2-2026-09-18/C/parity-exception-diff.txt`. **Two defects this release found in its own measurement, recorded rather than tidied away:** - The parity HARNESS rendered the three visitor shells through the DASHBOARD path, so the fixture would have baked a globe posting to `/settings/language` with a CSRF field — a form the real page never serves. Caught by reading the diff before re-capturing. The harness now branches on `i18nDirectTemplates`. - The first "is the change only the two blocks?" measurement compared LINE BY INDEX. An insertion shifts every line below it, so it reported 60 520 changed lines and measured nothing. **A line-index compare is not a diff.** `R-575` folded in where it was one call (`tier2NoTargetReason` now returns a note in the box's language); the soft memory-overcommit warning stays as its row says. ## v0.253.0 — Errors carry the key of the sentence they are (2026-09-18, R-557 slice 2 release B) **MinAgent: 0.131.0** (unchanged). **No hub release needed.** Every Hungarian sentence is byte-identical to the literal it replaced, measured by `i18n_go_parity.py`, and nothing on the wire moved. 179 Hungarian sentences were built deep inside a package with `fmt.Errorf` and printed by whoever caught them. They could not be translated where they are SHOWN (by then they are a finished string) nor where they are MADE (that code has no request and no language). **Every one of them now carries its key across that gap, and zero Hungarian error literals remain.** `util.MsgError` does three things at once, and each was earned: 1. **`Error()` returns the Hungarian, byte for byte.** Every un-converted printer — a log line, a `%v`, a third-party wrapper — keeps printing exactly what it printed before. That is what made converting 179 producers safe without converting all their printers in the same commit. 2. **`errors.Is` still answers, for the kind AND for a wrapped cause.** `Unwrap() []error` returns both. The predecessor `KindErrorf` returned the kind alone and dropped the cause; a converted producer usually wraps one, and a caller testing for it would have silently stopped matching. 3. **An error ARGUMENT is rendered recursively**, so `„formázás sikertelen: %w"` translates the whole chain. An inner error that is not ours (restic, docker, ssh, the stdlib) prints itself — that text is not written here and is not translated here (R-553's rule). - **Every display path goes through `errText`** (76 sites in `internal/web` and `internal/api`), and `TestNoErrErrorInPageOutput` keeps it that way. A `strings.Contains(err.Error(), …)` is a COMPARISON and is deliberately untouched — the four English ones are R-569. - **The memory refusal is now an error where it is made.** `memoryVerdict` returns `util.MsgErrorf(ErrNotEnoughMemory, …)` instead of a sentence a caller had to wrap, so the API's 409 and the household's language come from one value. `UpdateRefusal` gained a `Cause` so the same error survives to the API through the update path. - **Plurals, one rule, stated once:** a key that carries `.one`/`.other` forms in a language is a plural key and its FIRST parameter is the count (`i18n.Bundle.form`). Hungarian never carries them — it does not inflect after a numeral — so a Hungarian render is unchanged at every count. Not a per-call-site flag: the one producer somebody forgot would read „3 app is not running" with nothing to catch it. `TestPluralFirstArgIsNumeric` pins that every plural value really does take its count first; `TestNoOrdinaryKeyEndsInAPluralSuffix` pins that `.one`/`.other` stay reserved — a real collision (`alert.deadapp.one`) was caught by it on the day the rule landed. **Two defects this release found in its own tooling, both recorded rather than quietly fixed:** - The bulk converter **silently dropped the continuation of a multi-line concatenation**, damaging 7 producers. The parity gate did not catch it: every surviving fragment WAS a real base-commit literal, so its question was answered yes while the call had lost text. Two behaviour tests (`TestR356_ScenarioC`, `TestR379_ScenarioA`) caught it, because they assert the sentence a customer reads. All 7 rebuilt as joined keys. - The counting script was **case-sensitive**, so it reported "0 error literals left" while five remained. The R-565 shape, in the instrument. Re-measured; the five are converted. ## v0.252.0 — The sentences the program writes follow the language (2026-09-18, R-557 slice 2 release A) **MinAgent: 0.131.0** (unchanged). **No hub release needed.** Nothing a Hungarian household reads changes: every converted sentence is byte-identical to the literal it replaced, measured by a new gate rather than read, and nothing on the wire moved. Slice 1 translated the dashboard's MARKUP. The sentences the program BUILDS — flash lines, page data, the JSON the page's own script reads, the alert banners, the country names — were still Hungarian literals in Go, so an English household clicked an English button and was answered in Hungarian. This release moves 226 of them into the bundle and gives them a way to reach the reader. **The flash line was the hard part.** It travels to the page INSIDE THE REDIRECT URL, so it is rendered by a different request from the one that wrote it — until now in the language of whoever redirected. It now travels as a bundle key plus its parameters (`?flash=flash.share.enabled&fa=…`), and the page renders it. A link minted by an older controller carries prose; that is shown verbatim, never as a raw key and never dropped (`TestFlashKeyRoundTrip`, including the legacy and the `