Internationalization
The i18n package
loads gettext translations and resolves
the best locale per request. It implements the middleware.LocaleProvider seam,
so wiring it up makes ProcessLocale — and the renderer’s t/tDefault
template functions — work end to end.
Layout and loading
One top-level directory per locale, each containing a default.po:
locales/
├── en/default.po
├── es/default.po
└── pt_BR/default.po
Embed and load it:
//go:embed locales
var localesFS embed.FS
sub, _ := fs.Sub(localesFS, "locales")
locales, err := i18n.Load(sub) // options: WithDefaultLocale, WithReloading
if err != nil { /* handle */ }
r.Use(middleware.ProcessLocale(locales))
ProcessLocale resolves the locale from the ?lang= query parameter, then the
Accept-Language header (full q-list matching via x/text/language, with
region fallback — pt-br finds pt_BR). It stores the translator on the
context and populates locale, textLanguage, and textDirection (RTL-aware)
on the template map.
Using translations in templates
<label>{{ tDefault .locale "Leave a message" "form.label" }}</label>
tDefaultfalls back to the given default string when the key is missing — ideal while translations are incomplete.terrors on unknown keys — ideal once your catalogs are complete.
With a locales/es/default.po containing:
msgid "form.label"
msgstr "Deja un mensaje"
…requesting /?lang=es renders the Spanish label. The
example app
demonstrates the full flow.