Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user