多语言翻译

This commit is contained in:
yun
2026-09-20 22:20:16 +08:00
parent 4edb2d244e
commit 287fa9ed36
20 changed files with 1616 additions and 320 deletions
+99 -29
View File
@@ -1,54 +1,124 @@
package langx
import (
"fmt"
"strings"
)
// options 是库的全局配置。
//
// 它是值类型, 一旦随快照发布就不再被修改: 任何配置变更都必须构造一份新的 options
// 并通过 update/updateErr 原子替换, 这样读路径可以完全不加锁, 也不会与写入产生竞态。
type options struct {
// defaultCode 是未知 key 返回的响应码。
// 默认 -1, 表示"未定义", 避免 key 拼写错误时静默返回成功码。
defaultCode int
// defaultLang 是默认语言(已归一化), 兜底时使用。
defaultLang string
replaceKey string
// ctxLangKey string
// replaceKey 是占位符模式, 形如 "#%s#"。
replaceKey string
// replacePrefix/replaceSuffix 由 replaceKey 推导, 避免每次替换都做一次 Sprintf。
replacePrefix string
replaceSuffix string
// missing 是译文缺失时的回调, 可为 nil。
missing func(lang, key string)
}
func defaultOptions() *options {
return &options{
defaultCode: 200,
func defaultOptions() options {
o := options{
defaultCode: -1,
defaultLang: "zh",
replaceKey: "#%s#",
// ctxLangKey: "language",
}
o.derive()
return o
}
// 设置
func InitLangx(ops ...Option) {
for _, opt := range ops {
opt(l.ops)
// derive 由 replaceKey 推导出占位符的前缀与后缀
func (o *options) derive() {
const probe = "\x00langx\x00"
out := fmt.Sprintf(o.replaceKey, probe)
i := strings.Index(out, probe)
if i < 0 {
o.replacePrefix, o.replaceSuffix = "", ""
return
}
o.replacePrefix = out[:i]
o.replaceSuffix = out[i+len(probe):]
}
func (o options) validate() error {
if strings.TrimSpace(o.defaultLang) == "" {
return fmt.Errorf("langx: 默认语言不能为空")
}
return validateReplaceKey(o.replaceKey)
}
// validateReplaceKey 要求模式中有且只有一个 %s
func validateReplaceKey(pattern string) error {
if pattern == "" {
return fmt.Errorf("langx: replaceKey 不能为空, 形如 %q", "#%s#")
}
out := fmt.Sprintf(pattern, "x")
if strings.Contains(out, "%!") || !strings.Contains(out, "x") {
return fmt.Errorf("langx: replaceKey=%q 非法, 需要且只需要一个 %%s 占位符, 例如 %q", pattern, "#%s#")
}
return nil
}
// Option 是配置项, 通过 InitLangx 应用。
type Option func(*options)
// 设置默认的code
// InitLangx 应用配置项, 可重复调用, 后一次在前一次的配置上叠加。
//
// 与旧版本的区别: 配置非法时返回错误, 且本次传入的所有 Option 都不会生效(不会写入一半)。
// 不需要错误处理时可以直接当语句使用: langx.InitLangx(...)
func InitLangx(ops ...Option) error {
return updateErr(func(old *state) (*state, error) {
next := old.ops // 值拷贝, 不影响已经发布的配置
for _, opt := range ops {
if opt == nil {
continue
}
opt(&next)
}
if err := next.validate(); err != nil {
return nil, err
}
next.derive()
return setOptions(old, next), nil
})
}
// Reset 清空所有已注册的响应码与译文, 并恢复默认配置。便于测试与重新初始化。
func Reset() {
update(func(*state) *state { return newState() })
}
// SetDefaultCode 设置未知 key 返回的响应码, 默认 -1。
//
// 如果 code 会被直接当作 HTTP 状态码使用, 建议设置为 500, 让缺失的 key 以失败收场。
func SetDefaultCode(code int) Option {
return func(o *options) {
o.defaultCode = code
}
return func(o *options) { o.defaultCode = code }
}
// 从ctx里面获取语言的key
// func SetCtxLangKey(key string) Option {
// return func(o *options) {
// o.ctxLangKey = key
// }
// }
// 默认语言
// SetDefaultLanguage 设置默认(兜底)语言, 默认 "zh"。
func SetDefaultLanguage(lang string) Option {
return func(o *options) {
o.defaultLang = lang
}
return func(o *options) { o.defaultLang = normalizeLang(lang) }
}
// 替换规则 %s 为占位符 需要填进去
// SetReplaceKey 设置占位符模式, 必须包含且只包含一个 %s, 默认 "#%s#"(即 #name#)。
func SetReplaceKey(key string) Option {
return func(o *options) {
o.replaceKey = key
}
return func(o *options) { o.replaceKey = key }
}
// SetMissingHandler 设置译文缺失时的回调, 便于记录漏翻的 key。传 nil 表示取消回调。
//
// 回调在读取路径上同步执行, 且不在任何锁内, 因此可以安全地再次调用本库的函数。
func SetMissingHandler(fn func(lang, key string)) Option {
return func(o *options) { o.missing = fn }
}