国际化
VEF 内置了应用级 i18n 层,用于框架消息、验证输出和错误响应。
支持的语言
框架当前内置支持:
| 语言代码 |
|---|
zh-CN |
en |
默认应用语言:
| 配置项 | 值 |
|---|---|
| 默认语言 | zh-CN |
i18n.GetSupportedLanguages() 返回 supported-language slice 的副本,所以修改返回值不会改变框架内部状态。当前随包提供的 en.json 和 zh-CN.json catalogs 各包含同一组 195 个 message IDs。
公共 API
github.com/coldsmirk/vef-framework-go/i18n 与 github.com/coldsmirk/vef-framework-go/i18n/locales 的完整公开 API 是:
| API | 作用 |
|---|---|
i18n.DefaultLanguage | 默认语言常量(zh-CN) |
i18n.GetSupportedLanguages() | 返回支持的语言代码副本(zh-CN、en) |
i18n.IsLanguageSupported(code) | 判断语言代码是否受支持 |
i18n.CurrentLanguage() | 读取当前全局语言代码 |
i18n.SetLanguage(code) | 原子切换进程级全局 translator |
i18n.T(messageID, data...) | 翻译并在失败时回退到 message ID |
i18n.Te(messageID, data...) | 翻译并显式返回错误 |
i18n.New(config) | 根据 i18n.Config 创建独立 translator 实例 |
i18n.Config | 独立 translator 构造配置类型 |
i18n.Config.Locales | i18n.New(config) 使用的嵌入式 locale file set |
i18n.Translator | 独立 translator 实例实现的接口 |
i18n.Translator.T(messageID, data...) | 在独立 translator 上翻译,并在失败时回退 |
i18n.Translator.Te(messageID, data...) | 在独立 translator 上翻译,并显式返回错误 |
i18n.ErrUnsupportedLanguage | 请求语言不受支持时被包装的 sentinel |
i18n.ErrMessageIDEmpty | Te 收到空 message ID 时返回的 sentinel |
locales.EmbedLocales | 框架随包提供的嵌入式 locale file set |
绝大多数应用代码只需要 i18n.T(...)。
Translator 模型
公共抽象是 i18n.Translator:
| 方法 | 作用 |
|---|---|
T(messageID, data...) | 翻译并在失败时回退到 message ID |
Te(messageID, data...) | 翻译并返回显式错误 |
翻译行为刻意保持简单:
| 调用 | 行为 |
|---|---|
i18n.T("known_key") | 从当前全局 translator 返回本地化消息 |
i18n.T("missing_key") | 记录翻译错误,并把 "missing_key" 作为 fallback 返回 |
i18n.Te("known_key") | 返回本地化消息和 nil error |
i18n.Te("missing_key") | 返回 "" 和包装后的 localization failure |
i18n.Te("") | 返回 "" 和 i18n.ErrMessageIDEmpty |
templateData 设计成 variadic 是为了调用方便,但实现只使用第一个 map[string]any。额外的 map 会被忽略。
配置模型
构造器使用的配置类型是:
| 类型 | 作用 |
|---|---|
i18n.Config | 通过 Locales embed.FS 提供嵌入式语言文件 |
locales.EmbedLocales | 默认内置语言文件集合 |
locales.EmbedLocales 会 embed 框架随包提供的 *.json locale files,包括内置
zh-CN 和 en catalogs。
i18n.New(config) 会创建独立的 i18n.Translator。它在构造时根据 VEF_I18N_LANGUAGE 或 i18n.DefaultLanguage 选择语言,并且不会改变包级 i18n.T(...) 与 i18n.Te(...) 使用的进程级全局 translator。
i18n.Config.Locales 必须提供所有受支持的 locale 文件。以当前 supported-language list 来说,就是同时提供 zh-CN.json 和 en.json;空 embed.FS 或缺少某个受支持文件都会让 i18n.New(config) 返回 error。
错误哨兵
| 错误 | 含义 |
|---|---|
i18n.ErrUnsupportedLanguage | 请求的语言代码不受支持 |
i18n.ErrMessageIDEmpty | 翻译 message ID 为空 |
i18n.SetLanguage(...) 会包装 i18n.ErrUnsupportedLanguage,因此调用方可以用 errors.Is(err, i18n.ErrUnsupportedLanguage) 判断。i18n.Te("") 会直接返回 i18n.ErrMessageIDEmpty,所以 errors.Is(err, i18n.ErrMessageIDEmpty) 也可用。
语言选择
框架启动时按以下顺序决定语言:
| 优先级 | 来源 |
|---|---|
| 1 | VEF_I18N_LANGUAGE |
| 2 | 框架默认语言 zh-CN |
VEF_I18N_LANGUAGE 由 config 包以 config.EnvI18NLanguage 暴露;它不是 i18n 包 API。
包初始化时,全局 translator 会从 locales.EmbedLocales 创建。如果内置 catalogs 无法加载,初始化会 panic,因为框架消息在这种状态下不可用。
之后也可以通过 i18n.SetLanguage(...) 切换当前进程级 translator,这在测试里尤其有用。传入空字符串 i18n.SetLanguage("") 会重新读取 VEF_I18N_LANGUAGE,如果环境变量为空则回退到 i18n.DefaultLanguage。语言切换时会保留当前 active locale file set。
包级 i18n.T(...) 和 i18n.Te(...) 通过 atomic pointer 读取当前 translator,i18n.SetLanguage(...) 也通过同一个 pointer 发布新的 immutable translation state。因此翻译调用和语言切换并发时不会产生 data race;在切换边界上的请求自然可能观察到旧语言或新语言。
i18n 自动生效的地方
即使你从不手动调用 i18n,它也已经影响这些位置:
| 区域 | 效果 |
|---|---|
result.Ok(...) 和 result.Err(...) | 成功/错误消息自动本地化 |
| 验证输出 | 字段标签和自定义规则消息自动翻译 |
| 内置资源错误 | 框架返回值自动本地化 |
所以在 VEF 里,i18n 不只是 UI 文本问题,它本身就是后端响应契约的一部分。
最小示例
package userinfo
import (
"github.com/gofiber/fiber/v3"
"github.com/coldsmirk/vef-framework-go/i18n"
"github.com/coldsmirk/vef-framework-go/result"
"github.com/coldsmirk/vef-framework-go/security"
)
func UserInfoNotImplemented(ctx fiber.Ctx) error {
return result.ErrNotImplemented(
i18n.T(security.ErrMessageUserInfoLoaderNotImplemented),
)
}
与验证的集成
验证与 i18n 的集成主要体现在两点:
| 机制 | 效果 |
|---|---|
label_i18n | 字段标签可翻译 |
| 自定义 validator 规则翻译 | 框架自定义规则会返回本地化消息 |
这也是为什么真实项目里的请求验证错误会比默认 validator 输出更可读。
实践建议
- 普通业务代码优先使用
i18n.T(...) - 只有在你需要显式处理翻译失败时,才使用
i18n.Te(...) i18n.SetLanguage(...)更适合作为测试工具或受控运行时切换工具- 要意识到 result 消息本身已经是本地化内容,因此后端响应天然属于 i18n 表面的一部分
下一步
继续阅读 验证,看翻译后的字段标签和验证消息是如何出现在请求错误里的。