Files

通用国际化

本功能从 v0.26.0-preview 提供。i18n 可以单独用于 CLI、后台任务、通知或 Web 服务,不依赖 App、HTTP、错误目录、数据库或前端框架。业务文案、启用语言和用户偏好由接入项目维护。

模块没有全局当前语言、全局可变词典或自动文件扫描。词典构建后只读,支持多个实例、多个命名空间和并发调用。完整可运行示例见 examples/i18n,生成契约见 i18n.api。

独立使用

bundle, err := i18n.New(i18n.Config{
    SourceLocale: "en", DefaultLocale: "en",
    Locales: []string{"en", "zh-Hans"},
},
    i18n.Document{Version: 1, Namespace: "common", Locale: "en",
        Messages: map[string]i18n.Message{
            "greeting": {
                Params: map[string]i18n.Kind{"name": i18n.String},
                Value: i18n.Pattern{Text: "Hello, {name}"},
            },
        }},
    i18n.Document{Version: 1, Namespace: "common", Locale: "zh-Hans",
        Messages: map[string]i18n.Message{
            "greeting": {Value: i18n.Pattern{Text: "你好,{name}"}},
        }},
)
if err != nil { return err }
result, err := bundle.Render("zh-CN", i18n.Ref{
    Namespace: "common", Key: "greeting",
}, map[string]any{"name": "Alex"})
// result.Text == "你好,Alex"; result.Locale == "zh-Hans"

Render 显式接受语言。也可以用 WithLocale(ctx, locale) 和 RenderContext,语言只属于该上下文。任务消费者需由项目显式取得接收者语言;Go context 不会自动跨进程或写入队列。

Result 提供文本、匹配后的请求语言、实际词典语言和是否发生回退。缺键、缺参、类型错误、渲染限额返回 *Diagnostic;核心不决定 HTTP 状态,也不把任意错误自动转换成业务错误。诊断没有参数值、内部原因或原始文案。

语言选择与回退

  • SourceLocale 是定义消息和参数的语言,可以只用作最终回退而不对用户开放。
  • DefaultLocale 是无合适偏好时的选择,必须在 Locales 中。
  • Locales 是有序的启用语言。Match 使用 x/text/language 的 BCP 47 匹配,返回真实配置的词典标签;不会用匹配器生成的带扩展标签直接索引文件。
  • Fallbacks 是显式、有序、无环的关系。先查选中语言,再深度优先查配置的回退;然后查默认语言及其回退,最后查源语言,过程中去重。不会从文件名或地域字符串猜一条词典继承链。
  • 非法请求偏好忽略并使用可用偏好或默认语言;非法配置、重复规范化语言、回退循环在 New 时失败。

运行时允许部分语言缺词,Check 单独列出各启用语言缺少的直接翻译。CI 可因此失败,在线请求仍使用回退。存在但格式损坏的译文在构建时拒绝;替换词典应先构建新实例,成功后由项目替换实例。

文案资源 v1

{
  "version": 1,
  "namespace": "notifications",
  "locale": "en",
  "messages": {
    "unread": {
      "params": {"count": "integer"},
      "value": {
        "plural": "count",
        "cases": {
          "=0": {"text": "No unread messages"},
          "one": {"text": "{count} unread message"},
          "other": {"text": "{count} unread messages"}
        }
      }
    }
  }
}

同一命名空间的同一键只有一份源定义。译文省略 params 时继承源定义;重复声明时必须完全一致。参数是消息级契约:各语言使用相同参数集合,分支可以不使用其中某个参数,但整条消息的所有分支合起来必须使用全部参数。

value 支持:

  • text:命名插值 {name},{{、}} 表示字面花括号;空字符串是合法译文。
  • literal: true:文本原样保留,适用于已有固定消息。插入参数不会再次解析,也不会自动查词典。
  • select:根据 string/boolean 参数选择命名分支,必须有 other。
  • plural:根据 integer/number 参数选择基数复数分支,必须有 other;先匹配 =0 等精确 int64 分支,再匹配 zero/one/two/few/many/other。可与 select 嵌套。

复数规则固定为 Unicode CLDR JSON 46.1.0。Go 和浏览器使用同一份规则,导出包携带实际语言及回退语言的规则,避免浏览器自带规则版本不同。数据来源和许可见 cldr。规则版本与文案格式版本分别记录。

参数类型:

类型 Go 调用 浏览器调用
string 字符串 string
boolean bool boolean
integer int/int32/int64 等整数,或精确的 json.Number;范围为 int64 安全整数 number、bigint 或十进制整数字符串;超范围拒绝
number 有限 float32/float64 或 json.Number 有限 number

Number 按规范化数值处理:1.0 与 1 等价,负零显示为 0,不保留输入小数位数。整数不会经过 float64;不接受任意对象、Stringer、nil、自动字符串转数值。日期、货币、精度敏感的小数和字段标签可由项目显式格式化为字符串参数,时区与币种不会从语言推断。

v1 的范围是普通文本、命名参数、select 和基数复数,不宣称兼容完整 ICU MessageFormat;不含 ordinal、紧凑数字格式、HTML 执行或任意表达式。复数计算使用普通数值,CLDR 的 compact-number c/e 操作数为 0。字段文案可以先显式 Render 后再作为参数;用户输入和业务数据保持原样。

加载、覆盖与资源限制

LoadFS(fs.FS, paths...) 读取明确列出的 JSON 文件,支持 embed.FS 和 os.DirFS。DecodeJSON 可用于业务自己取得的内存数据,拒绝重复键、未知字段、无效 UTF-8、超深结构。它们不展开 ${...},不执行项目代码,不自动拉远程词典。文件系统实际访问范围由传入的 FS 决定。

项目已有配置加载器可加载 i18n.Config 的语言设置,词典正文用本包加载;避免配置加载器把词典里的 ${...} 解释成环境变量。数据库或远程词典由项目读取为 Document,再调用 New。

同一语言、命名空间可拆成多个文件;重复键默认报错。Overlay(base, override) 明确替换完整消息并返回独立副本,再传给 New 校验;不隐式合并分支或参数。项目决定覆盖的先后顺序。

主要限额:每个 JSON 文件 1 MiB、LoadFS 总量 4 MiB、最多 1024 个文档、64 个资源语言、128 个命名空间、10000 条消息、每条 64 个参数、单文本/字符串参数 64 KiB、模式深度 24、渲染结果 1 MiB。导出的单语言包最多 4 MiB,可按命名空间拆分。

Config()、Documents() 和 Schema() 返回独立副本;Config.Validate() 可预检语言配置。运行时没有修改词典的方法。

接入 go-omni 错误与 HTTP

// 项目自己的 Catalog,既有 status/code/message/params 继续维护在这里。
catalog = errori18n.Bind(catalog, "accountErrors")
sourceDocuments, err := errori18n.Source(catalog, "en")
// 把 sourceDocuments 与项目加载的译文一起传入 i18n.New。
// Bind 不修改输入;已有显式 translation 引用保持。

languagePolicy, err := httpi18n.New(bundle)
localizer := errori18n.New(bundle, nil) // 第二参数可提供并发安全的诊断回调。
if err := localizer.Validate(catalog); err != nil { return err }

app := omni.New(
    omni.WithErrors(catalog),
    omni.WithLanguagePolicy(languagePolicy),
    omni.WithErrorLocalizer(localizer),
    // 继续传入项目自己的 WithResponse / WithResponseEncoder。
)

也可在单项 Definition 或 .api errorCatalog 条目明确声明:

"translation": {"namespace":"accountErrors", "key":"username.invalid"}

翻译引用与逻辑错误标识、对外业务码分别独立。不同目录可以为同名错误配置不同引用;多个错误引用同一源消息时,Source 会拒绝不一致的源文案或参数。Go 源目录通过 Source 导出,保留原固定文案的字面花括号,不包含状态、业务码、example 或 cause。

HTTP 策略在应用中间件之前选语言,默认读取有界 Accept-Language,保留请求上下文中显式设置的语言优先级。httpi18n.WithResolver 可替换语言来源,并显式声明缓存 Vary 所需的头;无头可声明的个性化来源自动标记 private。HTTP 策略不会读取用户数据库、JWT 或 Cookie 中的业务字段,也不自动设置可能与逐词回退不符的 Content-Language。独立标准 HTTP 可以用 Policy.Wrap。

自定义 Resolver panic 时按无偏好处理,继续使用显式上下文语言或默认语言,避免偏好查询故障中断业务请求。项目可在自己的 Resolver 中记录诊断。

错误适配器先使用原有规则解析业务错误,只替换展示文案;缺资源、适配失败保持原文,参数调用错误仍遵循原有 INTERNAL_ERROR 处理。没有 translation 引用的条目保持原行为。标准响应的平铺字段、嵌套 ${message}、RawJSON 错误、旧 Messages 校验、路由独立目录、认证提前返回和缓冲克隆均保留原语义。

response.Result.Message 继续是契约原文;新增 MessageOverride 和 PublicMessage()。内置 Formatter 使用最终公开文案。自定义 Encoder 需要翻译时显式使用 PublicMessage();固定回执继续由 Encoder 决定。c.JSON/Text/XML/Bytes 等直接输出不自动重写。

失败快照保存当次展示文案。幂等重放保留首次响应和失败元数据,不重新翻译,不把语言加入业务幂等键。Localizer.Localize(ctx, registry, resolvedError) 也可供非 HTTP 入口使用;返回值只包含文本和是否应用,不修改传入错误。

生成器识别 translation 元数据并写入 Catalog、路由目录和 OpenAPI;已有无 translation 的契约不变。新 metadata 的远程 runtime 下限为 v0.26.0-preview,CLI 和框架使用同版;开发框架时可以显式使用本地 checkout。生成到不具备能力的旧依赖会在写入前失败。

CLI 提取、检查与导出

下面的命令在框架根目录可以直接运行;安装后的 CLI 把 go run ./cmd/go-omni 换成 go-omni。

go run ./cmd/go-omni i18n check --config examples/i18n/config.json \
  --resource examples/i18n/locales/ui.en.json \
  --resource examples/i18n/locales/ui.zh-Hans.json

go run ./cmd/go-omni i18n export --config examples/i18n/config.json \
  --resource examples/i18n/locales/ui.en.json \
  --resource examples/i18n/locales/ui.zh-Hans.json \
  --locale zh-Hans --namespace ui

go run ./cmd/go-omni i18n types --config examples/i18n/config.json \
  --resource examples/i18n/locales/ui.en.json \
  --resource examples/i18n/locales/ui.zh-Hans.json --namespace ui

go run ./cmd/go-omni i18n extract --go examples/i18n/main.go
  • check 检查资源和直接翻译遗漏,非空诊断退出非零;可重复提供 --go 检查静态引用。
  • extract --go 识别 Go 中 i18n.Ref 的字符串字面量及 import 别名;动态引用、非字面量 Render 调用和 dot import 会报告,不能声称完成任意 Go/JS/模板的静态分析。提取命令输出发现结果,check 把未解决动态引用视为诊断。
  • extract --catalog exported-errors.json --namespace errors --locale en 读取已有错误目录导出物,生成单个源语言 Document,不执行项目 Go。目录可以由已有 workspace Go Catalog 导出流程产生。
  • export 显式选择命名空间,输出一个选中语言及全部回退资源的 Pack,不输出业务错误元数据。可重复 --namespace。
  • types 输出按 namespace/key 分层的 TypeScript 参数类型及 Translate 类型,不强制 Go 命名风格,不把 int64 简单降成 number。

这些命令只读输入并输出 stdout,没有自动覆盖业务文件、扫描其他项目、翻译服务调用或安装前端依赖。Bundle.Export、TypeScript 提供相同的库入口。

前端

见 browser。运行时是普通 ES module,带 TypeScript 声明,可按项目需要复制进前端或通过 browseri18n.Files() 提供静态文件。语言包按需加载、先验证再提交;连续切换只有最新选择能生效,失败保留已提交语言。没有 DOM、localStorage、表单或业务请求的隐式操作。

升级本模块时必须一起验证资源格式、规则版本、前后端一致性、错误兼容和生成消费者。现有项目不启用这两个适配选项时,响应与错误行为保持原样。