Files

126 lines
2.9 KiB
Go
Raw Permalink Normal View History

package i18n
import (
"context"
"strconv"
"strings"
)
// Default is the fallback UI/API locale when Accept-Language is missing or unsupported.
const Default = "en"
// Supported UI/API locales for public error/validation copy.
// Keep aligned with apps/web/src/lib/i18n/locales.ts (UI_LOCALES).
var Supported = []string{"en", "es", "fr", "de", "it", "pt", "nl", "pl", "ja"}
var supportedSet map[string]struct{}
func init() {
supportedSet = make(map[string]struct{}, len(Supported))
for _, code := range Supported {
supportedSet[code] = struct{}{}
}
}
type ctxKey struct{}
// WithLocale stores a resolved locale on ctx.
func WithLocale(ctx context.Context, locale string) context.Context {
return context.WithValue(ctx, ctxKey{}, Normalize(locale))
}
// FromContext returns the locale stored by middleware, or Default.
func FromContext(ctx context.Context) string {
if ctx == nil {
return Default
}
if v, ok := ctx.Value(ctxKey{}).(string); ok && v != "" {
return v
}
return Default
}
// Normalize lowercases/trims and maps to a supported primary language tag, or Default.
func Normalize(raw string) string {
code := strings.ToLower(strings.TrimSpace(raw))
if code == "" || code == "*" {
return Default
}
if i := strings.IndexByte(code, '-'); i > 0 {
code = code[:i]
}
if i := strings.IndexByte(code, '_'); i > 0 {
code = code[:i]
}
if _, ok := supportedSet[code]; ok {
return code
}
return Default
}
// IsSupported reports whether the primary language tag is in Supported.
func IsSupported(raw string) bool {
code := strings.ToLower(strings.TrimSpace(raw))
if code == "" {
return false
}
if i := strings.IndexByte(code, '-'); i > 0 {
code = code[:i]
}
if i := strings.IndexByte(code, '_'); i > 0 {
code = code[:i]
}
_, ok := supportedSet[code]
return ok
}
// Resolve picks the best supported locale from an Accept-Language header value.
// Quality values are respected; unsupported tags are skipped; empty → Default.
func Resolve(acceptLanguage string) string {
header := strings.TrimSpace(acceptLanguage)
if header == "" {
return Default
}
bestTag := ""
bestQ := -1.0
for _, part := range strings.Split(header, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
tag := part
q := 1.0
if i := strings.IndexByte(part, ';'); i >= 0 {
tag = strings.TrimSpace(part[:i])
for _, p := range strings.Split(part[i+1:], ";") {
p = strings.TrimSpace(p)
if len(p) >= 2 && (p[0] == 'q' || p[0] == 'Q') && p[1] == '=' {
if parsed, err := strconv.ParseFloat(strings.TrimSpace(p[2:]), 64); err == nil {
q = parsed
}
}
}
}
primary := strings.ToLower(strings.TrimSpace(tag))
if primary == "*" {
if q > bestQ {
bestQ = q
bestTag = Default
}
continue
}
if !IsSupported(primary) {
continue
}
norm := Normalize(primary)
if q > bestQ {
bestQ = q
bestTag = norm
}
}
if bestTag == "" {
return Default
}
return bestTag
}