Replace CLI + SQLite architecture with a Go web server + vanilla JS frontend using IndexedDB for all client-side data storage. - Remove: cli, store, report, static packages - Add: compute engine (BuildDashboard), server package, web UI - Add: setup page with CRUD for profiles, rooms, devices, occupants, AC - Add: dashboard with SVG temperature timeline, risk analysis, care checklist - Add: i18n support (English/German) with server-side Go templates - Add: LLM provider selection UI with client-side API key storage - Add: per-room indoor temperature, edit buttons, language-aware AI summary
101 lines
2.1 KiB
Go
101 lines
2.1 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// translations holds the loaded translation maps keyed by language code.
|
|
type translations struct {
|
|
langs map[string]map[string]any // e.g. {"en": {...}, "de": {...}}
|
|
}
|
|
|
|
func loadTranslations(enJSON, deJSON []byte) (*translations, error) {
|
|
t := &translations{langs: make(map[string]map[string]any)}
|
|
|
|
var en map[string]any
|
|
if err := json.Unmarshal(enJSON, &en); err != nil {
|
|
return nil, fmt.Errorf("parse en.json: %w", err)
|
|
}
|
|
t.langs["en"] = en
|
|
|
|
var de map[string]any
|
|
if err := json.Unmarshal(deJSON, &de); err != nil {
|
|
return nil, fmt.Errorf("parse de.json: %w", err)
|
|
}
|
|
t.langs["de"] = de
|
|
|
|
return t, nil
|
|
}
|
|
|
|
// get resolves a dotted key (e.g. "setup.rooms.title") from the translation map.
|
|
func (t *translations) get(lang, key string) string {
|
|
m, ok := t.langs[lang]
|
|
if !ok {
|
|
m = t.langs["en"]
|
|
}
|
|
if m == nil {
|
|
return key
|
|
}
|
|
return resolve(m, key)
|
|
}
|
|
|
|
func resolve(m map[string]any, key string) string {
|
|
parts := strings.Split(key, ".")
|
|
var current any = m
|
|
for _, p := range parts {
|
|
cm, ok := current.(map[string]any)
|
|
if !ok {
|
|
return key
|
|
}
|
|
current, ok = cm[p]
|
|
if !ok {
|
|
return key
|
|
}
|
|
}
|
|
s, ok := current.(string)
|
|
if !ok {
|
|
return key
|
|
}
|
|
return s
|
|
}
|
|
|
|
// supportedLangs are the available languages.
|
|
var supportedLangs = []string{"en", "de"}
|
|
|
|
// detectLanguage determines the active language from query param, cookie, or Accept-Language header.
|
|
func detectLanguage(r *http.Request) string {
|
|
// 1. Query parameter
|
|
if lang := r.URL.Query().Get("lang"); isSupported(lang) {
|
|
return lang
|
|
}
|
|
|
|
// 2. Cookie
|
|
if c, err := r.Cookie("heatguard_lang"); err == nil && isSupported(c.Value) {
|
|
return c.Value
|
|
}
|
|
|
|
// 3. Accept-Language header
|
|
accept := r.Header.Get("Accept-Language")
|
|
for _, part := range strings.Split(accept, ",") {
|
|
lang := strings.TrimSpace(strings.SplitN(part, ";", 2)[0])
|
|
lang = strings.SplitN(lang, "-", 2)[0]
|
|
if isSupported(lang) {
|
|
return lang
|
|
}
|
|
}
|
|
|
|
return "en"
|
|
}
|
|
|
|
func isSupported(lang string) bool {
|
|
for _, l := range supportedLangs {
|
|
if l == lang {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|