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:
- golang.org/x/text - Official package with Unicode CLDR support, message formatting, and locale parsing
- go-i18n - Popular community library for message catalogs and pluralization
- 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
Terminal1# 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
GO1// 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
TOML1# 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"
TOML1# 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
GO1package 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
GO1// 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
GO1package 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
GO1package 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
TOML1# 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
GO1func 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
GO1// 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
Terminal1# 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")
JSON1// 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
GO1// 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
GO1// 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
GO1package 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
Terminal1# 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
GO1package 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
GO1package 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
GO1func 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:
GO1import "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:
- x/text package for CLDR data and locale handling
- go-i18n library for message catalogs and pluralization
- HTTP middleware for automatic locale detection
- Template integration for localized views
- CLI tools for message extraction and management
Key takeaways:
- Use
go-i18nfor 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.
