IntlPull
Tutorial
13 min read

Go i18n: Complete Golang Localization Guide for 2026

Master Go internationalization: golang.org/x/text package, go-i18n library, message extraction, CLDR plural rules, HTTP middleware, template functions, and IntlPull integration.

IntlPull Team
IntlPull Team
Feb 12, 2026
On this page
Summary

Master Go internationalization: golang.org/x/text package, go-i18n library, message extraction, CLDR plural rules, HTTP middleware, template functions, and IntlPull integration.

Go provides robust internationalization support through the golang.org/x/text package and community libraries like go-i18n. This guide covers everything you need to build fully localized Go applications, from basic message catalogs to advanced patterns including HTTP middleware, template integration, and external translation management with IntlPull.

Go i18n Fundamentals

Go's i18n ecosystem consists of three main components:

  1. golang.org/x/text - Official package with Unicode CLDR support, message formatting, and locale parsing
  2. go-i18n - Popular community library for message catalogs and pluralization
  3. gotext - GNU gettext-style translation (alternative approach)

This guide focuses on x/text and go-i18n as the recommended modern approach.

Project Setup

Install Dependencies

Terminal
1# Core i18n package
2go get golang.org/x/text
3
4# go-i18n library
5go get github.com/nicksnyder/go-i18n/v2
6
7# CLI tool for message extraction
8go install github.com/nicksnyder/go-i18n/v2/goi18n@latest

Project Structure

myapp/
├── main.go
├── i18n/
│   ├── i18n.go                    # i18n initialization
│   ├── middleware.go              # HTTP middleware
│   └── locales/
│       ├── active.en.toml         # English messages
│       ├── active.es.toml         # Spanish messages
│       ├── active.fr.toml         # French messages
│       └── translate.en.toml      # Untranslated English keys
├── handlers/
│   └── home.go
└── templates/
    └── index.html

Basic go-i18n Setup

Initialize Bundle

GO
1// i18n/i18n.go
2package i18n
3
4import (
5    "embed"
6    "encoding/json"
7
8    "github.com/nicksnyder/go-i18n/v2/i18n"
9    "golang.org/x/text/language"
10)
11
12//go:embed locales/*.toml
13var LocaleFS embed.FS
14
15var Bundle *i18n.Bundle
16
17func Init() {
18    Bundle = i18n.NewBundle(language.English)
19    Bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
20
21    // Load all locale files
22    Bundle.LoadMessageFileFS(LocaleFS, "locales/active.en.toml")
23    Bundle.LoadMessageFileFS(LocaleFS, "locales/active.es.toml")
24    Bundle.LoadMessageFileFS(LocaleFS, "locales/active.fr.toml")
25}

Message Files

TOML
1# locales/active.en.toml
2[welcome]
3description = "Welcome message for home page"
4other = "Welcome to our application!"
5
6[welcome_user]
7description = "Personalized welcome message"
8other = "Welcome back, {{.Name}}!"
9
10[item_count]
11description = "Number of items in cart"
12one = "You have {{.Count}} item in your cart"
13other = "You have {{.Count}} items in your cart"
14
15[error_not_found]
16description = "404 error message"
17other = "The resource you requested was not found"
18
19[button_submit]
20other = "Submit"
21
22[button_cancel]
23other = "Cancel"
TOML
1# locales/active.es.toml
2[welcome]
3other = "¡Bienvenido a nuestra aplicación!"
4
5[welcome_user]
6other = "¡Bienvenido de nuevo, {{.Name}}!"
7
8[item_count]
9one = "Tienes {{.Count}} artículo en tu carrito"
10other = "Tienes {{.Count}} artículos en tu carrito"
11
12[error_not_found]
13other = "El recurso que solicitaste no fue encontrado"
14
15[button_submit]
16other = "Enviar"
17
18[button_cancel]
19other = "Cancelar"

Using Localizer

GO
1package main
2
3import (
4    "fmt"
5    "myapp/i18n"
6
7    "github.com/nicksnyder/go-i18n/v2/i18n"
8    "golang.org/x/text/language"
9)
10
11func main() {
12    i18n.Init()
13
14    // Create localizer for Spanish
15    localizer := i18n.NewLocalizer(i18n.Bundle, "es")
16
17    // Simple message
18    msg := localizer.MustLocalize(&i18n.LocalizeConfig{
19        MessageID: "welcome",
20    })
21    fmt.Println(msg) // Output: ¡Bienvenido a nuestra aplicación!
22
23    // Message with template data
24    msg = localizer.MustLocalize(&i18n.LocalizeConfig{
25        MessageID: "welcome_user",
26        TemplateData: map[string]string{
27            "Name": "Juan",
28        },
29    })
30    fmt.Println(msg) // Output: ¡Bienvenido de nuevo, Juan!
31
32    // Plural message
33    msg = localizer.MustLocalize(&i18n.LocalizeConfig{
34        MessageID: "item_count",
35        TemplateData: map[string]int{
36            "Count": 5,
37        },
38        PluralCount: 5,
39    })
40    fmt.Println(msg) // Output: Tienes 5 artículos en tu carrito
41}

HTTP Middleware for Locale Detection

Locale Resolution Middleware

GO
1// i18n/middleware.go
2package i18n
3
4import (
5    "context"
6    "net/http"
7    "strings"
8
9    "github.com/nicksnyder/go-i18n/v2/i18n"
10    "golang.org/x/text/language"
11)
12
13type contextKey string
14
15const LocalizerKey contextKey = "localizer"
16
17// LocaleMiddleware detects user's preferred language and creates localizer
18func LocaleMiddleware(next http.Handler) http.Handler {
19    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20        lang := detectLanguage(r)
21        localizer := i18n.NewLocalizer(Bundle, lang)
22
23        ctx := context.WithValue(r.Context(), LocalizerKey, localizer)
24        next.ServeHTTP(w, r.WithContext(ctx))
25    })
26}
27
28func detectLanguage(r *http.Request) string {
29    // Priority 1: URL parameter (?lang=es)
30    if lang := r.URL.Query().Get("lang"); lang != "" {
31        return lang
32    }
33
34    // Priority 2: Cookie
35    if cookie, err := r.Cookie("user-lang"); err == nil {
36        return cookie.Value
37    }
38
39    // Priority 3: Accept-Language header
40    acceptLanguage := r.Header.Get("Accept-Language")
41    if acceptLanguage != "" {
42        tags, _, err := language.ParseAcceptLanguage(acceptLanguage)
43        if err == nil && len(tags) > 0 {
44            return tags[0].String()
45        }
46    }
47
48    // Default to English
49    return "en"
50}
51
52// GetLocalizer retrieves localizer from request context
53func GetLocalizer(r *http.Request) *i18n.Localizer {
54    if localizer, ok := r.Context().Value(LocalizerKey).(*i18n.Localizer); ok {
55        return localizer
56    }
57    // Fallback to English
58    return i18n.NewLocalizer(Bundle, "en")
59}
60
61// Localize is a helper function for handlers
62func Localize(r *http.Request, messageID string, templateData interface{}) string {
63    localizer := GetLocalizer(r)
64    return localizer.MustLocalize(&i18n.LocalizeConfig{
65        MessageID:    messageID,
66        TemplateData: templateData,
67    })
68}

Using Middleware in HTTP Server

GO
1package main
2
3import (
4    "encoding/json"
5    "net/http"
6    "myapp/i18n"
7
8    "github.com/gorilla/mux"
9)
10
11func main() {
12    i18n.Init()
13
14    router := mux.NewRouter()
15    router.Use(i18n.LocaleMiddleware)
16
17    router.HandleFunc("/", homeHandler)
18    router.HandleFunc("/api/greet", greetHandler)
19
20    http.ListenAndServe(":8080", router)
21}
22
23func homeHandler(w http.ResponseWriter, r *http.Request) {
24    localizer := i18n.GetLocalizer(r)
25
26    welcome := localizer.MustLocalize(&i18n.LocalizeConfig{
27        MessageID: "welcome",
28    })
29
30    w.Header().Set("Content-Type", "text/html; charset=utf-8")
31    fmt.Fprintf(w, "<h1>%s</h1>", welcome)
32}
33
34func greetHandler(w http.ResponseWriter, r *http.Request) {
35    name := r.URL.Query().Get("name")
36    if name == "" {
37        name = "Guest"
38    }
39
40    message := i18n.Localize(r, "welcome_user", map[string]string{
41        "Name": name,
42    })
43
44    w.Header().Set("Content-Type", "application/json")
45    json.NewEncoder(w).Encode(map[string]string{
46        "message": message,
47    })
48}

Template Integration

html/template Functions

GO
1package main
2
3import (
4    "html/template"
5    "net/http"
6    "myapp/i18n"
7)
8
9func createTemplate() *template.Template {
10    funcMap := template.FuncMap{
11        "t": func(r *http.Request, messageID string, data ...interface{}) string {
12            localizer := i18n.GetLocalizer(r)
13
14            var templateData interface{}
15            if len(data) > 0 {
16                templateData = data[0]
17            }
18
19            return localizer.MustLocalize(&i18n.LocalizeConfig{
20                MessageID:    messageID,
21                TemplateData: templateData,
22            })
23        },
24    }
25
26    return template.New("").Funcs(funcMap)
27}
28
29func renderTemplate(w http.ResponseWriter, r *http.Request, tmpl string, data interface{}) {
30    t := createTemplate()
31    t, _ = t.Parse(tmpl)
32    t.Execute(w, struct {
33        Data    interface{}
34        Request *http.Request
35    }{
36        Data:    data,
37        Request: r,
38    })
39}
40
41func homeHandler(w http.ResponseWriter, r *http.Request) {
42    tmpl := `
43    <!DOCTYPE html>
44    <html>
45    <head>
46        <title>{{t .Request "app.title"}}</title>
47    </head>
48    <body>
49        <h1>{{t .Request "welcome"}}</h1>
50        <p>{{t .Request "welcome_user" .Data}}</p>
51        <button>{{t .Request "button_submit"}}</button>
52    </body>
53    </html>
54    `
55
56    renderTemplate(w, r, tmpl, map[string]string{
57        "Name": "John",
58    })
59}

CLDR Plural Rules

Go's x/text package includes full CLDR (Common Locale Data Repository) support for plural rules.

Plural Rule Examples

TOML
1# English (2 forms: one, other)
2[item_count]
3one = "{{.Count}} item"
4other = "{{.Count}} items"
5
6# Spanish (2 forms: one, other)
7[item_count]
8one = "{{.Count}} artículo"
9other = "{{.Count}} artículos"
10
11# Russian (3 forms: one, few, other)
12[item_count]
13one = "{{.Count}} товар"     # 1, 21, 31, ...
14few = "{{.Count}} товара"    # 2-4, 22-24, 32-34, ...
15other = "{{.Count}} товаров" # 0, 5-20, 25-30, ...
16
17# Arabic (6 forms: zero, one, two, few, many, other)
18[item_count]
19zero = "لا توجد عناصر"
20one = "عنصر واحد"
21two = "عنصران"
22few = "{{.Count}} عناصر"
23many = "{{.Count}} عنصرًا"
24other = "{{.Count}} عنصر"

Using Plural Messages

GO
1func showCartItems(localizer *i18n.Localizer, count int) string {
2    return localizer.MustLocalize(&i18n.LocalizeConfig{
3        MessageID: "item_count",
4        TemplateData: map[string]int{
5            "Count": count,
6        },
7        PluralCount: count,
8    })
9}
10
11// Examples:
12// English: 1 item, 5 items
13// Spanish: 1 artículo, 5 artículos
14// Russian: 1 товар, 2 товара, 5 товаров
15// Arabic: لا توجد عناصر, عنصر واحد, عنصران, 3 عناصر

Message Extraction

Extract Messages from Code

GO
1// handlers/home.go
2package handlers
3
4import (
5    "net/http"
6    "myapp/i18n"
7
8    "github.com/nicksnyder/go-i18n/v2/i18n"
9)
10
11func HomeHandler(w http.ResponseWriter, r *http.Request) {
12    localizer := i18n.GetLocalizer(r)
13
14    // Messages to extract
15    welcome := localizer.MustLocalize(&i18n.LocalizeConfig{
16        DefaultMessage: &i18n.Message{
17            ID:          "welcome",
18            Description: "Home page welcome message",
19            Other:       "Welcome to our application!",
20        },
21    })
22
23    greeting := localizer.MustLocalize(&i18n.LocalizeConfig{
24        DefaultMessage: &i18n.Message{
25            ID:          "welcome_user",
26            Description: "Personalized greeting",
27            Other:       "Welcome back, {{.Name}}!",
28        },
29        TemplateData: map[string]string{
30            "Name": "User",
31        },
32    })
33
34    // Render response...
35}

Run Extraction

Terminal
1# Extract messages from code
2$ goi18n extract -outdir i18n/locales
3
4# Output: i18n/locales/active.en.toml (all messages)
5
6# Generate translation template for Spanish
7$ goi18n merge -outdir i18n/locales \
8    i18n/locales/active.en.toml \
9    i18n/locales/translate.es.toml
10
11# Output: i18n/locales/translate.es.toml (untranslated keys only)

JSON Format (Alternative)

GO
// Use JSON instead of TOML
Bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
Bundle.LoadMessageFileFS(LocaleFS, "locales/en.json")
JSON
1// locales/en.json
2{
3  "welcome": {
4    "description": "Welcome message for home page",
5    "other": "Welcome to our application!"
6  },
7  "welcome_user": {
8    "description": "Personalized welcome message",
9    "other": "Welcome back, {{.Name}}!"
10  },
11  "item_count": {
12    "description": "Number of items in cart",
13    "one": "You have {{.Count}} item in your cart",
14    "other": "You have {{.Count}} items in your cart"
15  }
16}

Error Handling

GO
1// Safe localization with error handling
2func SafeLocalize(localizer *i18n.Localizer, messageID string, templateData interface{}) string {
3    msg, err := localizer.Localize(&i18n.LocalizeConfig{
4        MessageID:    messageID,
5        TemplateData: templateData,
6    })
7
8    if err != nil {
9        // Log error and return message ID as fallback
10        log.Printf("Localization error for %s: %v", messageID, err)
11        return messageID
12    }
13
14    return msg
15}
16
17// API response with error handling
18type APIResponse struct {
19    Message string `json:"message"`
20    Error   string `json:"error,omitempty"`
21}
22
23func apiHandler(w http.ResponseWriter, r *http.Request) {
24    localizer := i18n.GetLocalizer(r)
25
26    msg, err := localizer.Localize(&i18n.LocalizeConfig{
27        MessageID: "api.success",
28    })
29
30    response := APIResponse{
31        Message: msg,
32    }
33
34    if err != nil {
35        response.Error = "Translation not available"
36    }
37
38    json.NewEncoder(w).Encode(response)
39}

IntlPull Integration

Configuration

GO
1// config/intlpull.go
2package config
3
4import (
5    "os"
6
7    "github.com/intlpull/go-sdk/intlpull"
8)
9
10var IntlPullClient *intlpull.Client
11
12func InitIntlPull() {
13    IntlPullClient = intlpull.NewClient(&intlpull.Config{
14        APIKey:    os.Getenv("INTLPULL_API_KEY"),
15        ProjectID: os.Getenv("INTLPULL_PROJECT_ID"),
16    })
17}

Auto-Sync Translations

GO
1package i18n
2
3import (
4    "log"
5    "myapp/config"
6    "time"
7)
8
9func StartAutoSync() {
10    ticker := time.NewTicker(1 * time.Hour)
11    defer ticker.Stop()
12
13    for range ticker.C {
14        if err := syncTranslations(); err != nil {
15            log.Printf("Translation sync error: %v", err)
16        }
17    }
18}
19
20func syncTranslations() error {
21    languages := []string{"en", "es", "fr", "de"}
22
23    for _, lang := range languages {
24        translations, err := config.IntlPullClient.Export.Download(lang, "json")
25        if err != nil {
26            return err
27        }
28
29        // Write to embedded FS location (for next build)
30        // Or store in database for runtime updates
31        if err := saveTranslations(lang, translations); err != nil {
32            return err
33        }
34
35        // Reload bundle
36        Bundle.LoadMessageFile(fmt.Sprintf("locales/active.%s.json", lang))
37    }
38
39    log.Println("Translations synced successfully")
40    return nil
41}

CLI Integration

Terminal
1# Push messages to IntlPull
2$ intlpull push --file i18n/locales/active.en.toml --format toml
3
4# Pull translations
5$ intlpull pull --languages es,fr,de --format toml --output i18n/locales
6
7# Files generated:
8# - i18n/locales/active.es.toml
9# - i18n/locales/active.fr.toml
10# - i18n/locales/active.de.toml

Testing i18n

GO
1package handlers
2
3import (
4    "net/http"
5    "net/http/httptest"
6    "testing"
7
8    "myapp/i18n"
9
10    "github.com/stretchr/testify/assert"
11)
12
13func TestHomeHandlerEnglish(t *testing.T) {
14    i18n.Init()
15
16    req := httptest.NewRequest("GET", "/", nil)
17    req.Header.Set("Accept-Language", "en")
18
19    rr := httptest.NewRecorder()
20
21    handler := i18n.LocaleMiddleware(http.HandlerFunc(HomeHandler))
22    handler.ServeHTTP(rr, req)
23
24    assert.Equal(t, http.StatusOK, rr.Code)
25    assert.Contains(t, rr.Body.String(), "Welcome to our application!")
26}
27
28func TestHomeHandlerSpanish(t *testing.T) {
29    i18n.Init()
30
31    req := httptest.NewRequest("GET", "/", nil)
32    req.Header.Set("Accept-Language", "es")
33
34    rr := httptest.NewRecorder()
35
36    handler := i18n.LocaleMiddleware(http.HandlerFunc(HomeHandler))
37    handler.ServeHTTP(rr, req)
38
39    assert.Equal(t, http.StatusOK, rr.Code)
40    assert.Contains(t, rr.Body.String(), "¡Bienvenido a nuestra aplicación!")
41}
42
43func TestPluralMessages(t *testing.T) {
44    i18n.Init()
45
46    localizer := i18n.NewLocalizer(i18n.Bundle, "en")
47
48    tests := []struct {
49        count    int
50        expected string
51    }{
52        {1, "You have 1 item in your cart"},
53        {5, "You have 5 items in your cart"},
54        {0, "You have 0 items in your cart"},
55    }
56
57    for _, tt := range tests {
58        msg := localizer.MustLocalize(&i18n.LocalizeConfig{
59            MessageID: "item_count",
60            TemplateData: map[string]int{
61                "Count": tt.count,
62            },
63            PluralCount: tt.count,
64        })
65
66        assert.Equal(t, tt.expected, msg)
67    }
68}

Performance Optimization

Caching Localizers

GO
1package i18n
2
3import (
4    "sync"
5
6    "github.com/nicksnyder/go-i18n/v2/i18n"
7)
8
9var (
10    localizerCache = make(map[string]*i18n.Localizer)
11    cacheMutex     sync.RWMutex
12)
13
14func GetCachedLocalizer(lang string) *i18n.Localizer {
15    cacheMutex.RLock()
16    if localizer, ok := localizerCache[lang]; ok {
17        cacheMutex.RUnlock()
18        return localizer
19    }
20    cacheMutex.RUnlock()
21
22    cacheMutex.Lock()
23    defer cacheMutex.Unlock()
24
25    // Double-check after acquiring write lock
26    if localizer, ok := localizerCache[lang]; ok {
27        return localizer
28    }
29
30    localizer := i18n.NewLocalizer(Bundle, lang)
31    localizerCache[lang] = localizer
32    return localizer
33}

Preloading Messages

GO
1func PreloadMessages() {
2    languages := []string{"en", "es", "fr", "de"}
3
4    for _, lang := range languages {
5        GetCachedLocalizer(lang)
6    }
7
8    log.Println("Messages preloaded for all languages")
9}

Frequently Asked Questions

Q: Should I use TOML or JSON for message files?

Both work. TOML is more human-readable with better comment support. JSON integrates easily with JavaScript frontends. Choose based on your workflow.

Q: How do I handle missing translations?

go-i18n automatically falls back to the default language (usually English). Use Localize instead of MustLocalize to handle errors gracefully.

Q: Can I store translations in a database?

Yes, implement a custom MessageFile loader that fetches from your database. Useful for CMS-driven translations or runtime updates.

Q: How do I localize dates and numbers?

Use golang.org/x/text/message package for locale-aware formatting:

GO
1import "golang.org/x/text/message"
2
3p := message.NewPrinter(language.Spanish)
4p.Printf("%d", 1234567) // Output: 1.234.567

Q: What's the difference between go-i18n and gotext?

go-i18n uses JSON/TOML catalogs and integrates with x/text. gotext uses GNU gettext (.po files). go-i18n is more modern and Go-idiomatic.

Q: How do I handle gender agreement?

Use message variants with select syntax (similar to ICU MessageFormat):

TOML
[welcome_gendered]
other = "{{.Gender | select "male" "Welcome, sir!" "female" "Welcome, madam!" "other" "Welcome!"}}"

Q: Can I use environment variables in messages?

No, messages should be static. Pass dynamic values via TemplateData.

Q: How does IntlPull help with Go i18n?

IntlPull provides centralized translation management, CLI sync, validation, team collaboration, and live updates—all compatible with go-i18n file formats.

Conclusion

Go provides robust i18n support through:

  1. x/text package for CLDR data and locale handling
  2. go-i18n library for message catalogs and pluralization
  3. HTTP middleware for automatic locale detection
  4. Template integration for localized views
  5. CLI tools for message extraction and management

Key takeaways:

  • Use go-i18n for message catalogs with plural support
  • Implement locale middleware for HTTP services
  • Use CLDR plural rules for accurate pluralization
  • Integrate with IntlPull for centralized translation management
  • Test all languages with locale-specific test cases

Go's i18n capabilities combined with tools like IntlPull enable building fully localized backend services with type safety, performance, and maintainability.

Tags
go
golang
i18n
localization
backend
x-text
message
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.