Files
langx/langx.go
T
2026-09-20 22:20:16 +08:00

329 lines
9.2 KiB
Go

// 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))
}