package langx import ( "context" "sort" "strconv" "strings" ) // ctxKey 使用私有类型做 context 的键, 避免与调用方使用的字符串键冲突 type ctxKey struct{ name string } var langCtxKey = ctxKey{"lang"} // acceptLanguageKey 是读取请求语言时使用的键。 // 这里故意保留字符串 "Accept-Language", 以兼容把 HTTP 头直接放进 context 的既有用法。 const acceptLanguageKey = "Accept-Language" // SetCtxLang 把语言写入 context, 其优先级高于 Accept-Language 请求头。 // ctx 为 nil 时的行为与 context.WithValue 一致(panic)。 func SetCtxLang(ctx context.Context, lang string) context.Context { return context.WithValue(ctx, langCtxKey, lang) } // GetCtxLang 按 显式设置 > Accept-Language 协商 > 默认语言 的顺序返回语言。 // // 该函数不会 panic: ctx 为 nil、context 中的值类型不是 string/[]string 都会安全回退。 func GetCtxLang(ctx context.Context) string { if ctx == nil { return GetDefaultLang() } if v, ok := ctx.Value(langCtxKey).(string); ok { if lang := strings.TrimSpace(v); lang != "" { return lang } } // 兼容直接塞 HTTP 头的用法 if header := headerFromCtx(ctx); header != "" { if lang := NegotiateLang(header); lang != "" { return lang } } return GetDefaultLang() } // headerFromCtx 读取 context 中的 Accept-Language, 类型不匹配时返回空串 func headerFromCtx(ctx context.Context) string { switch v := ctx.Value(acceptLanguageKey).(type) { case string: return strings.TrimSpace(v) case []string: if len(v) > 0 { return strings.TrimSpace(v[0]) } } return "" } // ParseAcceptLanguage 解析 Accept-Language 请求头, 按 q 值从高到低返回候选语言(已归一化为小写)。 // // 无 q 值的项按 1.0 处理; q=0 表示不接受, 会被丢弃; 通配符 "*" 原样保留, 由调用方决定如何处理。 func ParseAcceptLanguage(header string) []string { type candidate struct { lang string q float64 } candidates := make([]candidate, 0, 4) for _, part := range strings.Split(header, ",") { part = strings.TrimSpace(part) if part == "" { continue } lang, q := part, 1.0 if i := strings.IndexByte(part, ';'); i >= 0 { lang = strings.TrimSpace(part[:i]) for _, param := range strings.Split(part[i+1:], ";") { param = strings.TrimSpace(param) if len(param) > 2 && (param[0] == 'q' || param[0] == 'Q') && param[1] == '=' { if v, err := strconv.ParseFloat(strings.TrimSpace(param[2:]), 64); err == nil { q = v } } } } lang = normalizeLang(lang) if lang == "" { continue } if q <= 0 { // q=0 表示明确不接受该语言 continue } candidates = append(candidates, candidate{lang: lang, q: q}) } sort.SliceStable(candidates, func(i, j int) bool { return candidates[i].q > candidates[j].q }) langs := make([]string, 0, len(candidates)) for _, c := range candidates { langs = append(langs, c.lang) } return langs } // NegotiateLang 在已注册的语言中挑出与 Accept-Language 最匹配的一个; // 匹配时先精确匹配, 再逐级去掉区域后缀(zh-CN -> zh); 没有匹配时返回空串。 func NegotiateLang(header string) string { s := load() if len(s.trans) == 0 { return "" } for _, lang := range ParseAcceptLanguage(header) { if lang == "*" { return "" } if hit, ok := matchLang(s, lang); ok { return hit } } return "" } // matchLang 返回已注册语言中匹配 lang 的那一个: // 先精确匹配, 再逐级去掉区域后缀, 最后按语言前缀匹配(请求 en-US 可以命中已注册的 en-GB)。 func matchLang(s *state, lang string) (string, bool) { l := normalizeLang(lang) for l != "" { if _, ok := s.trans[l]; ok { return l, true } if best := bestPrefixLang(s.trans, l); best != "" { return best, true } i := strings.LastIndexByte(l, '-') if i <= 0 { break } l = l[:i] } return "", false }