Guide: i18n and l10n
github.com/angvp/tango/i18n is tanGO's small, opt-in translation layer. It translates text by symbolic key, keeps the English fallback inline in Go source, and stores the resolved locale on the request's standard context.Context.
It is intentionally not a full locale framework: no gettext pipeline, no extraction CLI, no plural rules, and no locale-aware date/time/number/currency formatting.
Translating text
Use i18n.T anywhere you have a context.Context:
message := i18n.T(ctx.Context(), "books.title", "Books")
The signature is:
func T(ctx context.Context, key, fallback string, args ...any) string
T resolves in this order:
- Use the request locale's catalog when it has
key. - If the locale has a region, try the base language catalog (
es-MX→es). - Return
fallback.
The fallback is the permanent English source of truth. If no i18n middleware is installed, no catalog exists, or a catalog misses a key, the app still renders the inline fallback.
For dynamic text, use fmt.Sprintf-style placeholders:
title := i18n.T(ctx.Context(), "admin.create.title", "New %s", meta.Name)
The same placeholders must exist in translated catalog values:
i18n.RegisterCatalog("es", map[string]string{
"admin.create.title": "Nuevo %s",
})
Catalogs
Catalogs are plain Go maps registered explicitly:
i18n.RegisterCatalog("es", map[string]string{
"admin.login.title": "Administración tanGO",
"admin.button.save": "Guardar",
})
Multiple calls for the same locale merge per key. If two catalogs register the same key for the same locale, the latest value wins. Different locales stay isolated.
A reusable app can register its own catalog from its Register function by convention:
func New() tango.App {
return tango.NewApp("shop", func(registry *tango.Registry) error {
i18n.RegisterCatalog("es", map[string]string{
"shop.cart.title": "Carrito",
})
return registry.Routes().Include("/shop/", URLs)
})
}
Locale middleware
Locale resolution is request-scoped and explicit. Add i18n.Middleware to tango.Config.Middleware:
config := tango.Config{
InstalledApps: []tango.App{
admin.New(store),
shop.New(),
},
Middleware: []tango.Middleware{
i18n.Middleware(nil), // nil uses Accept-Language
},
}
nil uses i18n.DefaultLocaleResolver, which parses Accept-Language and chooses the best registered catalog using tanGO's simplified exact-then-base matching. For example, es-MX uses the es-MX catalog when present, otherwise es, otherwise the inline fallback.
You can provide your own resolver:
resolver := func(r *http.Request) string {
if cookie, err := r.Cookie("locale"); err == nil {
return cookie.Value
}
return i18n.DefaultLocaleResolver(r)
}
config.Middleware = append(config.Middleware, i18n.Middleware(resolver))
No first-party app wires this automatically. Admin calls i18n.T internally, but it renders English fallbacks until the host registers catalogs and middleware.
Key naming
Use explicit symbolic keys, not English text as keys.
Good keys are namespaced by owner and surface:
admin.login.titleadmin.button.saveshop.cart.empty
This keeps English copy editable without breaking existing catalogs.
Not supported
- Pluralization: tanGO has no plural-rule engine yet; use separate simple keys when needed.
- Date/time formatting: HTML form controls still use browser-required formats such as
datetime-local. - Number/currency formatting: use app-level formatting for now.
- File-based catalogs: catalogs are Go source maps only; JSON/YAML/gettext loading can be layered on later without changing
T. - Automatic locale storage: no cookie, session, or account preference resolver ships by default, but
LocaleResolvermakes those easy to add in app code.