v0.247.0: i18n spike — the dashboard can speak English, Hungarian byte-identical
gates / gates (push) Successful in 19s
gates / gates (push) Successful in 19s
Message bundles (internal/i18n) expanded into templates before parsing, one template set per language. Launcher, /backups, /apps/<slug> and the layout converted; household language setting, POST /settings/language, ?lang= override, report field. Parity test against fixtures captured from unconverted templates; copy gates read templates expanded; new i18n_missing_gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// i18n (v0.247.0): the household language. Default Hungarian; only a supported value is stored; the
|
||||
// choice survives a restart (it is what the hub report and every page read).
|
||||
func TestLanguageSetting(t *testing.T) {
|
||||
lg := log.New(io.Discard, "", 0)
|
||||
p := filepath.Join(t.TempDir(), "settings.json")
|
||||
s, err := Load(p, lg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := s.GetLanguage(); got != "hu" {
|
||||
t.Fatalf("a box that never chose must read Hungarian, got %q", got)
|
||||
}
|
||||
if err := s.SetLanguage("de"); err == nil || s.GetLanguage() != "hu" {
|
||||
t.Fatalf("unsupported language must be refused and change nothing (err=%v, lang=%q)", err, s.GetLanguage())
|
||||
}
|
||||
if err := s.SetLanguage("en"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
again, err := Load(p, lg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := again.GetLanguage(); got != "en" {
|
||||
t.Fatalf("language did not survive a reload: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
|
||||
)
|
||||
|
||||
// Settings holds customer-modifiable overrides and cached state.
|
||||
@@ -34,6 +35,9 @@ type Settings struct {
|
||||
// OPTIONAL bcrypt hash for a per-share password, ALWAYS SEPARATE from the admin PasswordHash above.
|
||||
// The token is a secret and must never be logged.
|
||||
LauncherShareToken string `json:"launcher_share_token,omitempty"`
|
||||
// Language (v0.247.0, i18n) — the household's dashboard language: "hu" | "en". Empty means never
|
||||
// chosen and reads as Hungarian (GetLanguage). Reported to the hub so its e-mails can follow.
|
||||
Language string `json:"language,omitempty"`
|
||||
LauncherSharePasswordHash string `json:"launcher_share_password_hash,omitempty"`
|
||||
|
||||
// FileBrowser admin login (R-513, v0.243.0). Every box used to accept admin/admin. The controller
|
||||
@@ -809,6 +813,26 @@ func (s *Settings) SetBackupWindowStart(start string) error {
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// GetLanguage returns the household's dashboard language, normalised: anything unset or unknown is
|
||||
// Hungarian (i18n.Default).
|
||||
func (s *Settings) GetLanguage() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return i18n.Normalize(s.Language)
|
||||
}
|
||||
|
||||
// SetLanguage stores the household's dashboard language and saves. Only a supported language is
|
||||
// accepted; anything else is refused and nothing is written.
|
||||
func (s *Settings) SetLanguage(lang string) error {
|
||||
if !i18n.IsSupported(lang) {
|
||||
return fmt.Errorf("unsupported language %q", lang)
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.Language = lang
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// GetLauncherShareToken returns the guest-launcher capability token ("" = sharing disabled).
|
||||
func (s *Settings) GetLauncherShareToken() string {
|
||||
s.mu.RLock()
|
||||
|
||||
Reference in New Issue
Block a user