Go (Golang) i18n Tutorial: go-i18n & gettext for Go Apps

The whole Go i18n picture in 2026, checked against source rather than recalled — the Go backend of GNU gettext 0.26, go-i18n v2.6.1, leonelquinteros/gotext and golang.org/x/text at master. Opens with the fact that reframes the field: xgettext gained a native Go backend in gettext 0.25 (May 2025), built on tree-sitter with real Go type analysis, so a Go program is now a first-class citizen of the PO workflow — and on 0.24 or older the implicit form does not fail, it warns that the extension is unknown and parses your code as C. Then the structural limit nobody documents: method calls are resolved through the receiver type and only gotext and snapcore types have keyword tables, so i18n.T(ctx, s) extracts and ctx.T(s) cannot, no matter what --keyword you pass — a decision that has to be made before the first call site, not after. Covers the plural call that prints %!(EXTRA int=1) because the singular has no verb to consume the count, with the %.0s workaround GNU documents and the simpler habit that makes it impossible; the two facts that combine to publish unreviewed translations, since gotext prefers the .po over the .mo and the word fuzzy appears nowhere in its parser; and the missing Plural-Forms header that silently falls back to a Germanic two-form rule, making msgstr[2] unreachable in Russian. On the go-i18n side: why MustLocalize panics on any key a translator has not finished — the fallback to the default language returns the message and a MessageNotFoundErr together — why discarding that error is the mirror-image bug, where <no value> comes from and the missingkey=error parser that ends it, and the empty string returned when a decimal PluralCount needs a CLDR other form that a gettext-derived file has no slot for. Plus what x/text is genuinely best at (locale-aware numbers) against pipeline TODOs unchanged since 2016, the html/template FuncMap that must not return template.HTML, an executed TOML ↔ PO round trip with real Russian output and the hash field surviving the trip, a placeholder gate shown catching a renamed {{.Name}}, a fifteen-row symptom table and a CI job that fails on a stale POT.

Back to the PO-File blog