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

187 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# langx
轻量的 Go 多语言响应库:注册响应码与多语言译文,按语言(支持区域回退)取值,并支持占位符替换。零第三方依赖,`go 1.19` 起可用。
```go
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 不会影响库内数据。
## 安装
```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 文件,需要仓库作者确定许可协议后再补充。