126 lines
2.9 KiB
Go
126 lines
2.9 KiB
Go
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
|
||
|
|
}
|