139 lines
3.9 KiB
Go
139 lines
3.9 KiB
Go
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
|
|
}
|