# go-i18n: a Go package and CLI for translating Go programs

> go-i18n pairs a runtime lookup package with the goi18n command that extracts, merges and tracks message files. It fits Go services that need CLDR plural rules without a translation platform, and it assumes you already have translators to hand it TOML, JSON or YAML.

**nicksnyder/go-i18n** — Translate your Go program into multiple languages.

- Repository: https://github.com/nicksnyder/go-i18n
- Stars: 3,546 · Forks: 286
- Language: Go
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/nicksnyder-go-i18n

## What go-i18n solves for Go programs that ship in more than one language

Go's standard library gives you no message catalog. You can reach for golang.org/x/text, which go-i18n itself depends on, but x/text provides language matching and formatting primitives rather than a message store. go-i18n fills that gap with two pieces: the i18n package, which looks up a message for a set of locale preferences, and the goi18n command, which manages the files those messages live in.

The audience is narrow and specific. It is a Go team whose application already has translators or a translation vendor, and whose release process can carry message files. The README states the package supports pluralized strings for all 200+ languages in the Unicode Common Locale Data Repository, and that the code and tests for plural rules are generated from CLDR data. That generation step matters: plural categories differ by language, and hand-writing them is where most home-grown i18n layers break.

It is not a translation platform. There is no web interface, no translator assignment, no review queue. The command produces files; a human or a service moves them.

## Bundle, Localizer, and the path a message takes at runtime

The runtime model has three objects. A Bundle is created once for the lifetime of the application and holds the loaded message files. A Localizer is created per request or per user session from a set of language preferences. The Localizer performs the lookup.

The README shows the bundle being constructed with i18n.NewBundle(language.English), then a decoder being registered with bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) before files are loaded. That registration step is what makes the "any format" claim real: the package does not parse message files itself, it hands the bytes to a function you supply. TOML, YAML and JSON all work because the decoder is pluggable, and the go.mod file lists github.com/BurntSushi/toml and go.yaml.in/yaml/v3 as dependencies used by the example and tests.

Preference resolution is the interesting part. In the README's HTTP handler example, the localizer is built from a form value and the Accept-Language header together: i18n.NewLocalizer(bundle, lang, accept). The order of those arguments is the preference order, so an explicit lang parameter outranks the browser header. If you pass them the other way round, a user who clicked a language switcher will keep getting the header's language.

Lookup accepts a DefaultMessage inline or by ID. When a message is supplied inline with One and Other variants and a PluralCount, the localizer selects the CLDR plural category for the resolved language and renders the matching string through text/template syntax.

## Installing goi18n and translating a first message file

The command installs with the standard Go toolchain. The README gives this exact line, and it installs the CLI, not the library:

```bash
go install -v github.com/nicksnyder/go-i18n/v2/goi18n@latest
goi18n -help
```

After that, goi18n is on your PATH and goi18n -help prints the subcommands. The library itself is a normal module dependency, imported as github.com/nicksnyder/go-i18n/v2/i18n.

The workflow starts by extracting messages from your source. goi18n extract walks Go files and pulls out i18n.Message struct literals into an active file. The README's example output looks like this:

```toml
# active.en.toml
[PersonCats]
description = "The number of cats a person has"
one = "{{.Name}} has {{.Count}} cat."
other = "{{.Name}} has {{.Count}} cats."
```

The description field is the translator's only context, so write it as if the translator cannot see your code. They cannot.

To add Spanish, create an empty translate.es.toml and merge the active English file into it. The command populates the new file with entries carrying a hash of the source string:

```bash
goi18n merge active.en.toml translate.es.toml
```

The resulting translate.es.toml holds each message with a hash value and the untranslated text. A translator replaces the text, and you rename the file to active.es.toml. The hash is what lets goi18n detect later that the English source changed and the Spanish translation is stale, which is the mechanism behind the "translating new messages" section of the README.

Loading the finished file follows the same pattern as before, with the decoder registered first:

```go
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.LoadMessageFile("active.es.toml")
```

If you embed the files instead of shipping them loose, the README shows the embed variant: a //go:embed locale.*.toml directive over an embed.FS, then bundle.LoadMessageFileFS(LocaleFS, "locale.es.toml").

## Where the file-based workflow gets awkward

The merge model has a sharp edge. goi18n merge rewrites files in place, and the README's instructions assume you keep active and translate files in the same directory and run merge over a glob: goi18n merge active.*.toml, then goi18n merge active.*.toml translate.*.toml. That second command merges translations back into the active files. Run it against a directory where a translator has left a partially edited file and you get a partial translation promoted to active. There is no review gate in the tool.

The README does not document a dry-run flag, a diff output, or a rollback path for merge. If your team needs to inspect what merge will change before it changes it, the tool as documented does not give you that, and you should keep the message files in version control and read the diff after each run.

Format support is also a two-sided coin. Because decoding is delegated to a function you register, go-i18n does not validate message files at load time beyond what your decoder does. A malformed TOML file produces a decoder error, not an i18n-specific diagnostic. On the extraction side, goi18n only finds i18n.Message struct literals in Go source. Messages built dynamically, or read from a database, are invisible to extract and must be maintained by hand.

Finally, the community translations of the README itself are explicitly not maintained by the project author and are not guaranteed to be accurate or up to date. Treat them as a starting point only.

## go-i18n against golang.org/x/text and against a translation platform

The nearest alternative is golang.org/x/text, which go-i18n already depends on. The difference in approach is scope. x/text gives you language tags, matchers and formatters; it does not give you a message catalog, a file format for translators, or a command that extracts strings from source. If you use x/text alone you write the catalog layer yourself, including the CLDR plural category selection that go-i18n generates from CLDR data. That is the work go-i18n removes.

The other comparison is a hosted translation platform. Those systems hold strings in a database, give translators a browser interface, and often serve translations at runtime without a redeploy. go-i18n does none of that: messages are files, they are compiled into or shipped next to your binary, and a new translation is a new deploy. For a CLI tool or a self-contained service, that is a feature. For a product where marketing wants to fix a Spanish string without a release, it is a blocker.

There is no partial middle ground documented here. The README describes no runtime fetch of message files, no watch mode, and no server component.

## Maintenance, versioning and the MIT licence

The repository is not archived, and the last push was on 2026-09-19. The most recent tagged release is v2.6.1 from 2026-01-01, following v2.6.0 in April 2025 and v2.5.1 in February 2025. The module path carries a /v2 suffix, so the import path github.com/nicksnyder/go-i18n/v2/i18n is the stable one and major-version upgrades will not silently break the import.

The go.mod declares go 1.24.0, so building from source requires a toolchain at least that new. Dependencies are few and well-known: BurntSushi/toml, go.yaml.in/yaml/v3, and golang.org/x/text. That is a small surface to audit, and it is the main reason upgrade cost is low. The plural rule tables are generated from CLDR data, so a CLDR revision arrives as a new release rather than as something you regenerate yourself.

Licensing is MIT, stated in the README and present as a LICENSE file at the repository root. MIT is permissive and imposes no copyleft obligation on your application. This is a description of the licence text, not legal advice; if your organisation has a licence review process, the relevant file is LICENSE in the repository root.

The README does not document a deprecation policy or a support window for older minor versions, so pinning a version in go.mod and reading CHANGELOG.md before upgrading is the practical approach.

## Conclusion

Adopt go-i18n if your Go program ships message files alongside the binary and you want CLDR plural rules without a hosted translation service. Do not adopt it if you need a translation management UI, translator comments threaded through a web console, or runtime language packs fetched after deploy; goi18n is a file-based workflow and the README documents no server component. Before committing, verify that the plural categories your target locales need are covered by the generated CLDR data, and run goi18n merge on a scratch copy of your active files to see exactly which entries it rewrites.

## FAQ

### What does go-i18n do?

It is a Go package and a command that translate a Go program into multiple languages. The i18n package looks up messages for a set of locale preferences, and the goi18n command extracts and merges the message files those lookups read.

### How do I install the goi18n command?

The README gives go install -v github.com/nicksnyder/go-i18n/v2/goi18n@latest, followed by goi18n -help to list the subcommands. The library is a separate import, github.com/nicksnyder/go-i18n/v2/i18n.

### Which message file formats does go-i18n support?

The README says message files of any format are supported, and shows TOML, JSON and YAML as examples. Format support works because you register a decoder with bundle.RegisterUnmarshalFunc before loading a file.

### Does go-i18n handle plural forms?

Yes. The README states it supports pluralized strings for all 200+ languages in the Unicode CLDR, and that the plural rule code and tests are generated from CLDR data. You supply One and Other variants plus a PluralCount, and the localizer picks the right one.

### Is go-i18n a translation management platform?

No. It is a Go package plus the goi18n command, and the workflow is file-based: extract to active files, merge into translate files, translate, then merge back. The README documents no web interface or runtime translation service.

## Sources

- [Issues](https://github.com/nicksnyder/go-i18n/issues)
- [License: MIT](https://github.com/nicksnyder/go-i18n/blob/main/LICENSE)
- [nicksnyder/go-i18n on GitHub](https://github.com/nicksnyder/go-i18n)
- [README](https://github.com/nicksnyder/go-i18n/blob/main/README.md)
- [Releases](https://github.com/nicksnyder/go-i18n/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/nicksnyder-go-i18n
