你的 API 只会说中文?Gin i18n 国际化极简指南

Go 实战

Gin 框架轻松集成 i18n 国际化

使用 go-i18n + gin-contrib/i18n + embed.FS, 十分钟为你的 API 添加完整的多语言支持。

最终效果
bash
# 不传语言头 → 默认中文
$ curl http://localhost:8080/ping
{"message": "欢迎使用"}

# 传 x-lang: en → 英文
$ curl -H "x-lang: en" http://localhost:8080/ping
{"message": "Welcome"}

# 错误场景同样国际化
$ curl -H "x-lang: en" http://localhost:8080/login
{"code": 400, "message": "Invalid email format"}

1. 安装依赖

bash
go get github.com/gin-contrib/i18n
go get github.com/nicksnyder/go-i18n/v2
go get github.com/pelletier/go-toml/v2
gin-contrib/i18n
Gin 中间件封装
go-i18n/v2
核心国际化引擎
go-toml/v2
TOML 翻译文件解析

2. 目录结构

text
project/
├── main.go
└── i18n/
    ├── i18n.go         // 中间件 + 消息获取
    ├── keys.go        // 翻译 Key 常量
    └── locales/
        ├── zh.toml    // 中文(默认)
        └── en.toml    // 英文

3. 编写翻译文件

翻译文件使用 TOML 格式,比 JSON 更可读,支持注释。每条消息以 [messageID] 定义,other 为默认复数形式。

locales/zh.toml
# 通用
[welcome]
other = "欢迎使用"

[paramError]
other = "参数不合法"

[notFound]
other = "资源不存在"

# 认证相关
[invalidEmail]
other = "邮箱格式不正确"

[loginSuccess]
other = "登录成功"

# 带参数的模板消息
[greeting]
other = "你好,{{.Name}}!"
locales/en.toml
# Common
[welcome]
other = "Welcome"

[paramError]
other = "Invalid parameters"

[notFound]
other = "Resource not found"

# Auth
[invalidEmail]
other = "Invalid email format"

[loginSuccess]
other = "Login successful"

# Template message with params
[greeting]
other = "Hello, {{.Name}}!"

4. 定义 Key 常量

将翻译 Key 定义为 Go 常量,避免硬编码字符串,拼写错误在编译期即可发现。

Go
// i18n/keys.go
package i18n

const (
    Welcome      = "welcome"
    ParamError   = "paramError"
    NotFound     = "notFound"
    InvalidEmail = "invalidEmail"
    LoginSuccess = "loginSuccess"
    Greeting     = "greeting"
)
为什么用常量? — 当你写 i18n.InvalidEmail 拼错时编译器直接报错,而字符串 "invaldEmail" 只会在运行时默默返回 Key 本身。

5. 核心代码实现

i18n/i18n.go — 中间件 + 工具函数
Go
package i18n

import (
    "embed"
    "path/filepath"

    "github.com/gin-contrib/i18n"
    "github.com/gin-gonic/gin"
    goi18n "github.com/nicksnyder/go-i18n/v2/i18n"
    "github.com/pelletier/go-toml/v2"
    "golang.org/x/text/language"
)

// 编译时嵌入 locales 目录,部署时无需携带额外文件
//go:embed locales
var locales embed.FS

// langLoader 适配 embed.FS 到 i18n Loader 接口
type langLoader struct{}

func (l langLoader) LoadMessage(path string) ([]byte, error) {
    return locales.ReadFile(filepath.Join("locales", path))
}

// Middleware 返回 Gin i18n 中间件
func Middleware() gin.HandlerFunc {
    return i18n.Localize(
        i18n.WithBundle(&i18n.BundleCfg{
            AcceptLanguage:   []language.Tag{language.Chinese, language.English},
            DefaultLanguage:  language.Chinese,
            FormatBundleFile: "toml",
            UnmarshalFunc:    toml.Unmarshal,
            Loader:           &langLoader{},
        }),
        // 自定义语言检测:从 x-lang 请求头读取
        i18n.WithGetLngHandle(func(c *gin.Context, defaultLng string) string {
            if lang := c.GetHeader("x-lang"); lang != "" {
                return lang
            }
            return defaultLng
        }),
    )
}

// GetMsg 获取当前请求语言的翻译文本
func GetMsg(ctx *gin.Context, msgId string) string {
    msg, err := i18n.GetMessage(ctx, msgId)
    if err != nil {
        return msgId // 降级:返回 Key 本身
    }
    return msg
}

// GetMsgWithParam 获取带参数的翻译文本
func GetMsgWithParam(ctx *gin.Context, msgId string, data map[string]any) string {
    msg, err := i18n.GetMessage(ctx, goi18n.LocalizeConfig{
        MessageID:    msgId,
        TemplateData: data,
    })
    if err != nil {
        return msgId
    }
    return msg
}

6. 注册中间件 & 业务调用

Go
// main.go
package main

import (
    "net/http"
    "your-project/i18n"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    // 注册 i18n 中间件(放在最前面)
    r.Use(i18n.Middleware())

    // 简单消息
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": i18n.GetMsg(c, i18n.Welcome),
        })
    })

    // 带参数的模板消息
    r.GET("/hello", func(c *gin.Context) {
        name := c.Query("name")
        c.JSON(http.StatusOK, gin.H{
            "message": i18n.GetMsgWithParam(c, i18n.Greeting, map[string]any{
                "Name": name,
            }),
        })
    })

    // 错误响应国际化
    r.POST("/login", func(c *gin.Context) {
        email := c.PostForm("email")
        if !isValidEmail(email) {
            c.JSON(http.StatusBadRequest, gin.H{
                "code":    400,
                "message": i18n.GetMsg(c, i18n.InvalidEmail),
            })
            return
        }
        // ...
    })

    r.Run(":8080")
}

7. 请求处理流程

1
客户端发起请求

携带 x-lang: en Header(可选,缺省为中文)

2
i18n 中间件拦截

解析 x-lang 值,匹配最接近的语言标签,创建 Localizer 存入 Context

3
Handler / Service 调用 GetMsg

从 Context 中取出 Localizer,查找对应语言的 TOML 翻译条目

4
返回本地化响应

x-lang: en"Invalid email format"  |  x-lang: zh"邮箱格式不正确"

8. 设计要点

方案亮点
  • embed.FS 嵌入 — 翻译文件编译进二进制,部署零外部文件依赖
  • 常量化 Key — 编译期发现拼写错误,IDE 自动补全友好
  • 请求级隔离 — 每个请求独立语言上下文,天然并发安全
  • 优雅降级 — 翻译缺失返回 Key 本身,服务不会崩溃
  • TOML 格式 — 支持注释,比 JSON 更适合翻译人员维护
  • 模板消息 — 内置 Go template 语法,支持动态参数
实践建议
  • 中间件放在 第一个 注册,确保后续中间件也能使用 i18n
  • Key 命名用 camelCase,与 Go 常量风格保持一致
  • 按模块分组 Key 常量(auth、user、order…),文件大了可拆分
  • 新增 Key 时同步更新所有 .toml 文件,避免部分语言缺失翻译
  • Header 名选用自定义的 x-lang,避免解析复杂的 Accept-Language
  • 需要新语言?只需添加 .toml 文件 + AcceptLanguage 列表即可

9. 扩展新语言只需两步

1 新增翻译文件
toml
# locales/ja.toml
[welcome]
other = "ようこそ"

[paramError]
other = "パラメータが不正です"

# ... 其他条目
2 注册语言标签
Go
AcceptLanguage: []language.Tag{
    language.Chinese,
    language.English,
    language.Japanese, // ← 加上这行
},

无需修改任何业务代码,embed.FS 会自动包含新文件,客户端传 x-lang: ja 即可生效。

总结
三个文件
i18n.go + keys.go + .toml 翻译文件,全部代码不到 60 行
零运行时依赖
embed.FS 将翻译编译进二进制,单文件部署即可
零侵入接入
一行 r.Use(i18n.Middleware()) 即可,业务代码改动极小