Files
langx/common.go
T

139 lines
3.9 KiB
Go
Raw Permalink Normal View History

2024-11-10 22:28:02 +08:00
package langx
2026-09-20 22:20:16 +08:00
import (
"context"
"sort"
"strconv"
"strings"
)
2024-11-10 22:28:02 +08:00
2026-09-20 22:20:16 +08:00
// ctxKey 使用私有类型做 context 的键, 避免与调用方使用的字符串键冲突
type ctxKey struct{ name string }
2024-11-10 22:28:02 +08:00
2026-09-20 22:20:16 +08:00
var langCtxKey = ctxKey{"lang"}
// acceptLanguageKey 是读取请求语言时使用的键。
// 这里故意保留字符串 "Accept-Language", 以兼容把 HTTP 头直接放进 context 的既有用法。
const acceptLanguageKey = "Accept-Language"
// SetCtxLang 把语言写入 context, 其优先级高于 Accept-Language 请求头。
// ctx 为 nil 时的行为与 context.WithValue 一致(panic)。
2024-11-10 22:28:02 +08:00
func SetCtxLang(ctx context.Context, lang string) context.Context {
2026-09-20 22:20:16 +08:00
return context.WithValue(ctx, langCtxKey, lang)
2024-11-10 22:28:02 +08:00
}
2026-09-20 22:20:16 +08:00
// GetCtxLang 按 显式设置 > Accept-Language 协商 > 默认语言 的顺序返回语言。
//
// 该函数不会 panic: ctx 为 nil、context 中的值类型不是 string/[]string 都会安全回退。
2024-11-10 22:28:02 +08:00
func GetCtxLang(ctx context.Context) string {
2026-09-20 22:20:16 +08:00
if ctx == nil {
return GetDefaultLang()
2024-11-10 22:28:02 +08:00
}
2026-09-20 22:20:16 +08:00
if v, ok := ctx.Value(langCtxKey).(string); ok {
if lang := strings.TrimSpace(v); lang != "" {
return lang
2025-10-26 15:41:30 +08:00
}
}
2026-09-20 22:20:16 +08:00
// 兼容直接塞 HTTP 头的用法
if header := headerFromCtx(ctx); header != "" {
if lang := NegotiateLang(header); lang != "" {
return lang
}
2025-10-26 15:41:30 +08:00
}
2026-09-20 22:20:16 +08:00
return GetDefaultLang()
}
2025-10-26 15:41:30 +08:00
2026-09-20 22:20:16 +08:00
// 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
2024-11-10 22:28:02 +08:00
}