329 lines
9.2 KiB
Go
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))
|
|
}
|