rentease/internal/i18n/i18n.go
Ruidy 584d81f7bd
Some checks failed
CI / checks (push) Has been cancelled
feat(i18n): add language toggle and localize views
2026-01-09 16:09:20 -04:00

129 lines
3 KiB
Go

package i18n
import (
"context"
"embed"
"encoding/json"
"net/http"
"strings"
gi18n "github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
//go:embed locales/*.json
var localeFS embed.FS
const (
CookieName = "lang"
)
type ctxKey string
const (
ctxKeyLang ctxKey = "lang"
ctxKeyLocalizer ctxKey = "localizer"
)
// Bundle initializes the go-i18n bundle and loads embedded locale files.
func Bundle() (*gi18n.Bundle, error) {
b := gi18n.NewBundle(language.English)
b.RegisterUnmarshalFunc("json", json.Unmarshal)
// Load embedded message files
for _, name := range []string{"locales/en.json", "locales/fr.json"} {
data, err := localeFS.ReadFile(name)
if err != nil {
return nil, err
}
if _, err := b.ParseMessageFileBytes(data, name); err != nil {
return nil, err
}
}
return b, nil
}
// LanguageFromRequest resolves the preferred language from cookie or Accept-Language.
func LanguageFromRequest(r *http.Request) string {
// 1. Cookie preference
if c, err := r.Cookie(CookieName); err == nil {
if c.Value == "fr" {
return "fr"
}
return "en"
}
// 2. Accept-Language header
al := r.Header.Get("Accept-Language")
if strings.HasPrefix(strings.ToLower(al), "fr") {
return "fr"
}
return "en"
}
// LanguageMiddleware injects `lang` and a gi18n.Localizer in the request context.
func LanguageMiddleware(bundle *gi18n.Bundle) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
lang := LanguageFromRequest(r)
loc := gi18n.NewLocalizer(bundle, lang)
ctx := context.WithValue(r.Context(), ctxKeyLang, lang)
ctx = context.WithValue(ctx, ctxKeyLocalizer, loc)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// FromContext returns (lang, localizer) if present.
func FromContext(ctx context.Context) (string, *gi18n.Localizer) {
var lang string
if v := ctx.Value(ctxKeyLang); v != nil {
if s, ok := v.(string); ok {
lang = s
}
}
var loc *gi18n.Localizer
if v := ctx.Value(ctxKeyLocalizer); v != nil {
if l, ok := v.(*gi18n.Localizer); ok {
loc = l
}
}
if lang == "" {
lang = "en"
}
if loc == nil {
loc = gi18n.NewLocalizer(gi18n.NewBundle(language.English), lang)
}
return lang, loc
}
// LangFromContext returns the resolved language code from context.
func LangFromContext(ctx context.Context) string {
lang, _ := FromContext(ctx)
return lang
}
// Localize returns the localized message for a given message ID.
func Localize(ctx context.Context, id string) string {
_, loc := FromContext(ctx)
msg, err := loc.Localize(&gi18n.LocalizeConfig{MessageID: id})
if err != nil {
return id
}
return msg
}
// TranslatorFromRequest returns a simple `func(id string) string` based on request context.
func TranslatorFromRequest(r *http.Request) func(string) string {
_, loc := FromContext(r.Context())
return func(id string) string {
msg, err := loc.Localize(&gi18n.LocalizeConfig{MessageID: id})
if err != nil {
// Fallback to message id
return id
}
return msg
}
}