多语言翻译
This commit is contained in:
@@ -1,35 +1,138 @@
|
||||
package langx
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const ctxLangKey string = "ctxLang"
|
||||
// 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, ctxLangKey, lang)
|
||||
return context.WithValue(ctx, langCtxKey, lang)
|
||||
}
|
||||
|
||||
// GetCtxLang 按 显式设置 > Accept-Language 协商 > 默认语言 的顺序返回语言。
|
||||
//
|
||||
// 该函数不会 panic: ctx 为 nil、context 中的值类型不是 string/[]string 都会安全回退。
|
||||
func GetCtxLang(ctx context.Context) string {
|
||||
// 指定的
|
||||
lang := ctx.Value(ctxLangKey)
|
||||
|
||||
l := ""
|
||||
|
||||
if lang != nil {
|
||||
l = lang.(string)
|
||||
if ctx == nil {
|
||||
return GetDefaultLang()
|
||||
}
|
||||
|
||||
// HTTP头部的
|
||||
if l == "" {
|
||||
lang = ctx.Value("Accept-Language")
|
||||
if lang != nil {
|
||||
l = lang.(string)
|
||||
if v, ok := ctx.Value(langCtxKey).(string); ok {
|
||||
if lang := strings.TrimSpace(v); lang != "" {
|
||||
return lang
|
||||
}
|
||||
}
|
||||
|
||||
// 默认的
|
||||
if l == "" {
|
||||
l = GetDefaultLang()
|
||||
// 兼容直接塞 HTTP 头的用法
|
||||
if header := headerFromCtx(ctx); header != "" {
|
||||
if lang := NegotiateLang(header); lang != "" {
|
||||
return lang
|
||||
}
|
||||
}
|
||||
return GetDefaultLang()
|
||||
}
|
||||
|
||||
return l
|
||||
// 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user