Files
enkly 79ce8dc4c0
Some checks failed
Go CI Pipeline / build_and_test (push) Failing after 22m0s
Init Project
2026-10-30 18:06:55 +01:00

71 lines
3.1 KiB
Go

// Package handlers (i18n.go) is the gateway's half of multilingual
// support: resolving which locale a given request should render in,
// and the /language switcher that lets a visitor override it. The
// catalog itself — supported locales, their strings, the T() lookup —
// lives in pkg/i18n so the agent service can share the same locale
// codes and display names (see agentrun.WithPreferredLanguage)
// without the two services depending on each other.
//
// This mirrors ui.go's theme toggle almost exactly on purpose: same
// cookie-based persistence, same safeNext redirect, same form-POST
// shape — a visitor who already knows how the theme switcher works
// recognizes the language switcher instantly, and there's only one
// pattern for a future settings toggle to copy.
package handlers
import (
"net/http"
"time"
"enkly/pkg/i18n"
)
const langCookieName = "enkly_lang"
// localeFrom resolves the locale a request should render in:
// 1. an explicit ?lang= query override (lets a shared/bookmarked link
// force a language, and is how SetLanguage's own redirect target
// can take effect without a second round trip),
// 2. the enkly_lang cookie, set the first time a visitor picks one
// (explicitly here, or implicitly — see SetLanguage),
// 3. the browser's Accept-Language header, for a first-time visitor
// who hasn't chosen anything yet,
// 4. i18n.Default ("en") if none of the above yield a supported
// locale.
//
// There is deliberately no account-level preference here: enkly has
// no per-user settings table in any service's schema today, and
// adding `preferred_language` to the IAM User message/row is a schema
// + generated-gRPC-code change of its own — see ARCHITECTURE.md's
// "Language preference storage" note for the follow-up. The cookie
// already survives logins, browser restarts and (for a year) long
// gaps between visits; it just doesn't follow a visitor to a second
// device the way an account field would.
func localeFrom(r *http.Request) i18n.Locale {
if q := r.URL.Query().Get("lang"); q != "" && i18n.IsSupported(q) {
return i18n.Locale(q)
}
if c, err := r.Cookie(langCookieName); err == nil && i18n.IsSupported(c.Value) {
return i18n.Locale(c.Value)
}
return i18n.NegotiateAcceptLanguage(r.Header.Get("Accept-Language"))
}
// SetLanguage (POST /language) replaces Handlers.SetTheme's role for
// language: persists the chosen locale in a year-long cookie and
// sends the browser back to wherever it came from. Rejects an
// unsupported code rather than silently falling back, the same way
// SetTheme rejects an unknown theme value — a bad value here is
// either a stale client (an old switcher built against a locale list
// that has since changed) or a tampered request, and either way the
// right response is a 400, not a cookie nobody asked for.
func (h *Handlers) SetLanguage(w http.ResponseWriter, r *http.Request) {
lang := r.PostFormValue("lang")
if !i18n.IsSupported(lang) {
http.Error(w, "unknown language", http.StatusBadRequest)
return
}
h.setCookie(w, langCookieName, lang, int((365 * 24 * time.Hour).Seconds()))
http.Redirect(w, r, safeNext(r.PostFormValue("next")), http.StatusSeeOther)
}