package i18n import ( "sort" "strconv" "strings" ) // weightedTag is one entry of a parsed Accept-Language header. type weightedTag struct { primary string // "fr" out of "fr-FR" q float64 } // parseAcceptLanguage turns a raw "Accept-Language" header value (RFC // 9110 ยง12.5.4 โ€” e.g. "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7") into its // tags' primary subtags, ordered by descending quality weight. A // malformed segment is skipped rather than aborting the whole parse โ€” // browsers have been known to send odd values, and the worst case of // ignoring one bad segment is falling back to Default, never a panic // or a rejected request. func parseAcceptLanguage(header string) []weightedTag { var tags []weightedTag for _, part := range strings.Split(header, ",") { part = strings.TrimSpace(part) if part == "" { continue } tag, qStr, hasQ := strings.Cut(part, ";") tag = strings.TrimSpace(tag) if tag == "" || tag == "*" { continue } q := 1.0 if hasQ { qStr = strings.TrimSpace(qStr) if v, ok := strings.CutPrefix(qStr, "q="); ok { if parsed, err := strconv.ParseFloat(strings.TrimSpace(v), 64); err == nil { q = parsed } } } primary, _, _ := strings.Cut(tag, "-") tags = append(tags, weightedTag{primary: strings.ToLower(primary), q: q}) } sort.SliceStable(tags, func(i, j int) bool { return tags[i].q > tags[j].q }) return tags } // NegotiateAcceptLanguage picks the best Supported locale for a raw // "Accept-Language" header value, falling back to Default if nothing // in the header is one enkly ships a catalog for (or the header is // empty / unparseable). Callers only reach for this when there's no // stronger signal available โ€” see the gateway's localeFrom, which // tries the enkly_lang cookie first and only falls through to this // for a visitor who has never touched the language switcher. func NegotiateAcceptLanguage(header string) Locale { for _, tag := range parseAcceptLanguage(header) { if IsSupported(tag.primary) { return Locale(tag.primary) } } return Default }