diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab14b32..063f60c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,56 @@
+## 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
+`",
+ },
+ {
+ name: "no flash at all is empty, not a key",
+ query: "", lang: "en",
+ want: "",
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ r := flashRequest(t, tc.query+"&lang="+tc.lang)
+ if got := s.flashFrom(r, "flash"); got != tc.want {
+ t.Errorf("flash\n got %q\n want %q", got, tc.want)
+ }
+ })
+ }
+}
+
+// TestFlashKeyCarriesParameters — a flash whose sentence names something (an app, a drive) carries
+// that name as a separate `fa` parameter, so English may put it somewhere else in the sentence.
+func TestFlashKeyCarriesParameters(t *testing.T) {
+ s := flashServer(t)
+ // The parameter-carrying flash in the bundle today. Asserted through the real bundle rather than a
+ // fixture, and self-arming: if no flash message takes a parameter any more the test says so out
+ // loud instead of passing vacuously.
+ const key = "flash.offbox.config_invalid"
+ if !s.i18n.Has(i18n.Default, key) {
+ t.Fatalf("%s left the bundle — this test no longer covers a parameter-carrying flash", key)
+ }
+ if !strings.Contains(s.i18n.Msg(i18n.Default, key), "%s") {
+ t.Fatalf("%s no longer takes a parameter — point this test at one that does", key)
+ }
+ r := flashRequest(t, flashQuery("flash_error", key, "a port nem szam")+"&lang=hu")
+ got := s.flashFrom(r, "flash_error")
+ if want := "Érvénytelen beállítás: a port nem szam"; got != want {
+ t.Errorf("parameter did not reach the message\n got %q\n want %q", got, want)
+ }
+ // And the same key, same parameter, in English: the parameter survives a different word order.
+ r = flashRequest(t, flashQuery("flash_error", key, "a port nem szam")+"&lang=en")
+ if got := s.flashFrom(r, "flash_error"); !strings.Contains(got, "a port nem szam") {
+ t.Errorf("the parameter was lost in English: %q", got)
+ }
+}
+
+// TestFlashOnAServerWithNoBundleField — a Server built without going through loadTemplates still
+// renders SENTENCES, never raw keys. s.bundle() falls back to the embedded i18n.Shared() for exactly
+// this: a page that shows „flash.share.enabled" to a household is the failure mode §4 of the
+// localisation design forbids, and it was reachable from any construction path but the real one.
+//
+// RED-PROOF (REPORT): make s.bundle() return s.i18n unchanged → this test reads back the key.
+func TestFlashOnAServerWithNoBundleField(t *testing.T) {
+ s := &Server{} // i18n field never set
+ if got := s.flashFrom(flashRequest(t, "flash=flash.share.enabled"), "flash"); got != "A megosztás bekapcsolva." {
+ t.Errorf("a Server with no bundle field showed %q instead of the sentence", got)
+ }
+ // And a legacy sentence still passes through untouched.
+ if got := s.flashFrom(flashRequest(t, "flash=Sikeres+ment%C3%A9s"), "flash"); got != "Sikeres mentés" {
+ t.Errorf("got %q, want the legacy text verbatim", got)
+ }
+}
+
+// flashSentence resolves the flash a redirect Location carries, in Hungarian.
+//
+// It exists because v0.252.0 moved the SENTENCE out of the URL and put a bundle KEY there instead.
+// A test that searched the Location header for „elindult" or „letét" was never asserting the URL —
+// it was asserting what the customer is told — and that is still exactly checkable, one lookup
+// later. Tests assert through this rather than against the key, so a key renamed with its message
+// intact stays green and a message REWORDED still fails, which is the right way round.
+func flashSentence(t *testing.T, location string) string {
+ t.Helper()
+ u, err := url.Parse(location)
+ if err != nil {
+ t.Fatalf("Location %q: %v", location, err)
+ }
+ s := flashServer(t)
+ r := httptest.NewRequest(http.MethodGet, location, nil)
+ for _, p := range []string{"flash", "flash_error"} {
+ if v := u.Query().Get(p); v != "" {
+ return s.flashText(r, v)
+ }
+ }
+ return ""
+}
diff --git a/controller/internal/web/i18n_web.go b/controller/internal/web/i18n_web.go
index 1b956c8..eb1a9d5 100644
--- a/controller/internal/web/i18n_web.go
+++ b/controller/internal/web/i18n_web.go
@@ -4,6 +4,7 @@ import (
"fmt"
"html/template"
"net/http"
+ "net/url"
"strings"
"time"
@@ -18,6 +19,132 @@ import (
// was before this file existed — pinned by i18n_parity_test.go against fixtures captured from the
// unconverted templates.
+// ── Go-side copy (v0.252.0, slice 2 — R-557) ───────────────────────────────────────────────────
+//
+// A handler builds sentences the template never sees: flash lines, page data, JSON answers. Those go
+// through these three helpers, never through a Hungarian literal, so they follow the request's
+// language exactly as the template does.
+//
+// The Hungarian text is the SAME BYTES the literal carried, which scripts/i18n_go_parity.py measures
+// against a frozen capture of the base commit. So a handler that reads
+//
+// data["Msg"] = s.msg(r, "backup.window_updated")
+//
+// renders, for a Hungarian household, the literal that used to sit at that line and nothing else.
+
+// bundle returns the message bundle for these helpers.
+//
+// It falls back to the process-wide i18n.Shared() when the Server has none. That is not defensive
+// clutter: `s.i18n` is set by loadTemplates, so any Server built WITHOUT going through it — a test
+// fixture, a future construction path — would otherwise render raw KEYS onto a page, which is the one
+// outcome §4 of the localisation design forbids ("never a key, never a blank"). The bundle is
+// embedded in the binary, so the fallback cannot fail for any reason a running box can reach; if it
+// somehow does, the caller still gets the key rather than a panic.
+func (s *Server) bundle() *i18n.Bundle {
+ if s.i18n != nil {
+ return s.i18n
+ }
+ b, err := i18n.Shared()
+ if err != nil {
+ return nil
+ }
+ return b
+}
+
+// msg returns a Go-side message in the request's language, with the message's own printf verbs
+// filled in from a.
+func (s *Server) msg(r *http.Request, key string, a ...interface{}) string {
+ return s.msgLang(s.langFor(r), key, a...)
+}
+
+// msgLang is msg for a language that is already known — a background run reading the box's setting
+// (internal/settings GetLanguage), or a handler that resolved the language once for several lines.
+func (s *Server) msgLang(lang, key string, a ...interface{}) string {
+ b := s.bundle()
+ if b == nil {
+ return key
+ }
+ if len(a) == 0 {
+ return b.Msg(lang, key)
+ }
+ return b.Msgf(lang, key, a...)
+}
+
+// msgN returns a count-dependent message in the request's language. Hungarian carries one form under
+// the key itself; English carries key+".one" and key+".other" (i18n.Bundle.Plural).
+func (s *Server) msgN(r *http.Request, key string, n int) string {
+ b := s.bundle()
+ if b == nil {
+ return key
+ }
+ return b.Plural(s.langFor(r), key, n)
+}
+
+// ── Flash lines (v0.252.0) ─────────────────────────────────────────────────────────────────────
+//
+// A flash travels to the page INSIDE THE REDIRECT URL (`?flash=…`), so it is rendered by a DIFFERENT
+// request from the one that wrote it — and until now it travelled as Hungarian text, which is the
+// language of whoever redirected. It now travels as a bundle KEY plus its parameters, and the reader
+// renders it in its own language.
+//
+// Backward compatibility is not a nicety here: a customer's open tab, a bookmark or a browser's
+// back-forward cache can replay a URL minted by the previous version, and a mail client can carry
+// one. So the rule is: a value the bundle knows as a key is a MESSAGE; anything else is TEXT and is
+// shown verbatim, exactly as it was before. That also covers a hand-typed `?flash=