多语言翻译
This commit is contained in:
@@ -1,287 +1,328 @@
|
||||
// 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"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type langx struct {
|
||||
ops *options
|
||||
codeMap map[string]int
|
||||
transMap map[string]map[string]string
|
||||
mut sync.RWMutex
|
||||
// state 是一份不可变快照: 一旦被 current 发布, 其中的 map 就不会再被修改。
|
||||
// 读路径直接使用它, 因此无需加锁; 写路径总是构造新快照后再发布。
|
||||
type state struct {
|
||||
ops options
|
||||
code map[string]int
|
||||
trans map[string]map[string]string // key 为归一化后的语言名
|
||||
}
|
||||
|
||||
var l *langx = &langx{
|
||||
ops: defaultOptions(),
|
||||
codeMap: make(map[string]int),
|
||||
transMap: make(map[string]map[string]string),
|
||||
mut: sync.RWMutex{},
|
||||
}
|
||||
var (
|
||||
current atomic.Pointer[state]
|
||||
writeMu sync.Mutex
|
||||
)
|
||||
|
||||
// 这是语言的Code
|
||||
func RegisterCode(datas map[string]int) {
|
||||
l.mut.Lock()
|
||||
defer l.mut.Unlock()
|
||||
if datas == nil {
|
||||
datas = make(map[string]int)
|
||||
}
|
||||
l.codeMap = datas
|
||||
}
|
||||
|
||||
// 追加覆盖Code
|
||||
func AppendCode(datas map[string]int) {
|
||||
l.mut.Lock()
|
||||
defer l.mut.Unlock()
|
||||
|
||||
if datas == nil {
|
||||
datas = make(map[string]int)
|
||||
}
|
||||
if l.codeMap == nil {
|
||||
l.codeMap = map[string]int{}
|
||||
}
|
||||
|
||||
for k, v := range datas {
|
||||
l.codeMap[k] = v
|
||||
func newState() *state {
|
||||
return &state{
|
||||
ops: defaultOptions(),
|
||||
code: map[string]int{},
|
||||
trans: map[string]map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
// 注册语言翻译
|
||||
func RegisterTrans(langName string, trans map[string]string) {
|
||||
l.mut.Lock()
|
||||
defer l.mut.Unlock()
|
||||
l.transMap[langName] = trans
|
||||
func init() {
|
||||
current.Store(newState())
|
||||
}
|
||||
|
||||
// 追加覆盖翻译
|
||||
func AppendTrans(langName string, trans map[string]string) {
|
||||
l.mut.Lock()
|
||||
defer l.mut.Unlock()
|
||||
for k, v := range trans {
|
||||
if l.transMap[langName] == nil {
|
||||
l.transMap[langName] = map[string]string{}
|
||||
}
|
||||
l.transMap[langName][k] = v
|
||||
}
|
||||
// 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()))
|
||||
}
|
||||
|
||||
// 直接读取文件夹获取配置
|
||||
// 要求:
|
||||
// 1.json格式文件
|
||||
// 2.code.json为自定义响应码 格式map[string]int{}
|
||||
// 3.其他的json文件为对应语音 格式map[string]string{}
|
||||
// 4.如果json解析错误将会panic
|
||||
func RegisterDir(dir string) error {
|
||||
// 遍历dir获取.json的文件
|
||||
err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(info.Name(), ".json") {
|
||||
// 读取文件
|
||||
fileName := strings.Replace(info.Name(), ".json", "", 1)
|
||||
by, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fileName == "code" {
|
||||
data := map[string]int{}
|
||||
err = json.Unmarshal(by, &data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
RegisterCode(data)
|
||||
} else {
|
||||
data := map[string]string{}
|
||||
err = json.Unmarshal(by, &data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
RegisterTrans(fileName, data)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// code文件为状态码,其他为对应的语言文件,文件名为语言名
|
||||
return err
|
||||
}
|
||||
|
||||
func RegisterEmbed(asset embed.FS) error {
|
||||
paths, err := readEmbedAllPath(asset, "")
|
||||
// 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
|
||||
}
|
||||
|
||||
for _, ff := range paths {
|
||||
// 获取文件的后缀
|
||||
|
||||
fileName := strings.Replace(ff.path, ".json", "", 1)
|
||||
if fileName == "code" {
|
||||
data := map[string]int{}
|
||||
err = json.Unmarshal(ff.datas, &data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
RegisterCode(data)
|
||||
} else {
|
||||
data := map[string]string{}
|
||||
err = json.Unmarshal(ff.datas, &data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
RegisterTrans(fileName, data)
|
||||
}
|
||||
}
|
||||
|
||||
current.Store(next)
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileData struct {
|
||||
path string
|
||||
datas []byte
|
||||
// ---- 快照构造: 只创建新 map, 绝不修改已经发布的 map ----
|
||||
|
||||
func setOptions(s *state, ops options) *state {
|
||||
return &state{ops: ops, code: s.code, trans: s.trans}
|
||||
}
|
||||
|
||||
func readEmbedAllPath(asset embed.FS, path string) ([]fileData, error) {
|
||||
newRoot := ""
|
||||
if path == "" {
|
||||
newRoot = "."
|
||||
} else {
|
||||
newRoot = path
|
||||
}
|
||||
|
||||
fs, err := asset.ReadDir(newRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var files []fileData
|
||||
for _, f := range fs {
|
||||
if f.IsDir() {
|
||||
chaild := ""
|
||||
if newRoot == "." {
|
||||
chaild = f.Name()
|
||||
} else {
|
||||
chaild = newRoot + "/" + f.Name()
|
||||
}
|
||||
|
||||
df, err := readEmbedAllPath(asset, chaild)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, df...)
|
||||
continue
|
||||
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
|
||||
}
|
||||
|
||||
by, err := asset.ReadFile(newRoot + "/" + f.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ff := fileData{
|
||||
path: f.Name(),
|
||||
datas: by,
|
||||
}
|
||||
|
||||
files = append(files, ff)
|
||||
return next
|
||||
}
|
||||
return files, nil
|
||||
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
|
||||
}
|
||||
|
||||
// 获取翻译
|
||||
// 包含Code和Message
|
||||
func GetTransFormat(lang string, key string, format map[string]string) (code int, str string) {
|
||||
code = GetCode(key)
|
||||
str = GetFormat(lang, key, format)
|
||||
return
|
||||
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, map[string]string{})
|
||||
return GetTransFormat(lang, key, nil)
|
||||
}
|
||||
|
||||
func GetTransCtx(ctx context.Context, key string) (code int, str string) {
|
||||
return GetTrans(getLangFromCtx(ctx), key)
|
||||
}
|
||||
|
||||
func GetTransFormatCtx(ctx context.Context, key string, format map[string]string) (code int, str string) {
|
||||
return GetTransFormat(getLangFromCtx(ctx), key, format)
|
||||
}
|
||||
|
||||
// 根据Key获取code
|
||||
func GetCode(key string) int {
|
||||
l.mut.RLock()
|
||||
defer l.mut.RUnlock()
|
||||
code, ok := l.codeMap[key]
|
||||
// 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 {
|
||||
return l.ops.defaultCode
|
||||
notifyMissing(s, lang, key)
|
||||
return code, key
|
||||
}
|
||||
return code
|
||||
return code, applyFormat(s.ops, str, format)
|
||||
}
|
||||
|
||||
// 获取翻译
|
||||
func GetMsg(lang string, key string) string {
|
||||
l.mut.RLock()
|
||||
defer l.mut.RUnlock()
|
||||
// 找指定语言
|
||||
str, ok := l.transMap[lang]
|
||||
if ok {
|
||||
val, ok := str[key]
|
||||
if ok {
|
||||
return val
|
||||
}
|
||||
}
|
||||
// 找默认语言
|
||||
str, ok = l.transMap[l.ops.defaultLang]
|
||||
if ok {
|
||||
val, ok := str[key]
|
||||
if ok {
|
||||
return val
|
||||
}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
// 从ctx里面获取语言
|
||||
// GetMsgCtx 从 context 中取语言, 再取译文。
|
||||
func GetMsgCtx(ctx context.Context, key string) string {
|
||||
return GetMsg(getLangFromCtx(ctx), key)
|
||||
return GetMsg(GetCtxLang(ctx), key)
|
||||
}
|
||||
|
||||
// 拼接回复
|
||||
// GetFormat 取译文并替换占位符, 未提供值的占位符会原样保留在结果里。
|
||||
func GetFormat(lang string, key string, arr map[string]string) string {
|
||||
str := GetMsg(lang, key)
|
||||
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, fmt.Sprintf(l.ops.replaceKey, k), v)
|
||||
str = strings.ReplaceAll(str, ops.replacePrefix+k+ops.replaceSuffix, v)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func GetFormatCtx(ctx context.Context, key string, arr map[string]string) string {
|
||||
return GetFormat(getLangFromCtx(ctx), key, arr)
|
||||
}
|
||||
|
||||
// 获取默认Code
|
||||
func GetDefaultCode() int {
|
||||
return l.ops.defaultCode
|
||||
}
|
||||
|
||||
// 获取默认语言
|
||||
func GetDefaultLang() string {
|
||||
return l.ops.defaultLang
|
||||
}
|
||||
|
||||
func getLangFromCtx(ctx context.Context) string {
|
||||
lang := GetCtxLang(ctx)
|
||||
if lang == "" {
|
||||
lang = l.ops.defaultLang
|
||||
// notifyMissing 在锁外回调用户注册的缺失处理器
|
||||
func notifyMissing(s *state, lang, key string) {
|
||||
if s.ops.missing != nil {
|
||||
s.ops.missing(lang, key)
|
||||
}
|
||||
return lang
|
||||
}
|
||||
|
||||
// normalizeLang 归一化语言名: 去首尾空白并转小写
|
||||
func normalizeLang(lang string) string {
|
||||
return strings.ToLower(strings.TrimSpace(lang))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user