71 lines
3.1 KiB
Go
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)
|
|
}
|