Skip to content

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

Diagram for the go-i18n and Gin framework internationalization tutorial

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-i18n Beginner's guide to using the library

  • Developers 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:goi18n The 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.FS Bundle 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:

componentAnalogyLifecycleResponsibilities
Bundlea repository storing translations of all languages Large warehouseCreated at application startup, global singletonLoad and manage all active.*.toml Translate the file
Localizerpicking goods from the warehouse according to user preferences AssistantEach HTTP request creates oneAccording to the user's language preference (such as Accept-Language head), from Bundle Search for and return the best matching translation.
Messagethe smallest in the warehouse Cargo unitDefined in code or translation filesRepresents 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@latest

2 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:

fieldDescriptionIs it necessary?
IDThe unique identifier of the message, used for matching and lookup in the translation file.✅ Must
OneTranslation template in singular form (e.g.Count = 1 when using)Optional
OtherTranslation template in plural form (e.g.Count != 1 used when), and is also Required default form✅ Must
descriptionMessage description to help translators understand the contextRecommend

⚠️ Important Notice:

  • goi18n extract The command will scan all in the code i18n.Message Struct, automatic extraction ID,Description,One,Other fields such as

  • Generated after extraction active.en.toml will not be included in the file hash Field: only translation files for other languages generated from the master template via goi18n merge will contain it.hash Field used to track changes in message content

  • It is recommended to always use in the code i18n.Message Define 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 extract

This 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.toml

  • Run 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, will other The 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 all active.*.toml Document

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.*.toml

Best Practices

  • always take active.*.toml Treat it as the final translation result and submit it to Git.

  • during the translation process translate.*.toml The 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 extract Check 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.go

Step 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

  • lang Parameter values comply with the BCP 47 language tag specification.

  • English usage en-US(regional variant), Chinese usage zh(General) or zh-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? ​

  • Bundle of loading (LoadMessageFile) is an I/O-intensive operation,Be sure to execute only once.(at main or init Medium)

  • Localizer It is a stateless lightweight object; creating it for each request is cheap and safe.

  • Use embed.FS Compiling 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.toml

Load 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.

Last updated on11/07/2026, 02:16 PM
@fishfinal | @fishfinals | [email protected]