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

8.4 KiB
Raw Blame History

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) 按以下优先级返回语言:

  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 回调。

占位符

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 文件,需要仓库作者确定许可协议后再补充。