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=