package web import ( "fmt" "html/template" "net/http" "net/url" "strings" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/i18n" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" "gitea.dooplex.hu/admin/felhom-controller/internal/util" ) // ── i18n (v0.247.0) — the dashboard in more than one language ────────────────────────────────── // // Design: felhom.eu/documentation/architecture/10-localisation.md. Hungarian is the default and the // only language a household sees unless it switches. The Hungarian page is byte-identical to what it // 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) } // ── Errors on a page (v0.253.0, slice 2 release B) ───────────────────────────────────────────── // // An error is MADE deep in a package that has no request, and PRINTED by a handler that has one. // util.MsgError carries the key across that gap; errText is the handler side of it. // // A plain error — restic, docker, ssh, the Go stdlib — prints verbatim, in both languages, because // that sentence is not ours (10-localisation.md §9). So this is safe to put in front of EVERY // err.Error() on a display path, converted or not, and that is exactly what release B does. // errText renders an error in the request's language: its bundle message when it carries one, its own // text otherwise. func (s *Server) errText(r *http.Request, err error) string { return util.ErrText(s.langFor(r), err) } // errTextLang is errText for a language already resolved (a background run, or a handler that // resolved it once). func (s *Server) errTextLang(lang string, err error) string { return util.ErrText(lang, err) } // ── 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=