feat(config): locale settings (language/timezone) in global page and config

This commit is contained in:
Claus Lohmar 2026-06-15 04:55:10 +00:00
parent 5663364b12
commit 6944223d35
7 changed files with 125 additions and 13 deletions

View file

@ -244,6 +244,9 @@ smtp:
imap:
host: "${IMAP_HOST}"
port: ${IMAP_PORT}
locale:
language: "en"
timezone: "UTC"
session:
secret: "${SESSION_KEY}"
expiry_minutes: 60

View file

@ -1,6 +1,6 @@
package templates
templ GlobalSettings(smtpHost, smtpPort, smtpUser, smtpPass, imapHost, imapPort, nextwksURL, authURL, msg string) {
templ GlobalSettings(smtpHost, smtpPort, smtpUser, smtpPass, imapHost, imapPort, nextwksURL, authURL, lang, tz, msg string) {
@Layout("global") {
<h2 style="font-size:1.15rem;font-weight:600;margin-bottom:20px">Global Settings</h2>
if msg != "" {
@ -55,6 +55,29 @@ templ GlobalSettings(smtpHost, smtpPort, smtpUser, smtpPass, imapHost, imapPort,
</div>
</div>
<div class="form-card">
<h3 style="font-size:0.95rem;font-weight:600;margin-bottom:12px">Localization</h3>
<div class="form-row">
<div class="form-group">
<label>Default Language</label>
<select name="lang" class="input">
<option value="en" if lang == "en" { selected }>English</option>
<option value="de" if lang == "de" { selected }>Deutsch</option>
</select>
</div>
<div class="form-group">
<label>Default Timezone</label>
<select name="tz" class="input">
<option value="UTC" if tz == "UTC" { selected }>UTC</option>
<option value="Europe/London" if tz == "Europe/London" { selected }>London</option>
<option value="Europe/Berlin" if tz == "Europe/Berlin" { selected }>Berlin</option>
<option value="Europe/Paris" if tz == "Europe/Paris" { selected }>Paris</option>
<option value="America/New_York" if tz == "America/New_York" { selected }>New York</option>
</select>
</div>
</div>
</div>
<button type="submit" class="btn" style="width:100%">Save Settings</button>
</form>
}

View file

@ -8,7 +8,7 @@ package templates
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func GlobalSettings(smtpHost, smtpPort, smtpUser, smtpPass, imapHost, imapPort, nextwksURL, authURL, msg string) templ.Component {
func GlobalSettings(smtpHost, smtpPort, smtpUser, smtpPass, imapHost, imapPort, nextwksURL, authURL, lang, tz, msg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@ -168,7 +168,77 @@ func GlobalSettings(smtpHost, smtpPort, smtpUser, smtpPass, imapHost, imapPort,
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"></div></div></div><button type=\"submit\" class=\"btn\" style=\"width:100%\">Save Settings</button></form>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"></div></div></div><div class=\"form-card\"><h3 style=\"font-size:0.95rem;font-weight:600;margin-bottom:12px\">Localization</h3><div class=\"form-row\"><div class=\"form-group\"><label>Default Language</label> <select name=\"lang\" class=\"input\"><option value=\"en\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if lang == "en" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, ">English</option> <option value=\"de\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if lang == "de" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, ">Deutsch</option></select></div><div class=\"form-group\"><label>Default Timezone</label> <select name=\"tz\" class=\"input\"><option value=\"UTC\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if tz == "UTC" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, ">UTC</option> <option value=\"Europe/London\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if tz == "Europe/London" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, ">London</option> <option value=\"Europe/Berlin\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if tz == "Europe/Berlin" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, ">Berlin</option> <option value=\"Europe/Paris\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if tz == "Europe/Paris" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, ">Paris</option> <option value=\"America/New_York\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if tz == "America/New_York" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, ">New York</option></select></div></div></div><button type=\"submit\" class=\"btn\" style=\"width:100%\">Save Settings</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View file

@ -48,7 +48,8 @@ func (h *Handler) adminGlobal(w http.ResponseWriter, r *http.Request) {
cfg := readConfigRaw(h.configPath)
component := templates.GlobalSettings(
cfg["smtp_host"], cfg["smtp_port"], cfg["smtp_user"], cfg["smtp_pass"],
cfg["imap_host"], cfg["imap_port"], cfg["nextwks_url"], cfg["auth_url"], "",
cfg["imap_host"], cfg["imap_port"], cfg["nextwks_url"], cfg["auth_url"],
cfg["lang"], cfg["tz"], "",
)
component.Render(r.Context(), w)
}
@ -61,7 +62,8 @@ func (h *Handler) adminGlobalSave(w http.ResponseWriter, r *http.Request) {
cfg := readConfigRaw(h.configPath)
component := templates.GlobalSettings(
cfg["smtp_host"], cfg["smtp_port"], cfg["smtp_user"], cfg["smtp_pass"],
cfg["imap_host"], cfg["imap_port"], cfg["nextwks_url"], cfg["auth_url"], msg,
cfg["imap_host"], cfg["imap_port"], cfg["nextwks_url"], cfg["auth_url"],
cfg["lang"], cfg["tz"], msg,
)
component.Render(r.Context(), w)
}
@ -88,6 +90,8 @@ func readConfigRaw(path string) rawConfig {
cfg["nextwks_url"] = trimSuffix(cfg["nextwks_url"], "/auth/callback")
}
cfg["auth_url"] = getNested(m, "oidc", "issuer_url")
cfg["lang"] = getNested(m, "locale", "language")
cfg["tz"] = getNested(m, "locale", "timezone")
return cfg
}
@ -105,6 +109,8 @@ func writeConfigRaw(path string, r *http.Request) string {
setNested(m, r.FormValue("smtp_pass"), "smtp", "password")
setNested(m, r.FormValue("imap_host"), "imap", "host")
setNested(m, r.FormValue("imap_port"), "imap", "port")
setNested(m, r.FormValue("lang"), "locale", "language")
setNested(m, r.FormValue("tz"), "locale", "timezone")
nextwks := r.FormValue("nextwks_url")
if nextwks != "" {
setNested(m, nextwks+"/auth/callback", "oidc", "redirect_url")

View file

@ -16,6 +16,7 @@ type Config struct {
OIDC OIDCConfig `yaml:"oidc"`
SMTP SMTPConfig `yaml:"smtp"`
IMAP IMAPConfig `yaml:"imap"`
Locale LocaleConfig `yaml:"locale"`
Session SessionConfig `yaml:"session"`
}
@ -61,6 +62,11 @@ type IMAPConfig struct {
Port int `yaml:"port"`
}
type LocaleConfig struct {
Language string `yaml:"language"`
Timezone string `yaml:"timezone"`
}
type SessionConfig struct {
Secret string `yaml:"secret"`
ExpiryMinutes int `yaml:"expiry_minutes"`

View file

@ -17,11 +17,12 @@ type PageCtx struct {
}
type Handler struct {
appDir string
appDir string
defaultLang string
}
func NewHandler(appDir string) *Handler {
return &Handler{appDir: appDir}
func NewHandler(appDir string, defaultLang string) *Handler {
return &Handler{appDir: appDir, defaultLang: defaultLang}
}
func (h *Handler) RegisterRoutes(mux *http.ServeMux, authGate func(http.Handler) http.Handler) {
@ -36,21 +37,24 @@ func (h *Handler) launcherPage(w http.ResponseWriter, r *http.Request) {
return
}
ctx := getContext(r)
ctx := h.getContext(r)
apps := DefaultApps(ctx.Role)
component := LauncherPage(ctx, apps)
component.Render(r.Context(), w)
}
func getContext(r *http.Request) PageCtx {
func (h *Handler) getContext(r *http.Request) PageCtx {
userID, _ := auth.GetUserID(r)
role, _ := r.Context().Value(auth.ContextRole).(string)
if role == "" {
role = "user"
}
// Detect language: cookie > header > default
lang := "en"
// Detect language: cookie > header > config default
lang := h.defaultLang
if lang == "" {
lang = "en"
}
if cookie, err := r.Cookie("lang"); err == nil {
lang = cookie.Value
} else if al := r.Header.Get("Accept-Language"); al != "" {

View file

@ -98,7 +98,7 @@ func main() {
if appDir == "." {
appDir = "./"
}
uiHandler := ui.NewHandler(appDir)
uiHandler := ui.NewHandler(appDir, cfg.Locale.Language)
// Setup HTTP router
mux := http.NewServeMux()