Files
langx/readme.md
T

187 lines
8.4 KiB
Markdown
Raw Normal View History

2026-09-20 22:20:16 +08:00
# langx
2024-06-30 12:07:53 +08:00
2026-09-20 22:20:16 +08:00
轻量的 Go 多语言响应库:注册响应码与多语言译文,按语言(支持区域回退)取值,并支持占位符替换。零第三方依赖,`go 1.19` 起可用。
2024-07-24 19:47:00 +08:00
2026-09-20 22:20:16 +08:00
```go
code, msg := langx.GetTransFormatCtx(ctx, "login_success", map[string]string{"name": "张三"})
// code = 200, msg = "你好 张三, 登录成功"
```
2024-07-24 19:47:00 +08:00
2026-09-20 22:20:16 +08:00
## 特性
2024-06-30 12:07:53 +08:00
2026-09-20 22:20:16 +08:00
- **资源来源灵活**:`embed.FS`、磁盘目录、任意 `fs.FS`,或直接传 map。
- **语言协商**:解析 `Accept-Language`(q 值排序 + `zh-CN → zh` 区域回退 + RFC 4647 前缀匹配,请求 `en-US` 可命中已注册的 `en-GB`)。
- **占位符替换**:默认 `#name#` 形式,模式可自定义;缺少参数时占位符原样保留,便于及早发现。
- **并发安全**:读取路径无锁,注册与读取可安全并行,永远不会读到"写了一半"的数据。
- **明确的失败**:未知 key 默认不再返回成功码;资源文件解析失败时整体失败,不会写入半个文件。
- **不篡改调用方数据**:注册时深拷贝,注册之后调用方修改自己的 map 不会影响库内数据。
2024-06-30 12:07:53 +08:00
2026-09-20 22:20:16 +08:00
## 安装
```bash
go get github.com/yuninks/langx
```
## 快速开始
完整可运行示例见 [example/main.go](example/main.go),执行 `go run ./example` 即可查看输出。
```go
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)` 按以下优先级返回语言:
1. `langx.SetCtxLang(ctx, "en")` 显式设置的值(即使该语言没有注册也照用);
2. `context` 中的 `Accept-Language`(`string` 或 `[]string`,后者兼容 `http.Header` 风格),交给 `NegotiateLang` 与已注册语言协商;
3. 默认语言 `GetDefaultLang()`。
协商规则(`ParseAcceptLanguage` / `NegotiateLang` 也已导出,可单独使用):
- 按 `q` 值从高到低排序,`q=0` 表示明确不接受、会被丢弃,`*` 视为"任意"并回落默认语言;
- 每个候选先精确匹配,再逐级去掉区域后缀(`zh-Hans-CN → zh-hans → zh`),最后按语言前缀匹配(请求 `en-US` 可命中已注册的 `en-GB`)。
取值时的回退链同样是:指定语言 → 逐级去区域 → 语言前缀 → 默认语言(同样逐级);全都取不到时返回 key 本身,并触发 `SetMissingHandler` 回调。
## 占位符
```go
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` 语义未变 | 整体替换 / 追加覆盖的语义与旧版一致 |
## 开发
```bash
gofmt -l . # 应为空
go vet ./...
go test -race ./...
go run ./example
```
## License
尚未添加 LICENSE 文件,需要仓库作者确定许可协议后再补充。