最近在忙公司项目的国际化落地方案,当然是前端、后端、客户端都得要啦,前不久我写了前端相关的 Vue3 项目 i18next 国际化落地方案,这次续上后端 golang 技术栈的(至于 iOS 和 Android 客户端部分的如果我不鸽的话估计也会有,咕咕咕)。
刚开始的时候因为前端这边使用了 i18next 所以有考虑过后端也使用相同的国际化文件格式,但是看了一下 go-i18next 发现其维护好像并不算积极(截止 2024 年 12 月),于是我将目光瞄向了 go-i18n 也对接使用了一下,整个封装的还是可以的,非常简单就能上手。不过在后面我发现 golang 团队其实有提供一个基础包 golang.org/x/text 于是我决定基于 text 包自己套一层函数直接使用。
使用 gotext 的对接流程大致如下图所示,首先需要将待翻译的部分使用 Localizer 的 printer.Sprintf 函数调用替换如 fmt.printf("你好") 需要替换为 l.printer.Sprintf("你好") 语法,方便 gotext 能够扫描到并生成出 out.gotext.json 和 catalog.go 代码文件。
安装 gotext 包,创建 i18n 多语言管理
在项目中安装 golang.org/x/text/language 和 golang.org/x/text/message 包
go get golang.org/x/text/language
go get golang.org/x/text/message
创建文件夹 i18n/languages 并创建 languages.go 代码文件,${package}
占位符替换为实际的包
package languages
import (
"golang.org/x/text/language"
"golang.org/x/text/message"
)
//go:generate gotext -srclang=en update -out=catalog.go -lang=en,zh ${package}
// 注意: 增加删除语言列表时需要同步更改上列 generate 中的 -lang 参数
var tags = []language.Tag{
language.English,
language.Chinese,
}
var matchers = language.NewMatcher(tags)
// Define a Localizer type which stores the relevant locale ID (as used
// in our URLs) and a (deliberately unexported) message.Printer instance
// for the locale.
type Localizer struct {
TAG language.Tag
printer *message.Printer
}
func GetLocalizer(tag language.Tag) *Localizer {
return &Localizer{
TAG: tag,
printer: message.NewPrinter(tag),
}
}
func GetLocalizerForID(id string) *Localizer {
tag, _ := language.MatchStrings(matchers, id)
return GetLocalizer(tag)
}
// We also add a Translate() method to the Localizer type. This acts
// as a wrapper around the unexported message.Printer's Sprintf()
// function and returns the appropriate translation for the given
// message and arguments.
func (l *Localizer) Translate(key message.Reference, args ...interface{}) string {
return l.printer.Sprintf(key, args...)
}
func (l *Localizer) T(key message.Reference, args ...interface{}) string {
return l.Translate(key, args...)
}
创建 i18n/middleware.go
代码文件,该代码文件包含整个应用程序通过 LANG
环境变量构造的 languages.Localizer 实例,以及为 gin 提供的根据 accept-language
请求头获取 languages.Localizer 实例中间件
package i18n
import (
"net/http"
"os"
"application/i18n/languages"
"github.com/gin-gonic/gin"
)
// 通过环境变量构造 localizer 实例,示例值: "en_US.UTF-8"
var L = languages.GetLocalizerForID(os.Getenv("LANG"))
var GIN_CTX_L_KEY = "localizer"
// gin 国际化 i18n 中间件
func BrowserLocalizerMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.Set(GIN_CTX_L_KEY, GetBrowserLocalizer(ctx))
}
}
// 通过 gin.Context 获取 localizer
func GetBrowserLocalizer(ctx *gin.Context) *languages.Localizer {
// 判断 gin.Context 中间件是否构造过 localizer 如果存在的话直接返回
if l, exists := ctx.Get(GIN_CTX_L_KEY); exists && l != nil {
if localizer, ok := l.(*languages.Localizer); ok && localizer != nil {
return localizer
}
}
return GetRequestLocalizer(ctx.Request)
}
// 通过 http.Request 获取 localizer
func GetRequestLocalizer(r *http.Request) *languages.Localizer {
lang := r.Header.Get("accept-language")
// fmt.Println("get accept-language", lang)
if lang == "" {
// 如果通过请求未获取到的话直接返回 cli 的 localizer 实例
return L
}
// 否则使用请求头 accept-language 指定的 localizer 实例
return languages.GetLocalizerForID(lang)
}
对接以及生成翻译文件
在 main.go 文件中引入 application/i18n/languages
,application 为你的包名
package main
import (
"fmt"
……
_ "application/i18n/languages"
)
……
在代码文件中使用 l.Translate("hello")
函数替换需要翻译的文本。常见的后端应用程序中我们需要区分两种情况
常见的用户通过浏览器发起的 http 请求中的返回值需要国际化翻译,在 gin 请求处理函数中示例
import (
"application/i18n"
_ "application/i18n/languages"
"github.com/gin-gonic/gin"
……
)
func main() {
r := gin.Default()
r.Use(i18n.BrowserLocalizerMiddleware())
r.GET("/ping", func(ctx *gin.Context) {
// gin use i18n
l := i18n.GetBrowserLocalizer(ctx)
text := l.T("Welcome!")
ctx.Writer.Write([]byte(text))
})
}
在除浏览器 http 请求上下文中提供用户可见文本翻译,如 cli 程序输出示例
import (
"application/i18n"
_ "application/i18n/languages"
……
)
func main() {
// cli use i18n
text := i18n.L.Translate("hello")
fmt.Println(text)
}
执行 go generate
生成待翻译的 out.gotext.json
文件,将指定的默认翻译文件复制为 messages.gotext.json
# 生成翻译数据
go generate i18n/languages/languages.go
# 复制翻译文件 messages.gotext.json
cp i18n/languages/locales/${language}/out.gotext.json i18n/languages/locales/${language}/messages.gotext.json
接下来就是将应用程序中需要翻译的文本都使用 l.Translate
函数封装起来了,搞定之后之后执行 go generate 重新生成 out.gotext.json 文件,注意翻译完成之后修改 messages.gotext.json 也记得执行 go generate 重新生成 catalog.go 文件否则代码中会无法读取到翻译数据的。