SEO//13 min de lectura

Go (Golang) i18n: Patrones y Bibliotecas de Internacionalización

Eray Gündoğmuş
Compartir

Go (Golang) i18n: Patrones y Bibliotecas de Internacionalización

Go es un lenguaje cada vez más popular para servicios backend, CLIs y APIs que sirven a usuarios globales. Aunque la biblioteca estándar de Go no incluye un framework de i18n tan completo como el de otros ecosistemas, la combinación de golang.org/x/text, go-i18n y una arquitectura cuidadosa permite crear aplicaciones Go multilingües robustas.

Esta guía cubre el kit de herramientas completo de i18n para Go—desde el formateo consciente de la configuración regional usando x/text hasta la gestión de traducciones con go-i18n, además de patrones para estructurar i18n en servicios Go y CLIs.

Visión General del Ecosistema i18n de Go

Las herramientas de i18n de Go están distribuidas en varios paquetes:

PaquetePropósito
golang.org/x/text/languageAnálisis y coincidencia de etiquetas de idioma BCP 47
golang.org/x/text/messageMensajes formateados al estilo Printf con i18n
golang.org/x/text/numberFormateo de números (plurales, moneda)
golang.org/x/text/collateOrdenamiento de cadenas consciente de la configuración regional
golang.org/x/text/unicode/normNormalización Unicode
github.com/nicksnyder/go-i18n/v2Gestión de archivos de traducción, pluralización
github.com/BurntSushi/tomlAnálisis TOML (formato de traducción común)

Análisis de Etiquetas de Idioma con golang.org/x/text

El paquete golang.org/x/text/language implementa las etiquetas de idioma BCP 47—el estándar para identificadores de configuración regional usado en encabezados HTTP Accept-Language, atributos HTML lang y cadenas de configuración regional.

Análisis y Coincidencia de Etiquetas de Idioma

package main

import (
    "fmt"
    "golang.org/x/text/language"
)

func main() {
    // Analizar una etiqueta de idioma
    tag, err := language.Parse("zh-Hant-TW")
    if err != nil {
        panic(err)
    }
    fmt.Println(tag) // zh-Hant-TW

    // Coincidencia de idiomas: encontrar la mejor coincidencia entre los idiomas soportados
    supported := []language.Tag{
        language.English,
        language.French,
        language.SimplifiedChinese,
        language.TraditionalChinese,
    }
    
    matcher := language.NewMatcher(supported)
    
    // El usuario solicita zh-TW (Chino Tradicional, Taiwán)
    t, _, _ := matcher.Match(language.MustParse("zh-TW"))
    fmt.Println(t) // zh-Hant → coincide con Chino Tradicional
    
    // El usuario solicita es-MX (Español Mexicano)
    t, _, _ = matcher.Match(language.MustParse("es-MX"))
    fmt.Println(t) // en → retrocede al inglés (Español no soportado)
}

Análisis de Encabezados Accept-Language

import (
    "net/http"
    "golang.org/x/text/language"
)

var supported = []language.Tag{
    language.English,
    language.French,
    language.German,
    language.Japanese,
}

var matcher = language.NewMatcher(supported)

func getLocale(r *http.Request) language.Tag {
    // Analizar el encabezado Accept-Language
    accept := r.Header.Get("Accept-Language")
    tags, _, err := language.ParseAcceptLanguage(accept)
    if err != nil || len(tags) == 0 {
        return language.English
    }
    
    tag, _, _ := matcher.Match(tags...)
    return tag
}

go-i18n: Gestión de Traducciones

go-i18n es la biblioteca Go más usada para la gestión de traducciones. Soporta:

  • Archivos de traducción JSON, TOML y YAML
  • Formato de mensajes ICU para pluralización
  • Interpolación de plantillas
  • Carga de múltiples archivos de traducción

Instalación y Configuración

go get github.com/nicksnyder/go-i18n/v2@latest
go get github.com/BurntSushi/toml

Estructura de Archivos de Traducción

# locales/en.toml
[welcome]
description = "Mensaje de bienvenida para nuevos usuarios"
one = "Bienvenido, {{.Name}}! Tienes {{.Count}} nueva notificación."
other = "Bienvenido, {{.Name}}! Tienes {{.Count}} nuevas notificaciones."

[item_count]
description = "Número de elementos en una lista"
zero = "Sin elementos"
one = "{{.Count}} elemento"
other = "{{.Count}} elementos"
# locales/fr.toml
[welcome]
one = "Bienvenue, {{.Name}} ! Vous avez {{.Count}} nouvelle notification."
other = "Bienvenue, {{.Name}} ! Vous avez {{.Count}} nouvelles notifications."

[item_count]
zero = "Aucun élément"
one = "{{.Count}} élément"
other = "{{.Count}} éléments"

Inicialización de go-i18n

package i18n

import (
    "embed"
    "encoding/json"

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

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

var bundle *i18n.Bundle

func Init() error {
    bundle = i18n.NewBundle(language.English)
    bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)

    // Cargar todos los archivos de configuración regional
    entries, err := localeFS.ReadDir("locales")
    if err != nil {
        return err
    }

    for _, entry := range entries {
        if _, err := bundle.LoadMessageFileFS(localeFS, "locales/"+entry.Name()); err != nil {
            return fmt.Errorf("cargando configuración regional %s: %w", entry.Name(), err)
        }
    }

    return nil
}

// NewLocalizer crea un localizador para las etiquetas de idioma dadas
func NewLocalizer(langs ...string) *i18n.Localizer {
    return i18n.NewLocalizer(bundle, langs...)
}

Traducción de Mensajes

package handlers

import (
    "net/http"
    "myapp/i18n"
)

type WelcomeData struct {
    Name  string
    Count int
}

func WelcomeHandler(w http.ResponseWriter, r *http.Request) {
    // Obtener localizador para el idioma de la solicitud
    accept := r.Header.Get("Accept-Language")
    localizer := i18n.NewLocalizer(accept, "en")

    // Mensaje simple con manejo de plurales
    itemCount := 3
    msg, err := localizer.Localize(&i18n.LocalizeConfig{
        MessageID: "item_count",
        PluralCount: itemCount,
        TemplateData: map[string]interface{}{
            "Count": itemCount,
        },
    })
    if err != nil {
        msg = "Número desconocido de elementos" // respaldo
    }

    // Mensaje con nombre y plural
    welcome, err := localizer.Localize(&i18n.LocalizeConfig{
        MessageID:   "welcome",
        PluralCount: itemCount,
        TemplateData: WelcomeData{
            Name:  "Alice",
            Count: itemCount,
        },
    })

    fmt.Fprintf(w, "%s\n%s\n", welcome, msg)
}

Formateo de Números y Moneda

El paquete golang.org/x/text/message proporciona formateo de números consciente de la configuración regional:

package main

import (
    "fmt"
    "golang.org/x/text/language"
    "golang.org/x/text/message"
    "golang.org/x/text/number"
)

func main() {
    // Crear impresoras específicas de configuración regional
    enPrinter := message.NewPrinter(language.English)
    dePrinter := message.NewPrinter(language.German)
    jaPrinter := message.NewPrinter(language.Japanese)

    amount := 1234567.89

    // Formateo de números consciente de la configuración regional
    enPrinter.Printf("%v\n", number.Decimal(amount))
    // 1,234,567.89
    
    dePrinter.Printf("%v\n", number.Decimal(amount))
    // 1.234.567,89
    
    jaPrinter.Printf("%v\n", number.Decimal(amount))
    // 1,234,567.89

    // Formateo de porcentajes
    enPrinter.Printf("%.2%\n", number.Percent(0.8527))
    // 85.27%
    
    dePrinter.Printf("%.2%\n", number.Percent(0.8527))
    // 85,27 %
}

Formateo de Fecha y Hora

El paquete time de Go maneja el formateo básico de fechas, pero para el formateo de fechas consciente de la configuración regional se necesita x/text o una biblioteca de terceros:

import (
    "time"
    "golang.org/x/text/language"
    "golang.org/x/text/language/display"
)

func formatDateLocale(t time.Time, locale language.Tag) string {
    // El formateo de tiempo integrado de Go usa cadenas de diseño fijas
    // Para el formateo específico de la configuración regional, usar bibliotecas equivalentes a strftime
    // o generar patrones a partir de datos CLDR
    
    switch locale {
    case language.English:
        return t.Format("January 2, 2006")
    case language.German:
        return t.Format("2. January 2006") // Formato de fecha alemán
    case language.Japanese:
        return t.Format("2006年01月02日")
    default:
        return t.Format(time.RFC3339)
    }
}

Para un formateo de fechas de calidad de producción consciente de la configuración regional, usa una biblioteca como github.com/goodsign/monday para los nombres de días y meses.

Estructuración de i18n en un Servicio Web Go

Detección de Configuración Regional Basada en Middleware

package middleware

import (
    "context"
    "net/http"
    "golang.org/x/text/language"
    "myapp/i18n"
)

type contextKey string
const localizerKey contextKey = "localizer"

var supported = []language.Tag{
    language.English,
    language.French,
    language.German,
    language.Japanese,
}
var matcher = language.NewMatcher(supported)

func LocaleMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Verificar primero el parámetro de consulta URL (p.ej., ?lang=fr)
        lang := r.URL.Query().Get("lang")
        
        // Luego verificar la cookie
        if lang == "" {
            if cookie, err := r.Cookie("lang"); err == nil {
                lang = cookie.Value
            }
        }
        
        // Retroceder al encabezado Accept-Language
        if lang == "" {
            lang = r.Header.Get("Accept-Language")
        }

        // Crear localizador y adjuntar al contexto
        localizer := i18n.NewLocalizer(lang, "en")
        ctx := context.WithValue(r.Context(), localizerKey, localizer)
        
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// GetLocalizer recupera el localizador del contexto
func GetLocalizer(ctx context.Context) *i18n.Localizer {
    l, ok := ctx.Value(localizerKey).(*i18n.Localizer)
    if !ok {
        return i18n.NewLocalizer("en")
    }
    return l
}

Mensajes de Error como Contenido Traducible

package errors

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

// AppError es un error con un mensaje traducible
type AppError struct {
    MessageID   string
    TemplateData interface{}
    Err         error
}

func (e *AppError) Error() string {
    return e.MessageID // Identificador interno
}

func (e *AppError) Localize(localizer *i18n.Localizer) string {
    msg, err := localizer.Localize(&i18n.LocalizeConfig{
        MessageID:    e.MessageID,
        TemplateData: e.TemplateData,
    })
    if err != nil {
        return e.MessageID // Retroceder a la clave
    }
    return msg
}

// Uso
var ErrNotFound = &AppError{MessageID: "error.not_found"}
var ErrUnauthorized = &AppError{MessageID: "error.unauthorized"}

i18n para Aplicaciones CLI de Go

Las aplicaciones CLI tienen requisitos de i18n distintos: deben detectar la configuración regional del sistema, formatear la salida apropiadamente y manejar la codificación del terminal.

package main

import (
    "os"
    "golang.org/x/text/language"
    "myapp/i18n"
)

func detectSystemLocale() string {
    // Verificar la variable de entorno LANG (Unix/Linux/macOS)
    if lang := os.Getenv("LANG"); lang != "" {
        // LANG típicamente tiene el formato "en_US.UTF-8"
        // Extraer etiqueta de idioma
        parts := strings.Split(lang, ".")
        if len(parts) > 0 {
            // Convertir "en_US" a "en-US" (BCP 47)
            return strings.ReplaceAll(parts[0], "_", "-")
        }
    }
    
    // Verificar LC_ALL, LC_MESSAGES
    for _, env := range []string{"LC_ALL", "LC_MESSAGES"} {
        if lang := os.Getenv(env); lang != "" {
            return strings.ReplaceAll(strings.Split(lang, ".")[0], "_", "-")
        }
    }
    
    return "en" // Predeterminado al inglés
}

func main() {
    locale := detectSystemLocale()
    localizer := i18n.NewLocalizer(locale, "en")
    
    // Usar localizador para toda la salida
    fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{
        MessageID: "cli.welcome",
    }))
}

Herramientas de Extracción y Gestión

go-i18n proporciona una herramienta de línea de comandos goi18n para extraer cadenas traducibles:

# Instalar la herramienta CLI
go install github.com/nicksnyder/go-i18n/v2/goi18n@latest

# Extraer cadenas del código fuente Go (crea active.en.toml)
goi18n extract -format toml -outdir locales ./...

# Fusionar nuevas cadenas en las traducciones existentes
# (crea translate.fr.toml con solo cadenas nuevas/cambiadas)
goi18n merge -format toml -outdir locales locales/active.en.toml locales/active.fr.toml

# Después de traducir, fusionar translate.fr.toml de vuelta en active.fr.toml
goi18n merge -format toml -outdir locales locales/active.fr.toml locales/translate.fr.toml

Este flujo de trabajo se integra bien con la automatización de localización CI/CD para actualizaciones continuas de traducción.

Pluralización para Idiomas Complejos

Go-i18n maneja la pluralización automáticamente mediante reglas CLDR para todos los idiomas soportados:

# locales/ru.toml (Ruso - 4 formas de plural)
[item_count]
zero = "Нет элементов"
one = "{{.Count}} элемент"     # 1, 21, 31...
few = "{{.Count}} элемента"    # 2-4, 22-24...
many = "{{.Count}} элементов"  # 5-20, 25-30...
other = "{{.Count}} элемента"  # fracciones, otros
# locales/ar.toml (Árabe - 6 formas de plural)
[item_count]
zero = "لا عناصر"
one = "{{.Count}} عنصر"
two = "{{.Count}} عنصران"
few = "{{.Count}} عناصر"
many = "{{.Count}} عنصرًا"
other = "{{.Count}} عنصر"

Para una cobertura más profunda de las reglas de pluralización, consulta reglas de pluralización en diferentes idiomas.

Pruebas de i18n en Go

package i18n_test

import (
    "testing"
    "github.com/nicksnyder/go-i18n/v2/i18n"
    "golang.org/x/text/language"
)

func TestPluralForms(t *testing.T) {
    bundle := setupTestBundle(t)
    
    tests := []struct {
        lang     string
        count    int
        expected string
    }{
        {"en", 0, "No items"},
        {"en", 1, "1 item"},
        {"en", 2, "2 items"},
        {"ru", 1, "1 элемент"},
        {"ru", 2, "2 элемента"},
        {"ru", 5, "5 элементов"},
        {"ru", 11, "11 элементов"}, // Difícil: 11 usa "many", no "one"
    }
    
    for _, tt := range tests {
        t.Run(fmt.Sprintf("%s_%d", tt.lang, tt.count), func(t *testing.T) {
            localizer := i18n.NewLocalizer(bundle, tt.lang)
            got, err := localizer.Localize(&i18n.LocalizeConfig{
                MessageID:   "item_count",
                PluralCount: tt.count,
                TemplateData: map[string]interface{}{"Count": tt.count},
            })
            if err != nil {
                t.Fatalf("error de localización: %v", err)
            }
            if got != tt.expected {
                t.Errorf("obtenido %q, esperado %q", got, tt.expected)
            }
        })
    }
}

Para estrategias de prueba exhaustivas, consulta herramientas, estrategias y automatización de pruebas i18n.


Lleva tu aplicación al mundo con better-i18n

better-i18n combina traducciones impulsadas por IA, flujos de trabajo nativos de git y entrega CDN global en una plataforma centrada en el desarrollador. Deja de gestionar hojas de cálculo y empieza a publicar en todos los idiomas.

Empieza gratis → · Explora las características · Lee los docs

Comments

Loading comments...