Adding a second language to a Go service looks like a two-hour task until you hit plural rules. English has two forms, Polish has four, Arabic has six, and Japanese has one — and the moment a product manager asks for “3 pliki” versus “5 plików” you discover that fmt.Sprintf with an if count == 1 branch is not an internationalization strategy. Go has no blessed answer in the standard library; there are three credible packages and they disagree about almost everything: message format, translation workflow, and who owns plural logic.
Here is how they actually differ, with real APIs and the operational trade-offs that show up after the first translation round.
TL;DR
- Choose
nicksnyder/go-i18nfor most applications. It ships CLDR plural rules for 200+ languages, reads JSON/TOML/YAML message files, and aLocalizerthat resolvesAccept-Languagecorrectly. Best ratio of features to effort. - Choose
leonelquinteros/gotextif your translators already live in PO/MO files or you are porting a GNU gettext workflow from another language. Standard tooling, standard file format, zero surprises for translation agencies. - Choose
golang.org/x/textwhen you need more than strings: locale-aware number, currency, date and collation. It is a text-processing foundation, not a translation workflow, and its tooling is the most involved of the three.
Side-by-Side
| Dimension | go-i18n (v2) | gotext | golang.org/x/text |
|---|---|---|---|
| GitHub stars | 3,552 | 513 | 810 (mirror) |
| Last push | 2026-10-28 | 2026-10-14 | 2026-10-21 |
| License | MIT | MIT | BSD-3-Clause |
| Message format | JSON / TOML / YAML | PO / MO (gettext) | Go source + generated catalog |
| Plural rules | CLDR, 200+ languages | GNU gettext plural expressions | CLDR (via feature/plural) |
| Variables | text/template syntax | fmt verbs (%s, %d) | fmt verbs via Printer |
| Locale negotiation | Accept-Language parsing built in | Manual / colon-separated fallback | language.Matcher (best-in-class) |
| Number & date formatting | No | No | Yes — currency, units, dates |
| Codegen required | No | No (PO compiled at runtime or pre-parsed) | Yes (gotext generate/update) |
| Translation tooling | goi18n extract / merge | xgotext | gotext extract/rewrite/update |
| Runtime lookup cost | Map lookup + template exec | Map lookup + fmt | Catalog lookup |
| Learning curve | Low | Low–medium | High |
Decision Matrix
| Your situation | Recommendation | Why |
|---|---|---|
| REST API returning user-facing messages | go-i18n | Localizer handles Accept-Language, tags, and fallbacks in one call |
Porting a Django/PHP app with existing .po files | gotext | Same files, same plural headers, no re-translation cost |
| Multi-tenant SaaS with per-tenant locale | go-i18n | One Bundle, many Localizers — cheap and goroutine-safe |
| CLI tool with 3 languages, 40 strings | gotext or go-i18n | Either works; gotext avoids template syntax |
| Formatting prices and dates per locale | x/text (possibly alongside go-i18n) | Only x/text formats 1.234,56 € and locale dates correctly |
| Machine-translated content pipeline | gotext | PO files are the lingua franca of translation platforms |
| Huge catalog, strict latency budget | gotext with pre-parsed MO / Locale.MarshalBinary | Avoids parsing text files at startup |
go-i18n — The Pragmatic Default
nicksnyder/go-i18n (3,552 stars, pushed 2026-10-28) is the package most Go teams reach for, and the design is why. You create one Bundle at startup, load message files in whatever format you already use, and create a Localizer per request from the user’s language preferences.
| |
| |
Per request, hand it both an explicit choice and the header:
| |
Lookups default to the source string when a translation is missing. That matters more than it sounds: it means a partially translated locale degrades to English instead of showing PersonCats keys:
| |
The plural support is generated from Unicode CLDR data, covering 200+ languages, which is the main reason to prefer this package over hand-rolled maps. Polish, Russian, Arabic and Welsh all resolve to the right form without extra code — you only supply the One/Other variants (and Few/Many where the language needs them).
The goi18n CLI closes the loop: goi18n extract scans Go source for i18n.Message literals into active.en.toml, and goi18n merge active.*.toml produces translate.*.toml files for translators, then merges them back. Message IDs do not churn because you own them explicitly.
Trade-off: no number, date or currency formatting. {{.Count}} renders Go’s default integer formatting, which is wrong for German (1.234) and unusable for currencies. Pair it with x/text when you need those.
gotext — GNU Gettext Without the C Dependency
leonelquinteros/gotext (513 stars, pushed 2026-10-14) is a native Go implementation of GNU gettext. If your organization already runs a translation pipeline — Weblate, Poedit, Crowdin, agency handoffs — this is the shortest path, because the file format is the industry’s default.
| |
The package-level API is the quick start:
| |
For anything with more than one domain or language in play, use a Locale object instead of the global configuration — it is the pattern that survives concurrent requests:
| |
Plurals come from the PO header, not from your Go code:
| |
The directory layout is the conventional gettext tree, and the library tolerates both forms:
| |
Three features are underrated. Fallback chains are a colon-separated string — gotext.Configure("/locales", "es_UY:es:en_US", "default") — so a regional variant degrades to the base language automatically. Message contexts (msgctxt) disambiguate homonyms, which plain key-value formats cannot. And Locale.MarshalBinary / UnmarshalBinary let you pre-parse PO files at build time and ship a binary blob, removing startup parsing from your cold-start path.
Extraction is handled by xgotext:
| |
That writes .pot templates you merge into per-language .po files with standard tooling.
golang.org/x/text — The Foundation, Not the Workflow
golang.org/x/text (mirror: 810 stars, BSD-3, pushed 2026-10-21) is maintained by the Go team and covers the entire text-processing surface: language tags, matching, collation, segmentation, plural rules, and locale-aware formatting. It is not a translation library in the “load my message file” sense — it is the layer you build one on.
Basic use is pleasantly small:
| |
That number grouping is the whole point: x/text knows German uses . as a thousands separator and , as a decimal mark, that Japanese yen has no minor units, and how to render a date in de-DE versus en-GB. No other Go package here does this correctly without a helper library.
Getting translations into the Printer requires code generation. You install the bundled tool and run it against your packages:
| |
The tool has four subcommands — extract, update, rewrite, generate — and rewrite is the interesting one: it converts fmt.Printf calls in a package to use a message.Printer so arguments can be reordered by translators. Then at runtime you pass the generated catalog to the printer:
| |
The cost is real. Generated catalogs are Go source, so every translation change is a build. Locale matching is excellent (language.NewMatcher handles de-AT → de → en with quality values), but the workflow assumes your team is comfortable with codegen in the build pipeline. For a 40-string CLI, that is a heavy tax; for a product that formats money and dates in 12 markets, it is the only correct answer.
Pitfalls That Surface After the First Translation Round
1. Never key messages by the English string. Both go-i18n and gettext let you use the source text as the key. It seems convenient until marketing rewrites a sentence and every translation silently disappears. Use stable IDs (checkout.cart.empty) and let DefaultMessage/msgid carry the source copy.
2. Plural categories are not “one and other”. Polish needs one/few/many/other, Arabic needs six forms, and Japanese only needs other. If your message file only defines two, the fallback will pick the wrong one silently — no error, just wrong grammar. Test with counts of 1, 2, 5, 11, 21, and 100 for every locale you ship.
3. Accept-Language is not a locale. A header may contain de-AT,de;q=0.9,en;q=0.8. Take the whole header string, not r.Header.Get(...) split on commas — both go-i18n’s Localizer and x/text’s matcher expect the full preference list and quality values. Passing only the first token means Austrian users get English.
4. Do not format numbers with fmt and then translate the string. fmt.Sprintf("%d", n) produces 1234 regardless of locale, and no message file can fix it after the fact. Format first with x/text’s printer, then interpolate the already-formatted value.
5. Watch for RTL text in templates. Arabic and Hebrew render correctly only if your HTML carries dir="rtl" and your CSS uses logical properties. A translation library cannot fix a layout bug, and RTL layouts typically expose hardcoded margin-left values across a codebase.
6. Loading message files on every request is a scalability bug. Parse once at startup into a Bundle (go-i18n) or Locale (gotext) and reuse it. Hitting the filesystem per request adds milliseconds and file-descriptor pressure under load.
7. Missing translations fail differently in each library. go-i18n falls back to DefaultMessage; gotext falls back to the msgid; x/text falls back to the source string in the catalog. All three “work”, which is precisely why nobody notices a broken locale until a customer complains. Add a CI check that diffs message keys across locales.
If you are standardizing the translation workflow itself, our self-hosted translation management comparison covers the platforms that consume these files, while the JavaScript i18n libraries comparison shows how the frontend half of the same problem is usually solved. And if pluralization rules specifically are your pain point, the pluralization libraries roundup compares the string-inflection side of it.
FAQ
What is the best Go internationalization library in 2026?
For application strings, nicksnyder/go-i18n v2 is the best default: CLDR plural rules for 200+ languages, JSON/TOML/YAML message files, and a Localizer that resolves Accept-Language correctly. Use gotext instead when you must consume existing PO/MO translation assets.
Does Go have i18n in the standard library?
No. The standard library has no message catalog or plural handling. golang.org/x/text is the Go team’s official supplementary module — it lives outside the standard library but is maintained with it and covers locale matching, formatting, collation and plural rules.
How do I handle plurals for languages with more than two forms?
Use a library backed by CLDR data. go-i18n generates plural rules from Unicode CLDR for 200+ languages; gotext reads plural expressions from the PO file header. Never implement count == 1 branching manually — Polish, Arabic, Russian and Welsh all have forms English does not.
Can I format currency and dates with these libraries?
Only golang.org/x/text does this properly, via message.NewPrinter combined with the language package. go-i18n and gotext translate strings only, so pair them with x/text when you need locale-correct 1.234,56 € or localized date formats.
Which library is fastest at runtime?
gotext with pre-parsed MO files or Locale.MarshalBinary avoids startup parsing and does simple map lookups plus fmt calls. go-i18n adds a text/template execution per message, which is negligible for HTML pages but measurable in tight loops. Measure with your message sizes before optimizing.
Do I need code generation to use golang.org/x/text for translations?
Yes. The bundled gotext tool (extract, update, generate) produces a Go catalog file that must be rebuilt when translations change. If you want to add a language without recompiling, use go-i18n or gotext with runtime-loaded message files.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com