package api import ( "net/http" "gitea.dooplex.hu/admin/felhom-controller/internal/i18n" "gitea.dooplex.hu/admin/felhom-controller/internal/util" ) // ── The JSON answers follow the language too (v0.252.0, slice 2 — R-557) ─────────────────────── // // Design: felhom.eu/documentation/architecture/10-localisation.md. The dashboard's page markup has // followed the household's language since slice 1, but a large part of what a customer READS on // those pages never passes through a template: the page's own script fetches /api/… and shows the // `message` or `error` the answer carries. Until now those were Hungarian literals, so an English // dashboard answered in Hungarian the moment anything was clicked. // // These helpers mirror internal/web's (i18n_web.go) exactly, and for the same reason: the Hungarian // text is the SAME BYTES the literal carried, pinned by scripts/i18n_go_parity.py against a frozen // capture of the base commit. // // NOT everything here is display. `internal/report` and `internal/notify` put Hungarian on the wire, // where it is read by the hub and mailed to the household — those stay Hungarian in every language // until slice 3 (R-558) teaches the hub which language to compose in, and their bytes are pinned by // the wire goldens in those packages. // langFor decides the language of one API request: `?lang=hu|en` (a testing override, never // persisted), else the household's saved setting, else Hungarian. Identical in meaning to // web.Server.langFor, deliberately — the page and the calls its script makes must never disagree. func (r *Router) langFor(req *http.Request) string { if req != nil { if q := req.URL.Query().Get("lang"); i18n.IsSupported(q) { return q } } if r.sett != nil { return r.sett.GetLanguage() } return i18n.Default } // bundle returns the process-wide bundle, or nil if it somehow failed to load. It is embedded in the // binary, so a running box cannot reach the nil case; the callers still degrade to the key rather // than panicking. func bundle() *i18n.Bundle { b, err := i18n.Shared() if err != nil { return nil } return b } // msg returns a message in the request's language, with the message's own printf verbs filled in. func (r *Router) msg(req *http.Request, key string, a ...interface{}) string { return r.msgLang(r.langFor(req), key, a...) } // msgLang is msg for a language that is already known. func (r *Router) msgLang(lang, key string, a ...interface{}) string { b := bundle() if b == nil { return key } if len(a) == 0 { return b.Msg(lang, key) } return b.Msgf(lang, key, a...) } // errText renders an error in the request's language: its bundle message when it carries one, its own // text otherwise (restic, docker, ssh and the Go stdlib print verbatim — that text is not ours). func (r *Router) errText(req *http.Request, err error) string { return util.ErrText(r.langFor(req), err) } // countryName returns a country's name in lang, falling back to the name the cloudflare table // carries when the bundle has no entry for the code. // // The TABLE is not translated and must not be: internal/report puts country CODES on the wire, and // cloudflare/countries.go is also what validates them. Only the NAME shown in the geo picker follows // the language, which is why this lives here and not in that package. func (r *Router) countryName(lang, code, fallback string) string { b := bundle() if b == nil { return fallback } if v, _, ok := b.Text(lang, "country."+code); ok { return v } return fallback }