English
go-i18n Tutorial: Using the Gin Framework to Implement Multilingual Internationalization in a Go Project

When building web applications for global users, internationalization (i18n) and localization (l10n) are indispensable. For Go language developers,go-i18n is a powerful library with a mature ecosystem, and it supports Plural rules for 200+ languages(Based on CLDR data),Template variables as well as Multiple translation file formats(such as JSON, TOML, YAML).
This article will take you from scratch to gain an in-depth understanding of go-i18n core concepts, workflow, and combined with Gin framework Complete a full hands-on example.
Intended audience
Developers looking for internationalization solutions for Go projects
Want to know
go-i18nBeginner's guide to using the libraryDevelopers who need to integrate multilingual support into the Gin framework
Why choose go-i18n?
Full CLDR support: Automatically handle complex plural rules in different languages (e.g., Arabic has 6 plural forms)
Flexible translation file: You can use TOML, JSON, or YAML, which facilitates team collaboration
Built-in command-line tool:
goi18nThe command can automatically extract the text to be translated from the code and manage the synchronization and merging of translation files.Seamless integration with the Go ecosystem: Support
embed.FSBundle translation files into the binary to simplify deployment.Active community: GitHub 3.5k Stars, continuously maintained and updated
Core Concepts at a Glance
To use this library proficiently, first understand its three core components:
| component | Analogy | Lifecycle | Responsibilities |
|---|---|---|---|
Bundle | a repository storing translations of all languages Large warehouse | Created at application startup, global singleton | Load and manage all active.*.toml Translate the file |
Localizer | picking goods from the warehouse according to user preferences Assistant | Each HTTP request creates one | According to the user's language preference (such as Accept-Language head), from Bundle Search for and return the best matching translation. |
Message | the smallest in the warehouse Cargo unit | Defined in code or translation files | Represents a translatable message, supports singular and plural (One/Other) and template variables (\{\{.Name\}\} and \{\{.Count\}\}) |
💡 Note
\{\{.Count\}\} This is to avoid the escaping performed for replacing placeholders in template syntax in VUE files contained in code blocks. Please be aware!
Workflow: Manage Translation Files
go-i18n One of the biggest highlights is through command-line tools goi18n to manage translation files. Its workflow is very clear, and it is recommended to follow this specification from the early stage of the project.
1. Install command-line tools
bash
go install -v github.com/nicksnyder/go-i18n/v2/goi18n@latest2 Define the message to be translated in the code
Before starting extraction, we first need to define the message struct in the Go code.go-i18n Use i18n.Message Use a struct to represent a translatable message:
go
package main
import (
"github.com/nicksnyder/go-i18n/v2/i18n"
)
func main() {
// Define a message that supports singular and plural forms.
notificationMessage := &i18n.Message{
ID: "NotificationMessage", // Message unique identifier
Description: "Greet user and show unread message count", // Message Description
One: "Hello, {{.Name}}! You have {{.Count}} new message.", // singular form
Other: "Hello, {{.Name}}! You have {{.Count}} new messages.", // Plural form
}
// Define a simple message (singular/plural not required)
welcomeMessage := &i18n.Message{
ID: "WelcomeMessage",
Description: "Welcome message",
Other: "Welcome to our website!",
}
// ... Subsequently, these messages will be registered into the Bundle
}Key Field Description:
| field | Description | Is it necessary? |
|---|---|---|
ID | The unique identifier of the message, used for matching and lookup in the translation file. | ✅ Must |
One | Translation template in singular form (e.g.Count = 1 when using) | Optional |
Other | Translation template in plural form (e.g.Count != 1 used when), and is also Required default form | ✅ Must |
description | Message description to help translators understand the context | Recommend |
⚠️ Important Notice:
goi18n extractThe command will scan all in the codei18n.MessageStruct, automatic extractionID,Description,One,Otherfields such asGenerated after extraction
active.en.tomlwill not be included in the filehashField: only translation files for other languages generated from the master template via goi18n merge will contain it.hashField used to track changes in message contentIt is recommended to always use in the code
i18n.MessageDefine message, rather than directly hand-writing the translation files, this keeps the code and translations in sync
3. Extract the text to be translated
After defining the message struct in the code, run the following in the project root directory:
bash
goi18n extractThis command will scan all .go File, extract i18n.Message struct, and generate a active.en.toml File (default language is English). This file is for all subsequent translations.Master.
toml
[NotificationMessage]
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. Add new language translation
Suppose we want to add Chinese support:
Create an empty file
translate.zh.tomlRun the merge command to populate the content from the English master template:
bash
goi18n merge active.en.toml translate.zh.toml- Open
translate.zh.toml, willotherThe value of translated into Chinese:
toml
[NotificationMessage]
description = "Greets the user and displays the number of unread messages"
hash = "sha1-56ebe38b4445b9c4c445e53e95e7743a92582389"
one = "Hello, {{.Name}}! You have {{.Count}} new messages."
other = "Hello, {{.Name}}! You have {{.Count}} new messages."
[WelcomeMessage]
description = "Welcome message"
hash = "sha1-22f78ebcbe82934f15d046c898fbc7131b1a05dd"
other = "Welcome to our website!"- After the translation is completed, the file Rename for
active.zh.toml, what the program ultimately loads is allactive.*.tomlDocument
5. Update existing translation
When messages are added or modified in the code, simply repeat:
bash
# 1. Update the English master
goi18n extract
# 2. Generate/update the files to be translated for each language.
goi18n merge active.*.toml
# 3. Translate the newly added entries in translate.*.toml
# 4. Merge the translations back into active.*.toml
goi18n merge active.*.toml translate.*.tomlBest Practices
always take
active.*.tomlTreat it as the final translation result and submit it to Git.during the translation process
translate.*.tomlThe file may not be submitted, or it may be used as a temporary file.It is recommended to add to the CI/CD process
goi18n extractCheck to ensure the code and translation files are in sync.
Gin Framework in Practice (JSON API Mode)
Next, we use a complete example to show how to, in a Gin project, use Frontend-backend separation integrated in the manner of go-i18n, all interfaces return data in JSON format.
Project Structure
your-project/
├── go.mod
├── main.go
├── locale/ # Store translation files
│ ├── active.en.toml
│ └── active.zh.toml
└── exchange/ # Data Transfer Object
└── response.goStep 1: Define a unified response structure
go
package exchange
// Response defines the unified API response format
type Response struct {
Message string `json:"message"` // Response message, e.g. "OK"
Code int `json:"code"` // Business status code, e.g. 100100 for success
Data any `json:"data,omitempty"` // Response data, returned only when needed
}
const (
CodeWithSuccess = 100100
CodeWithFailed = 100101
)
func Success(data interface{}) *Response {
return &Response{
Code: CodeWithSuccess,
Message: "success",
Data: data,
}
}
func Error(message string) *Response {
return &Response{
Code: CodeWithFailed,
Message: message,
}
}Step 2: Initialize the Bundle and load translations
At main.go In it, we use //go:embed Embed the translation files (they will be automatically included when building the binary executable), and initialize the global 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. Create a Bundle and set the default language to English.
bundle = i18n.NewBundle(language.English)
// 2. Register the TOML parser
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
// 3. Load all translation files
if _, err := bundle.LoadMessageFileFS(localeFS, "locale/active.en.toml"); err != nil {
log.Fatal("Failed to load English translation:", err)
}
if _, err := bundle.LoadMessageFileFS(localeFS, "locale/active.zh.toml"); err != nil {
log.Fatal("Failed to load Chinese translation:", err)
}
}
func main() {
r := gin.Default()
// Middleware: create a Localizer for each request and inject it into the context
r.Use(func(ctx *gin.Context) {
lang := ctx.Query("lang")
if lang == "" {
lang = ctx.GetHeader("Accept-Language")
}
localizer := i18n.NewLocalizer(bundle, lang)
ctx.Set("localizer", localizer)
ctx.Set("language", lang)
ctx.Next()
})
// ... routing configuration
}Step 3: Implement the JSON API interface
go
func main() {
// ... The above code is omitted.
// Homepage API: returns a localized welcome message
r.GET("/api", func(ctx *gin.Context) {
localizer, _ := ctx.Get("localizer")
l := localizer.(*i18n.Localizer)
// Prepare the message
welcomeMessage := &i18n.Message{
ID: "WelcomeMessage",
Description: "Welcome message",
Other: "Welcome to our website!",
}
// Localization
welcome, err := l.Localize(&i18n.LocalizeConfig{
MessageID: "WelcomeMessage",
DefaultMessage: welcomeMessage,
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, exchange.Error("Localization failed"))
return
}
ctx.JSON(http.StatusOK, exchange.Success(welcome))
})
// Message list API: returns a localized unread message notification.
r.GET("/api/notifications", func(ctx *gin.Context) {
localizer, _ := ctx.Get("localizer")
l := localizer.(*i18n.Localizer)
// Prepare the message
notificationMessage := &i18n.Message{
ID: "NotificationMessage", // Message unique identifier
Description: "Greet user and show unread message count", // Message Description
One: "Hello, {{.Name}}! You have {{.Count}} new message.", // singular form
Other: "Hello, {{.Name}}! You have {{.Count}} new messages.", // Plural form
}
// Localization
greeting, err := l.Localize(&i18n.LocalizeConfig{
MessageID: "NotificationMessage",
DefaultMessage: notificationMessage,
TemplateData: map[string]interface{}{
"Name": ctx.Query("name"),
"Count": 5,
},
// PluralCount determines which plural form of the message is used.
PluralCount: 5,
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, exchange.Error("Localization failed"))
return
}
ctx.JSON(http.StatusOK, exchange.Success(greeting))
})
// Batch translation API: get multiple translated texts in one request
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("Invalid request parameter"))
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 {
// If no translation is found, return the key itself as a fallback.
result[key] = key
continue
}
result[key] = translated
}
c.JSON(http.StatusOK, exchange.Success(result))
})
// Get current language information API
r.GET("/api/locale", func(ctx *gin.Context) {
lang, _ := ctx.Get("language")
// Get the current matching language
ctx.JSON(http.StatusOK, exchange.Success(gin.H{
"language": lang,
"tag": lang,
}))
})
r.Run(":8080")
}Step 4: Translation file example
toml
[NotificationMessage]
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!"toml
[NotificationMessage]
description = "Greet user and show unread message count"
hash = "sha1-56ebe38b4445b9c4c445e53e95e7743a92582389"
other = "Hello, {{.Name}}! You have {{.Count}} new messages."
[WelcomeMessage]
description = "Welcome message"
hash = "sha1-22f78ebcbe82934f15d046c898fbc7131b1a05dd"
other = "Welcome to our website!"Step 5: API Testing
Use curl Or test the API with Postman:
5.1 Homepage Welcome
bash
curl "http://localhost:8080/api?lang=en-US"
# response: {"code":100100,"message":"success","data":"Welcome to our website!"}bash
curl "http://localhost:8080/api?lang=zh"
# response: {"code":100100,"message":"success","data":"Welcome to our website!"}💡 Note
langParameter values comply with the BCP 47 language tag specification.English usage
en-US(regional variant), Chinese usagezh(General) orzh-CN/zh-TW(regional variant)Please ensure that the backend service supports the language code you pass in; otherwise, it may fall back to the default language.
It is recommended to consistently use lowercase letters for languages and uppercase letters for regions (e.g.
en-US)
5.2 Message List API
bash
curl "http://localhost:8080/api/notifications?lang=en&name=Kitty"
# response: {"code":0,"message":"success","data":{"greeting":"Hello, Kitty! You have 5 new messages."}}bash
curl "http://localhost:8080/api/notifications?lang=zh&name=Fish%20Tail%20Unending
# {"code":0,"message":"success","data":{"greeting":"Hello, Fishfinal! You have 5 new messages."}}5.3 Batch Translation
bash
curl -X POST http://localhost:8080/api/translate \
-H "Content-Type: application/json" \
-H "Accept-Language: en-US" \
-d '{"keys": ["NotificationMessage", "WelcomeMessage"], "data": {"Name": "Kitty", "Count": 3}}'
# response: {"code":100100,"message":"success","data":{"NotificationMessage":"Hello, Kitty! You have 3 new messages.","WelcomeMessage":"Welcome to our website!"}}bash
curl -X POST http://localhost:8080/api/translate \
-H "Content-Type: application/json" \
-H "Accept-Language: zh" \
-d '{"keys": ["NotificationMessage", "WelcomeMessage"], "data": {"Name": "Endless Fish Tail", "Count": 3}}'
# {"code":100100,"message":"success","data":{"NotificationMessage":"Hello, Fishfinal! You have 3 new messages.","WelcomeMessage":"Welcome to our website!"}}5.4 Get current language information
bash
curl "http://localhost:8080/api/locale?lang=en-US"
# response: {"code":100100,"message":"success","data":{"language":"en-US","tag":"en-US"}}bash
curl "http://localhost:8080/api/locale?lang=zh-CN"
# response: {"code":100100,"message":"success","data":{"language":"zh-CN","tag":"zh-CN"}}Step 6: Frontend consumption example (Vue 3)
vue
<template>
<div class="i18n-demo">
<h2>{{ greeting }}</h2>
<div v-for="(value, key) in translations" :key="key">
{{ key }}: {{ value }}
</div>
<p>Current language: {{ currentLang }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
});
// Reactive data
const greeting = ref<string>('');
const translations = ref<Record<string, string>>({});
const currentLang = ref<string>('zh');
// Get greeting with translation
async function getGreeting(name: string, lang: string) {
const response = await api.get('/hello', {
params: { name, lang },
});
greeting.value = response.data.data.greeting;
}
// Batch fetch translations
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;
}
// Get current language information
async function getLocale(lang: string) {
const response = await api.get('/locale', {
params: { lang },
});
currentLang.value = response.data.data.language;
}
// Called on page load
onMounted(async () => {
const lang = 'zh';
await getGreeting('Fishfinal', lang);
await getTranslations(['NotificationMessage', 'WelcomeMessage'], lang, {
Name: 'Fish Tail Endless',
Count: 3,
});
await getLocale(lang);
});
</script>Frequently Asked Questions and Practical Suggestions
Q: How should the situation be handled when the translation file does not exist or the lookup fails?
Be sure to always check.Localize returned error. A common pattern is to use the message's ID As degraded content:
go
translated, err := l.Localize(&i18n.LocalizeConfig{MessageID: "WelcomeMessage"})
if err != nil {
// Try using the default language, or return the ID directly
translated = "WelcomeMessage"
}Q: How to support switching languages via URL parameters (e.g.?lang=zh)?
In middleware, language preferences can be obtained from query parameters and override or supplement Accept-Language:
go
lang := c.Query("lang")
if lang == "" {
lang = c.GetHeader("Accept-Language")
}
localizer := i18n.NewLocalizer(bundle, lang)Q: Are there any performance considerations to note?
Bundleof loading (LoadMessageFile) is an I/O-intensive operation,Be sure to execute only once.(atmainorinitMedium)LocalizerIt is a stateless lightweight object; creating it for each request is cheap and safe.Use
embed.FSCompiling files into the binary can reduce runtime filesystem overhead.
Q: How should translation files be organized for large projects?
For large projects, it is recommended to split translation files by module:
locale/
├── active.en.toml # General translation
├── active.zh.toml
├── auth/
│ ├── active.en.toml # Authentication module
│ └── active.zh.toml
└── payment/
├── active.en.toml # Payment module
└── active.zh.tomlLoad all files in a loop when loading:
go
files, _ := fs.Glob(localeFS, "locale/**/*.toml")
for _, f := range files {
bundle.LoadMessageFileFS(localeFS, f)
}Q: How to uniformly handle i18n errors in Gin JSON responses?
You can encapsulate a helper function:
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 // Return ID on downgrade
}
return translated
}Summary
go-i18n Provides professional and complete internationalization solutions for Go applications. By understanding Bundle,Localizer and Message These three pillars, and make good use of them goi18n With the command-line tool, you can efficiently manage multilingual content.
In a front-end and back-end separated architecture, combined with the Gin framework to provide JSON API API, allowing the frontend to freely obtain localized translation data, perfectly adapting to the development model of modern Web applications.
I hope this practical guide helps you get started quickly and enables your Go application to better serve users around the world!
Complete code for this article: All code snippets in this article can be used directly in real projects; simply follow the steps to complete.go-i18n Integration with the Gin framework.