// Package langx 提供轻量的多语言响应能力: 注册响应码与多语言译文, 按语言(含区域回退)取值, // 并支持占位符替换。 // // 典型用法: // // _ = langx.InitLangx(langx.SetDefaultLanguage("zh"), langx.SetDefaultCode(500)) // _ = langx.RegisterDir("langs") // 也可以是 RegisterEmbed / RegisterFS / RegisterTrans // code, msg := langx.GetTransFormatCtx(ctx, "login_success", map[string]string{"name": "张三"}) // // 并发说明: 注册与读取可以安全地并发进行。读取路径无锁, 并且总是看到某个完整快照, // 不会出现读到一半被写坏的情况; 所有写入都串行执行并原子发布新快照。 // 注册时会对传入的 map 做深拷贝, 注册之后调用方修改自己的 map 不会影响库内数据。 package langx import ( "context" "sort" "strings" "sync" "sync/atomic" ) // state 是一份不可变快照: 一旦被 current 发布, 其中的 map 就不会再被修改。 // 读路径直接使用它, 因此无需加锁; 写路径总是构造新快照后再发布。 type state struct { ops options code map[string]int trans map[string]map[string]string // key 为归一化后的语言名 } var ( current atomic.Pointer[state] writeMu sync.Mutex ) func newState() *state { return &state{ ops: defaultOptions(), code: map[string]int{}, trans: map[string]map[string]string{}, } } func init() { current.Store(newState()) } // load 返回当前快照, 一次调用内应复用同一个快照以保证数据一致 func load() *state { return current.Load() } // update 串行执行写操作并发布新快照 func update(fn func(old *state) *state) { writeMu.Lock() defer writeMu.Unlock() current.Store(fn(current.Load())) } // updateErr 与 update 相同, 但 fn 返回错误时不发布任何变更 func updateErr(fn func(old *state) (*state, error)) error { writeMu.Lock() defer writeMu.Unlock() next, err := fn(current.Load()) if err != nil { return err } current.Store(next) return nil } // ---- 快照构造: 只创建新 map, 绝不修改已经发布的 map ---- func setOptions(s *state, ops options) *state { return &state{ops: ops, code: s.code, trans: s.trans} } func setCodes(s *state, m map[string]int, merge bool) *state { next := &state{ops: s.ops, trans: s.trans} if !merge { next.code = make(map[string]int, len(m)) for k, v := range m { next.code[k] = v } return next } next.code = make(map[string]int, len(s.code)+len(m)) for k, v := range s.code { next.code[k] = v } for k, v := range m { next.code[k] = v } return next } func setTrans(s *state, lang string, m map[string]string, merge bool) *state { lang = normalizeLang(lang) next := &state{ops: s.ops, code: s.code} next.trans = make(map[string]map[string]string, len(s.trans)+1) for k, v := range s.trans { next.trans[k] = v // 内层 map 可以共享, 但绝不会被就地修改 } src := s.trans[lang] if !merge { src = nil } inner := make(map[string]string, len(src)+len(m)) for k, v := range src { inner[k] = v } for k, v := range m { inner[k] = v } next.trans[lang] = inner return next } // ---- 注册 ---- // RegisterCode 整体替换响应码表, 传 nil 表示清空。 func RegisterCode(datas map[string]int) { update(func(old *state) *state { return setCodes(old, datas, false) }) } // AppendCode 追加响应码, 已存在的 key 会被覆盖。 func AppendCode(datas map[string]int) { update(func(old *state) *state { return setCodes(old, datas, true) }) } // RegisterTrans 整体替换某个语言的译文, 传 nil 表示清空该语言。 func RegisterTrans(langName string, trans map[string]string) { update(func(old *state) *state { return setTrans(old, langName, trans, false) }) } // AppendTrans 追加某个语言的译文, 已存在的 key 会被覆盖。 func AppendTrans(langName string, trans map[string]string) { update(func(old *state) *state { return setTrans(old, langName, trans, true) }) } // ---- 读取 ---- // GetTrans 取 key 对应的响应码与译文。 func GetTrans(lang string, key string) (code int, str string) { return GetTransFormat(lang, key, nil) } // GetTransFormat 取 key 对应的响应码与译文, 并把占位符替换为 format 中的值。 // code 与 str 来自同一份快照, 不会出现撕裂。 func GetTransFormat(lang string, key string, format map[string]string) (code int, str string) { s := load() code = codeOf(s, key) str, ok := findMsg(s, lang, key) if !ok { notifyMissing(s, lang, key) return code, key } return code, applyFormat(s.ops, str, format) } // GetTransCtx 从 context 中取语言, 再取响应码与译文。 func GetTransCtx(ctx context.Context, key string) (code int, str string) { return GetTrans(GetCtxLang(ctx), key) } // GetTransFormatCtx 从 context 中取语言, 再取响应码与译文并替换占位符。 func GetTransFormatCtx(ctx context.Context, key string, format map[string]string) (code int, str string) { return GetTransFormat(GetCtxLang(ctx), key, format) } // GetCode 根据 key 取响应码, 未注册时返回默认码(默认 -1, 见 SetDefaultCode)。 func GetCode(key string) int { return codeOf(load(), key) } // GetMsg 取译文: 先按语言查找(逐级去掉区域后缀), 再按默认语言查找, 都没有时返回 key 本身。 func GetMsg(lang string, key string) string { s := load() if v, ok := findMsg(s, lang, key); ok { return v } notifyMissing(s, lang, key) return key } // GetMsgCtx 从 context 中取语言, 再取译文。 func GetMsgCtx(ctx context.Context, key string) string { return GetMsg(GetCtxLang(ctx), key) } // GetFormat 取译文并替换占位符, 未提供值的占位符会原样保留在结果里。 func GetFormat(lang string, key string, arr map[string]string) string { s := load() str, ok := findMsg(s, lang, key) if !ok { notifyMissing(s, lang, key) str = key } return applyFormat(s.ops, str, arr) } // GetFormatCtx 从 context 中取语言, 再取译文并替换占位符。 func GetFormatCtx(ctx context.Context, key string, arr map[string]string) string { return GetFormat(GetCtxLang(ctx), key, arr) } // Has 判断按回退链(语言 -> 逐级去区域 -> 默认语言)能否取到该 key 的译文。 func Has(lang string, key string) bool { _, ok := findMsg(load(), lang, key) return ok } // HasCode 判断响应码表中是否注册了该 key。 func HasCode(key string) bool { _, ok := load().code[key] return ok } // Langs 返回已注册的语言名(已归一化为小写, 升序排列)。 func Langs() []string { s := load() langs := make([]string, 0, len(s.trans)) for name := range s.trans { langs = append(langs, name) } sort.Strings(langs) return langs } // GetDefaultCode 返回未知 key 使用的响应码。 func GetDefaultCode() int { return load().ops.defaultCode } // GetDefaultLang 返回默认(兜底)语言。 func GetDefaultLang() string { return load().ops.defaultLang } // ---- 内部实现 ---- func codeOf(s *state, key string) int { if code, ok := s.code[key]; ok { return code } return s.ops.defaultCode } // findMsg 依次尝试: 指定语言(逐级去区域后缀) -> 默认语言(同样逐级), 命中即返回 func findMsg(s *state, lang string, key string) (string, bool) { if v, ok := lookupMsg(s.trans, lang, key); ok { return v, true } if normalizeLang(lang) == normalizeLang(s.ops.defaultLang) { return "", false } return lookupMsg(s.trans, s.ops.defaultLang, key) } // lookupMsg 在语言及其各级父语言中查找 key, 例如 zh-Hans-CN -> zh-hans -> zh。 // 精确命中时不产生任何分配, 属于快路径。 func lookupMsg(trans map[string]map[string]string, lang string, key string) (string, bool) { l := normalizeLang(lang) if v, ok := msgOf(trans[l], key); ok { return v, true } for l != "" { if v, ok := msgOf(trans[l], key); ok { return v, true } // RFC 4647 basic filtering: 请求 en 时可以用 en-GB 兜底 if best := bestPrefixLang(trans, l); best != "" { if v, ok := msgOf(trans[best], key); ok { return v, true } } i := strings.LastIndexByte(l, '-') if i <= 0 { break } l = l[:i] } return "", false } func msgOf(m map[string]string, key string) (string, bool) { v, ok := m[key] return v, ok } // bestPrefixLang 返回以 "lang-" 开头且字典序最小的已注册语言, 保证结果稳定 func bestPrefixLang(trans map[string]map[string]string, lang string) string { if lang == "" { return "" } prefix := lang + "-" best := "" for name := range trans { if strings.HasPrefix(name, prefix) && (best == "" || name < best) { best = name } } return best } // applyFormat 用 arr 替换 str 中的占位符 func applyFormat(ops options, str string, arr map[string]string) string { if str == "" || len(arr) == 0 { return str } if ops.replacePrefix != "" && !strings.Contains(str, ops.replacePrefix) { return str } for k, v := range arr { str = strings.ReplaceAll(str, ops.replacePrefix+k+ops.replaceSuffix, v) } return str } // notifyMissing 在锁外回调用户注册的缺失处理器 func notifyMissing(s *state, lang, key string) { if s.ops.missing != nil { s.ops.missing(lang, key) } } // normalizeLang 归一化语言名: 去首尾空白并转小写 func normalizeLang(lang string) string { return strings.ToLower(strings.TrimSpace(lang)) }