8.4 KiB
8.4 KiB
langx
轻量的 Go 多语言响应库:注册响应码与多语言译文,按语言(支持区域回退)取值,并支持占位符替换。零第三方依赖,go 1.19 起可用。
code, msg := langx.GetTransFormatCtx(ctx, "login_success", map[string]string{"name": "张三"})
// code = 200, msg = "你好 张三, 登录成功"
特性
- 资源来源灵活:
embed.FS、磁盘目录、任意fs.FS,或直接传 map。 - 语言协商:解析
Accept-Language(q 值排序 +zh-CN → zh区域回退 + RFC 4647 前缀匹配,请求en-US可命中已注册的en-GB)。 - 占位符替换:默认
#name#形式,模式可自定义;缺少参数时占位符原样保留,便于及早发现。 - 并发安全:读取路径无锁,注册与读取可安全并行,永远不会读到"写了一半"的数据。
- 明确的失败:未知 key 默认不再返回成功码;资源文件解析失败时整体失败,不会写入半个文件。
- 不篡改调用方数据:注册时深拷贝,注册之后调用方修改自己的 map 不会影响库内数据。
安装
go get github.com/yuninks/langx
快速开始
完整可运行示例见 example/main.go,执行 go run ./example 即可查看输出。
package main
import (
"context"
"embed"
"fmt"
"github.com/yuninks/langx"
)
//go:embed langs
var langFS embed.FS
func main() {
// 1. 可选配置:默认语言、未知 key 的响应码、漏翻回调
if err := langx.InitLangx(
langx.SetDefaultLanguage("zh"),
langx.SetDefaultCode(500), // code 若会作为 HTTP 状态码,建议让缺失的 key 以失败收场
langx.SetMissingHandler(func(lang, key string) {
fmt.Printf("缺少译文: lang=%s key=%s\n", lang, key)
}),
); err != nil {
panic(err)
}
// 2. 装载资源(四选一)
if err := langx.RegisterEmbed(langFS); err != nil { // 或 RegisterDir("langs") / RegisterFS(fsys)
panic(err)
}
// 3. 取值
code, msg := langx.GetTrans("zh", "login_success")
// 4. 带占位符
code, msg = langx.GetTransFormat("en", "welcome", map[string]string{"name": "Tom"})
// 5. 从请求 context 取语言:显式设置 > Accept-Language 协商 > 默认语言
ctx := context.WithValue(context.Background(), "Accept-Language", "en-US,en;q=0.9")
msg = langx.GetMsgCtx(ctx, "login_success")
}
资源文件约定
RegisterDir / RegisterFS / RegisterEmbed 使用同一套规则:
| 路径 | 含义 |
|---|---|
zh.json |
语言 zh 的译文,格式 map[string]string |
en-US.json |
语言 en-US 的译文(语言名不区分大小写,统一按小写归一化) |
code.json(根目录) |
响应码,格式 map[string]int |
zh/common.json、zh/errors.json |
子目录第一段是语言名,多个文件按键合并进 zh |
README.md 等非 json 文件 |
直接忽略 |
langs/(唯一子目录且自身没有 json) |
视为容器目录,自动向内继续查找(最多 8 层) |
规则要点:
- 加载是合并语义:多次调用会累加,同一个 key 以最后一次为准。
- 加载是事务的:任意文件解析失败都会返回错误,且不会改变已加载的数据。
- 根目录下的
code.json是保留文件名;更深层的code.json(如zh/code.json)按该语言的普通文件处理。
API 一览
注册
| 函数 | 说明 |
|---|---|
RegisterCode(map[string]int) |
整体替换响应码表,传 nil 清空 |
AppendCode(map[string]int) |
追加/覆盖响应码 |
RegisterTrans(lang, map[string]string) |
整体替换某语言译文 |
AppendTrans(lang, map[string]string) |
追加/覆盖某语言译文 |
RegisterDir(dir string) error |
从磁盘目录加载 |
RegisterFS(fsys fs.FS) error |
从任意 fs.FS 加载 |
RegisterEmbed(asset embed.FS) error |
从 embed.FS 加载 |
取值
| 函数 | 说明 |
|---|---|
GetTrans(lang, key) (int, string) |
取响应码与译文 |
GetTransFormat(lang, key, map[string]string) (int, string) |
同上,并替换占位符 |
GetCode(key) int |
只取响应码 |
GetMsg(lang, key) string |
只取译文 |
GetFormat(lang, key, map[string]string) string |
只取译文并替换占位符 |
GetTransCtx / GetTransFormatCtx / GetMsgCtx / GetFormatCtx |
从 context.Context 取语言 |
Has(lang, key) bool / HasCode(key) bool |
判断译文/响应码是否存在 |
Langs() []string |
已注册语言(小写、升序) |
配置
| 函数 | 默认值 | 说明 |
|---|---|---|
InitLangx(...Option) error |
— | 应用配置;可重复调用;配置非法时返回错误且完全不生效 |
SetDefaultLanguage(lang) |
zh |
默认(兜底)语言 |
SetDefaultCode(code) |
-1 |
未知 key 的响应码 |
SetReplaceKey(pattern) |
#%s# |
占位符模式,必须包含且只包含一个 %s |
SetMissingHandler(fn) |
nil |
译文缺失回调,便于记录漏翻的 key |
Reset() |
— | 清空全部数据并恢复默认配置(测试用) |
语言协商
GetCtxLang(ctx) 按以下优先级返回语言:
langx.SetCtxLang(ctx, "en")显式设置的值(即使该语言没有注册也照用);context中的Accept-Language(string或[]string,后者兼容http.Header风格),交给NegotiateLang与已注册语言协商;- 默认语言
GetDefaultLang()。
协商规则(ParseAcceptLanguage / NegotiateLang 也已导出,可单独使用):
- 按
q值从高到低排序,q=0表示明确不接受、会被丢弃,*视为"任意"并回落默认语言; - 每个候选先精确匹配,再逐级去掉区域后缀(
zh-Hans-CN → zh-hans → zh),最后按语言前缀匹配(请求en-US可命中已注册的en-GB)。
取值时的回退链同样是:指定语言 → 逐级去区域 → 语言前缀 → 默认语言(同样逐级);全都取不到时返回 key 本身,并触发 SetMissingHandler 回调。
占位符
langx.RegisterTrans("zh", map[string]string{"welcome": "你好 #name#, 欢迎回来"})
langx.GetFormat("zh", "welcome", map[string]string{"name": "张三"}) // 你好 张三, 欢迎回来
- 模式由
SetReplaceKey配置,必须包含且只包含一个%s;写成#{}#、#%d#这类非法模式会在InitLangx时报错,而不是静默失效。 - 同一个占位符出现多次会全部替换;没有提供值的占位符原样保留。
- 未注册的 key 也会走替换逻辑,方便过渡期使用动态 key。
并发模型
- 读取(
Get*)基于不可变快照,完全无锁;一次调用内使用同一份快照,因此GetTransFormat返回的 code 与 msg 一定是一致的。 - 写入(注册与配置)串行执行并原子发布新快照:读取要么看到旧数据,要么看到新数据,不会看到中间状态,也不存在数据竞态。
- 频繁写入会重建索引,建议把注册放在启动阶段;运行期更新适合用
AppendTrans做增量补丁。
从 v0.0.7 升级
本次修复包含以下行为变更,请按需调整:
| 变更 | 说明 |
|---|---|
InitLangx 现在返回 error |
当语句调用不受影响;非法 replaceKey(如 #{}#)此前会静默失效,现在返回错误且配置完全不生效 |
未知 key 的默认响应码 200 → -1 |
旧行为会让拼错的错误码静默变成"成功";需要旧行为可显式 SetDefaultCode(200),HTTP 场景建议 SetDefaultCode(500) |
| 注册不再与调用方共享 map | 依赖"注册后修改自己的 map 仍然生效"的代码需要改为再次调用注册函数 |
加载器只处理 .json,子目录参与语言名推导 |
此前子目录中的同名文件会互相覆盖、en/common.json 会被当成语言 common;现在子目录第一段即语言名并按键合并 |
| 加载失败不再留下半加载状态 | 此前 RegisterEmbed 遇到非 json 文件会中断,已注册的部分仍然生效 |
GetCtxLang 返回协商后的语言 |
此前会原样返回请求头字符串(如 en-US,en;q=0.9),并在 context 值不是 string 时 panic |
RegisterCode / RegisterTrans / AppendCode / AppendTrans 语义未变 |
整体替换 / 追加覆盖的语义与旧版一致 |
开发
gofmt -l . # 应为空
go vet ./...
go test -race ./...
go run ./example
License
尚未添加 LICENSE 文件,需要仓库作者确定许可协议后再补充。