Skip to content

go-i18n 入门教程:用 Gin 框架实现 Go 项目多语言国际化

go-i18n 与 Gin 框架国际化教程示意图

在构建面向全球用户的 Web 应用时,国际化(i18n)与本地化(l10n)是不可或缺的一环。对于 Go 语言开发者来说,go-i18n 是一个功能强大且生态成熟的库,它支持 200+ 种语言的复数规则(基于 CLDR 数据)、模板变量 以及 多种翻译文件格式(如 JSON、TOML、YAML)。

本文将带你从零开始,深入理解 go-i18n 的核心概念、工作流,并结合 Gin 框架 完成一个完整的实战示例。

适用读者

  • 正在为 Go 项目寻找国际化方案的开发者
  • 想了解 go-i18n 库如何使用的新手
  • 需要在 Gin 框架中集成多语言支持的开发者

为什么选择 go-i18n?

  • 完整的 CLDR 支持:自动处理不同语言复杂的复数规则(如阿拉伯语有 6 种复数形式)
  • 灵活的翻译文件:可以使用 TOML、JSON 或 YAML,便于团队协作
  • 内置命令行工具goi18n 命令可以自动提取代码中的待翻译文本,并管理翻译文件的同步与合并
  • 与 Go 生态无缝集成:支持 embed.FS,将翻译文件打包进二进制,简化部署
  • 社区活跃:GitHub 3.5k Stars,持续维护更新

核心概念速览

要熟练使用这个库,先理解它的三个核心组件:

组件类比生命周期职责
Bundle一个存放所有语言翻译的大仓库应用启动时创建,全局单例加载并管理所有 active.*.toml 翻译文件
Localizer根据用户偏好从仓库取货的助手每个 HTTP 请求创建一个根据用户的语言偏好(如 Accept-Language 头),从 Bundle 中查找并返回最匹配的翻译
Message仓库中最小的货物单元定义在代码或翻译文件中代表一条可翻译的消息,支持单复数(One/Other)和模板变量(\{\{.Name\}\}\{\{.Count\}\}

💡 注意

\{\{.Count\}\} 是为了规避代码块包含 VUE 文件中的模板语法替换占位符进行的转义,请知晓!

工作流:管理翻译文件

go-i18n 最大的亮点之一是通过命令行工具 goi18n 来管理翻译文件。它的工作流非常清晰,建议在项目初期就按此规范操作。

1. 安装命令行工具

bash
go install -v github.com/nicksnyder/go-i18n/v2/goi18n@latest

2 在代码中定义待翻译的消息

在开始提取之前,我们首先需要在 Go 代码中定义好消息结构体。go-i18n 使用 i18n.Message 结构体来表示一条可翻译的消息:

go
package main

import (
    "github.com/nicksnyder/go-i18n/v2/i18n"
)

func main() {
    // 定义一条支持单复数的消息
    helloMessage := &i18n.Message{
      ID:          "HelloUser",                                           // 消息唯一标识符
      Description: "Greet user and show unread message count",            // 消息描述
      One:         "Hello, {{.Name}}! You have {{.Count}} new message.",  // 单数形式
      Other:       "Hello, {{.Name}}! You have {{.Count}} new messages.", // 复数形式
    }

    // 定义一条简单的消息(不需要单复数)
    welcomeMessage := &i18n.Message{
      ID:          "WelcomeMessage",
      Description: "Welcome message",
      Other:       "Welcome to our website!",
    }

    // ... 后续会将这些消息注册到 Bundle 中
}

关键字段说明

字段说明是否必须
ID消息的唯一标识符,用于在翻译文件中匹配和查找✅ 必须
One单数形式的翻译模板(如 Count = 1 时使用)可选
Other复数形式的翻译模板(如 Count != 1 时使用),也是必填的默认形式✅ 必须
description消息描述,帮助翻译人员理解上下文推荐

⚠️ 重要提示

  • goi18n extract 命令会扫描代码中所有 i18n.Message 结构体,自动提取 IDDescriptionOneOther 等字段
  • 提取后生成的 active.en.toml 文件中不会包含 hash 字段,只有通过 goi18n merge 基于母版生成的其它语言翻译文件才会包含 hash 字段,用于跟踪消息内容变化
  • 建议始终在代码中使用 i18n.Message 定义消息,而不是直接在手写翻译文件,这样可以保持代码和翻译的同步

3. 提取待翻译文本

在代码中定义好消息结构体后,在项目根目录运行:

bash
goi18n extract

该命令会扫描所有 .go 文件,提取 i18n.Message 结构体,并生成一个 active.en.toml 文件(默认语言为英文)。这个文件是后续所有翻译的母版

active.en.toml
toml
[HelloUser]
description = "Greet user and show unread message count"
one = "Hello, {{.Name}}! You have {{.Count}} new message."
other = "Hello, {{.Name}}! You have {{.Count}} new messages."

[WelcomeMessage]
description = "Welcome message"
other = "Welcome to our website!"

4. 添加新语言翻译

假设我们要添加中文支持:

  1. 创建一个空文件 translate.zh.toml

  2. 运行合并命令,将英文母版的内容填充进去:

bash
goi18n merge active.en.toml translate.zh.toml
  1. 打开 translate.zh.toml,将 other 的值翻译成中文:
translate.zh.toml
toml
[HelloUser]
description = "问候用户并显示未读消息数"
hash = "sha1-56ebe38b4445b9c4c445e53e95e7743a92582389"
one = "你好,{{.Name}}!你有 {{.Count}} 条新消息。"
other = "你好,{{.Name}}!你有 {{.Count}} 条新消息。"

[WelcomeMessage]
description = "欢迎语"
hash = "sha1-22f78ebcbe82934f15d046c898fbc7131b1a05dd"
other = "欢迎来到我们的网站!"
  1. 翻译完成后,将文件重命名active.zh.toml,程序最终加载的就是所有 active.*.toml 文件

5. 更新已有翻译

当代码中新增或修改了消息,只需重复执行:

bash
# 1. 更新英文母版
goi18n extract

# 2. 生成/更新各语言的待翻译文件
goi18n merge active.*.toml

# 3. 翻译 translate.*.toml 中新增的条目

# 4. 将翻译合并回 active.*.toml
goi18n merge active.*.toml translate.*.toml

最佳实践

  • 始终将 active.*.toml 视为最终翻译成果,提交到 Git
  • 翻译过程中的 translate.*.toml 文件可以不提交,或者作为临时文件使用
  • 建议在 CI/CD 流程中加入 goi18n extract 检查,确保代码和翻译文件同步

Gin 框架实战(JSON API 模式)

下面,我们通过一个完整的示例,展示如何在 Gin 项目中以 前后端分离 的方式集成 go-i18n,所有接口均返回 JSON 格式数据。

项目结构

your-project/
├── go.mod
├── main.go
├── locale/                  # 存放翻译文件
│   ├── active.en.toml
│   └── active.zh.toml
└── exchange/                # 数据传输对象
    └── response.go

第一步:定义统一响应结构

go
// exchange/response.go
package exchange

type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}

func Success(data interface{}) *Response {
    return &Response{
        Code:    0,
        Message: "success",
        Data:    data,
    }
}

func Error(message string) *Response {
    return &Response{
        Code:    500,
        Message: message,
        Data:    nil,
    }
}

第二步:初始化 Bundle 并加载翻译

main.go 中,我们使用 //go:embed 将翻译文件嵌入(构建二进制可执行文件时将自动包含在内),并初始化全局的 Bundle

go
package main

import (
	"embed"
	"log"
	"net/http"

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

//go:embed locale/*.toml
var localeFS embed.FS

var bundle *i18n.Bundle

func init() {
    // 1. 创建 Bundle,设置默认语言为英语
    bundle = i18n.NewBundle(language.English)

    // 2. 注册 TOML 解析器
    bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)

    // 3. 加载所有翻译文件
    if _, err := bundle.LoadMessageFileFS(localeFS, "locale/active.en.toml"); err != nil {
        log.Fatal("加载英文翻译失败:", err)
    }
    if _, err := bundle.LoadMessageFileFS(localeFS, "locale/active.zh.toml"); err != nil {
        log.Fatal("加载中文翻译失败:", err)
    }
}

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

    // 中间件:为每个请求创建 Localizer 并注入上下文
    r.Use(func(c *gin.Context) {
        // 优先从查询参数获取语言,如 ?lang=zh
        lang := c.Query("lang")
        if lang == "" {
            // 其次从 Accept-Language 头获取
            lang = c.GetHeader("Accept-Language")
        }
        localizer := i18n.NewLocalizer(bundle, lang)
        c.Set("localizer", localizer)
        c.Next()
    })

    // ... 路由配置
}

第三步:实现 JSON API 接口

go
func main() {
    // ... 省略上述代码

    // 首页接口:返回本地化的欢迎消息
    r.GET("/api/hello", func(c *gin.Context) {
        localizer, _ := c.Get("localizer")
        l := localizer.(*i18n.Localizer)

        // 准备消息
        message := &i18n.Message{
            ID:    "HelloUser",
            One:   "Hello, \{\{.Name\}\}! You have \{\{.Count\}\} new message.",
            Other: "Hello, \{\{.Name\}\}! You have \{\{.Count\}\} new messages.",
        }

        // 本地化
        greeting, err := l.Localize(&i18n.LocalizeConfig{
            DefaultMessage: message,
            TemplateData: map[string]interface{}{
                "Name":  c.Query("name"),
                "Count": 5,
            },
            PluralCount: 5,
        })
        if err != nil {
            c.JSON(http.StatusOK, exchange.Error("本地化失败"))
            return
        }

        c.JSON(http.StatusOK, exchange.Success(gin.H{
            "greeting": greeting,
        }))
    })

    // 批量翻译接口:一次请求获取多个翻译文本
    r.POST("/api/translate", func(c *gin.Context) {
        localizer, _ := c.Get("localizer")
        l := localizer.(*i18n.Localizer)

        var req struct {
            Keys  []string               `json:"keys"`
            Data  map[string]interface{} `json:"data,omitempty"`
        }
        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(http.StatusBadRequest, exchange.Error("无效请求参数"))
            return
        }

        result := make(map[string]string)
        for _, key := range req.Keys {
            translated, err := l.Localize(&i18n.LocalizeConfig{
                MessageID:    key,
                TemplateData: req.Data,
            })
            if err != nil {
                // 如果找不到翻译,返回 key 本身作为降级
                result[key] = key
                continue
            }
            result[key] = translated
        }

        c.JSON(http.StatusOK, exchange.Success(result))
    })

    // 获取当前语言信息接口
    r.GET("/api/locale", func(c *gin.Context) {
        localizer, _ := c.Get("localizer")
        l := localizer.(*i18n.Localizer)

        // 获取当前匹配的语言
        matchedLang := l.Language()
        c.JSON(http.StatusOK, exchange.Success(gin.H{
            "language": matchedLang.String(),
            "tag":      matchedLang.String(),
        }))
    })

    r.Run(":8080")
}

第四步:翻译文件示例

locale/active.zh.toml(中文翻译):

toml
# locale/active.zh.toml
[HelloUser]
description = "问候用户并显示未读消息数"
one = "你好,\{\{.Name\}\}!你有 \{\{.Count\}\} 条新消息。"
other = "你好,\{\{.Name\}\}!你有 \{\{.Count\}\} 条新消息。"

[WelcomeMessage]
description = "欢迎语"
other = "欢迎来到我们的网站!"

locale/active.en.toml(英文翻译):

toml
# locale/active.en.toml
[HelloUser]
description = "Greet user and show unread message count"
one = "Hello, \{\{.Name\}\}! You have \{\{.Count\}\} new message."
other = "Hello, \{\{.Name\}\}! You have \{\{.Count\}\} new messages."

[WelcomeMessage]
description = "Welcome message"
other = "Welcome to our website!"

第五步:API 测试

使用 curl 或 Postman 测试接口:

bash
# 1. 单个消息翻译(带查询参数)
curl "http://localhost:8080/api/hello?lang=zh&name=张三"
# 响应: {"code":0,"message":"success","data":{"greeting":"你好,张三!你有 5 条新消息。"}}

# 2. 英文版本
curl "http://localhost:8080/api/hello?lang=en&name=John"
# 响应: {"code":0,"message":"success","data":{"greeting":"Hello, John! You have 5 new messages."}}

# 3. 批量翻译
curl -X POST http://localhost:8080/api/translate \
  -H "Content-Type: application/json" \
  -H "Accept-Language: zh" \
  -d '{"keys": ["HelloUser", "WelcomeMessage"], "data": {"Name": "小明", "Count": 3}}'
# 响应: {"code":0,"message":"success","data":{"HelloUser":"你好,小明!你有 3 条新消息。","WelcomeMessage":"欢迎来到我们的网站!"}}

# 4. 获取当前语言信息
curl "http://localhost:8080/api/locale?lang=zh"
# 响应: {"code":0,"message":"success","data":{"language":"zh","tag":"zh"}}

第六步:前端消费示例(Vue 3)

vue
<template>
  <div class="i18n-demo">
    <h2>{{ greeting }}</h2>
    <div v-for="(value, key) in translations" :key="key">
      {{ key }}: {{ value }}
    </div>
    <p>当前语言: {{ currentLang }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import axios from 'axios';

const api = axios.create({
  baseURL: '/api',
});

// 响应式数据
const greeting = ref<string>('');
const translations = ref<Record<string, string>>({});
const currentLang = ref<string>('zh');

// 获取带翻译的问候语
async function getGreeting(name: string, lang: string) {
  const response = await api.get('/hello', {
    params: { name, lang },
  });
  greeting.value = response.data.data.greeting;
}

// 批量获取翻译
async function getTranslations(keys: string[], lang: string, data?: object) {
  const response = await api.post(
    '/translate',
    { keys, data },
    {
      headers: { 'Accept-Language': lang },
    }
  );
  translations.value = response.data.data;
}

// 获取当前语言信息
async function getLocale(lang: string) {
  const response = await api.get('/locale', {
    params: { lang },
  });
  currentLang.value = response.data.data.language;
}

// 页面加载时调用
onMounted(async () => {
  const lang = 'zh';
  await getGreeting('小明', lang);
  await getTranslations(['HelloUser', 'WelcomeMessage'], lang, {
    Name: '小明',
    Count: 3,
  });
  await getLocale(lang);
});
</script>

常见问题与实用建议

Q: 如何处理翻译文件不存在或查找失败的情况?

务必始终检查 Localize 返回的 error。一个常见的模式是使用消息的 ID 作为降级内容:

go
translated, err := l.Localize(&i18n.LocalizeConfig{MessageID: "WelcomeMessage"})
if err != nil {
    // 尝试使用默认语言,或者直接返回 ID
    translated = "WelcomeMessage"
}

Q: 如何支持 URL 参数切换语言(如 ?lang=zh)?

在中间件中,可以从查询参数获取语言偏好,并覆盖或补充 Accept-Language

go
lang := c.Query("lang")
if lang == "" {
    lang = c.GetHeader("Accept-Language")
}
localizer := i18n.NewLocalizer(bundle, lang)

Q: 性能方面有什么需要注意的吗?

  • Bundle 的加载(LoadMessageFile)是 I/O 密集型操作,务必只执行一次(在 maininit 中)
  • Localizer 是无状态的轻量对象,每个请求创建它是廉价且安全的
  • 使用 embed.FS 将文件编译进二进制,可以减少运行时文件系统的开销

Q: 如何组织大型项目的翻译文件?

对于大型项目,建议按模块拆分翻译文件:

locale/
├── active.en.toml        # 通用翻译
├── active.zh.toml
├── auth/
│   ├── active.en.toml    # 认证模块
│   └── active.zh.toml
└── payment/
    ├── active.en.toml    # 支付模块
    └── active.zh.toml

加载时循环加载所有文件:

go
files, _ := fs.Glob(localeFS, "locale/**/*.toml")
for _, f := range files {
    bundle.LoadMessageFileFS(localeFS, f)
}

Q: 如何在 Gin 的 JSON 响应中统一处理 i18n 错误?

可以封装一个辅助函数:

go
func LocalizeOrFallback(l *i18n.Localizer, messageID string, data interface{}) string {
    translated, err := l.Localize(&i18n.LocalizeConfig{
        MessageID:    messageID,
        TemplateData: data,
    })
    if err != nil {
        return messageID // 降级返回 ID
    }
    return translated
}

总结

go-i18n 为 Go 应用提供了专业、完整的国际化解决方案。通过理解 BundleLocalizerMessage 这三驾马车,并善用 goi18n 命令行工具,你可以高效地管理多语言内容。

在前后端分离的架构下,结合 Gin 框架提供 JSON API 接口,前端可以自由获取本地化的翻译数据,完美适配现代 Web 应用的开发模式。

希望这篇实战指南能帮助你快速上手,让你的 Go 应用更好地服务全球用户!



本文完整代码:文中所有代码片段均可在实际项目中直接使用,按步骤操作即可完成 go-i18n 与 Gin 框架的集成。

最后更新2026/06/30 14:04
如果你觉得这篇文章有帮助,或者想聊聊技术、工作,欢迎通过下面方式联系我:
contact fishfinal