Documentation
¶
Overview ¶
Package i18n provides GNU gettext-style internationalization for Go modules, designed around three ideas:
- Each module owns and embeds its own translations. Catalogs are plain .po files, committed to version control, so translation works for modules fetched through the Go module proxy.
- Translation lookups bind late. The locale travels in a context.Context (see WithLocale); catalog data is registered once per module (see Register) and resolved per call.
- The failure mode is always the source text. A missing locale, domain, catalog, or message returns the msgid unchanged — never an error, and never a crash of the host application.
The per-module pattern ¶
A module that ships translations has exactly one package owning them, conventionally internal/l10n, holding the po/ directory and nothing else:
package l10n
import (
"embed"
"gitlab.com/zygoon/go-i18n"
)
//go:embed po
var poFS embed.FS
const D i18n.Domain = "gitlab.com/zygoon/mymodule"
func init() { i18n.Register(D, poFS) }
Every other package in the module imports it and translates through the exported domain constant:
import "gitlab.com/zygoon/mymodule/internal/l10n"
func hello(ctx context.Context) string {
return l10n.D.Tr(ctx, "hello")
}
Because l10n imports nothing else in the module, import cycles are impossible; because registration happens in init, catalogs are available in any binary that links translating code, at any dependency depth, with no wiring in the application.
The application decides the locale, once:
func main() {
ctx := i18n.WithLocale(context.Background(), i18n.DetectLocale())
...
}
Translation files ¶
The po/ directory holds the extraction template (mymodule.pot, generated by "i18n scan") and one .po file per language (de.po, pt_BR.po, ...), created and updated with "i18n merge" and edited by translators. Both are text files, version controlled. There is no compile step: .po files are parsed at runtime, lazily, on the first lookup for each language.
Formatting composes with the standard library and is not part of this package's API:
fmt.Sprintf(l10n.D.Tr(ctx, "Connected as %s"), username)
Example ¶
package main
import (
"context"
"fmt"
"testing/fstest"
"gitlab.com/zygoon/go-i18n"
"golang.org/x/text/language"
)
// In a real module the catalogs live in internal/l10n/po and are embedded
// with //go:embed po; a fstest.MapFS stands in for embed.FS here.
var exampleFS = fstest.MapFS{
"po/de.po": &fstest.MapFile{Data: []byte(`msgid ""
msgstr ""
"Language: de\n"
msgid "Hello, world!"
msgstr "Hallo, Welt!"
msgid "%d file"
msgid_plural "%d files"
msgstr[0] "%d Datei"
msgstr[1] "%d Dateien"
msgctxt "verb"
msgid "Open"
msgstr "Öffnen"
`)},
}
func main() {
// Once per module, in the init of its internal/l10n package:
const d i18n.Domain = "example.com/mymodule"
i18n.Register(d, exampleFS)
// Once per program run (or per request, in a server):
ctx := i18n.WithLocale(context.Background(), language.German)
// Everywhere else:
fmt.Println(d.Tr(ctx, "Hello, world!"))
fmt.Println(fmt.Sprintf(d.TrN(ctx, "%d file", "%d files", 3), 3))
fmt.Println(d.TrCtx(ctx, "verb", "Open"))
}
Output: Hallo, Welt! 3 Dateien Öffnen
Index ¶
- func DetectLocale() language.Tag
- func FormatInt(ctx context.Context, v int64) string
- func LocaleFromContext(ctx context.Context) (language.Tag, bool)
- func MarkForTranslation(msgid string) string
- func MarkForTranslationCtx(msgctxt, msgid string) (string, string)
- func MarkForTranslationCtxPlural(msgctxt, msgid, msgidPlural string) (string, string, string)
- func MarkForTranslationPlural(msgid, msgidPlural string) (string, string)
- func Register(d Domain, fsys fs.FS)
- func SetLogger(l *slog.Logger)
- func WithLocale(ctx context.Context, tag language.Tag) context.Context
- type Domain
- func (d Domain) Bind(ctx context.Context) Translator
- func (d Domain) Errorf(format string, args ...any) error
- func (d Domain) LocalizedError(ctx context.Context, err error) string
- func (d Domain) Tr(ctx context.Context, msgid string) string
- func (d Domain) TrCtx(ctx context.Context, msgctxt, msgid string) string
- func (d Domain) TrCtxN(ctx context.Context, msgctxt, msgid, msgidPlural string, n int) string
- func (d Domain) TrN(ctx context.Context, msgid, msgidPlural string, n int) string
- func (d Domain) WrapError(err error) error
- type LocalizedError
- type Numbers
- type Translator
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DetectLocale ¶
DetectLocale reads the user's preferred locale using platform-specific methods:
- Linux: checks LC_ALL, LC_MESSAGES, and LANG environment variables (POSIX priority)
- Windows: uses GetUserDefaultLocaleName API
- macOS: reads AppleLocale from CoreFoundation preferences
Falls back to English if no locale is available. The result is cached after the first call.
func FormatInt ¶ added in v0.3.0
FormatInt renders v the way the locale carried by ctx writes numbers. It is NumbersFor(ctx).Int(v), for a single number.
Example ¶
package main
import (
"context"
"fmt"
"golang.org/x/text/language"
"gitlab.com/zygoon/go-i18n"
)
func main() {
ctx := i18n.WithLocale(context.Background(), language.German)
fmt.Println(i18n.FormatInt(ctx, 1234567))
}
Output: 1.234.567
func LocaleFromContext ¶
LocaleFromContext returns the locale set by WithLocale and whether one was set.
func MarkForTranslation ¶
MarkForTranslation marks a string as translatable without translating it at runtime. Use it when extraction and lookup happen in different places — for example when storing a message key that is translated later:
key := i18n.MarkForTranslation("hello")
...
msg := d.Tr(ctx, key)
This is equivalent to GNU gettext's gettext_noop or the N_ macro.
func MarkForTranslationCtx ¶ added in v0.2.0
MarkForTranslationCtx marks a msgctxt/msgid pair as translatable without translating it at runtime. Use it when extraction and lookup happen in different places — for example when storing a message key that is translated later:
msgctxt, key := i18n.MarkForTranslationCtx("Verb", "file")
...
msg := d.TrCtx(ctx, msgctxt, key)
This mirrors the pgettext_noop convention the GNU gettext manual documents for deferred context-qualified extraction (unlike gettext_noop, pgettext_noop is not a macro libintl itself ships).
func MarkForTranslationCtxPlural ¶ added in v0.2.0
MarkForTranslationCtxPlural marks a msgctxt/msgid/msgidPlural triple as translatable without translating it at runtime. Returns all three strings unchanged, suitable for later use with Domain.TrCtxN.
This mirrors the pgettext_noop convention with plural forms (see MarkForTranslationCtx on why it's a convention rather than a shipped libintl macro).
func MarkForTranslationPlural ¶
MarkForTranslationPlural marks a singular/plural pair as translatable without translating it at runtime. Returns both strings unchanged, suitable for later use with Domain.TrN.
This is equivalent to GNU gettext's gettext_noop with plural forms.
func Register ¶
Register associates a domain with a filesystem containing its .po translation catalogs, discovering every file whose base name parses as a language tag (de.po, pt_BR.po, ...). Catalogs are parsed lazily on first lookup; Register itself does not parse anything and never fails — a missing or malformed catalog degrades to untranslated output, logged via the package logger.
Register is meant to be called once, from the init function of the one package in a module that owns its translations:
package l10n
//go:embed po
var poFS embed.FS
const D i18n.Domain = "gitlab.com/zygoon/mymodule"
func init() { i18n.Register(D, poFS) }
Registering an already-registered domain replaces its catalogs and logs a warning.
func SetLogger ¶
SetLogger sets the logger used by this package for warnings and diagnostics. The logger is nil-safe: if nil is passed, the default logger (slog.Default()) is used. This is a package-level setting shared by all domains.
func WithLocale ¶
WithLocale returns a copy of ctx carrying tag as the locale for all Domain translation methods. Locale is per-request or per-run policy and is deliberately separate from catalog registration: set it once at the program's entry point and let it flow to every translating package.
In a CLI:
ctx := i18n.WithLocale(context.Background(), i18n.DetectLocale())
In a server, per request, from Accept-Language or user preferences:
ctx := i18n.WithLocale(r.Context(), tag)
A context without a locale translates nothing: every Domain method returns its source-text fallback.
Example ¶
package main
import (
"context"
"fmt"
"testing/fstest"
"gitlab.com/zygoon/go-i18n"
"golang.org/x/text/language"
)
// In a real module the catalogs live in internal/l10n/po and are embedded
// with //go:embed po; a fstest.MapFS stands in for embed.FS here.
var exampleFS = fstest.MapFS{
"po/de.po": &fstest.MapFile{Data: []byte(`msgid ""
msgstr ""
"Language: de\n"
msgid "Hello, world!"
msgstr "Hallo, Welt!"
msgid "%d file"
msgid_plural "%d files"
msgstr[0] "%d Datei"
msgstr[1] "%d Dateien"
msgctxt "verb"
msgid "Open"
msgstr "Öffnen"
`)},
}
func main() {
const d i18n.Domain = "example.com/withlocale"
i18n.Register(d, exampleFS)
// A context without a locale translates nothing: the source text comes
// back unchanged. This is the deliberate failure mode — visible, never
// the wrong language.
fmt.Println(d.Tr(context.Background(), "Hello, world!"))
// de-AT has no catalog of its own; the parent chain finds de.
ctx := i18n.WithLocale(context.Background(), language.MustParse("de-AT"))
fmt.Println(d.Tr(ctx, "Hello, world!"))
}
Output: Hello, world! Hallo, Welt!
Types ¶
type Domain ¶
type Domain string
Domain identifies a translation domain: one module's set of translations. By convention the value is the Go module path of the module that owns the translations, which makes it globally unique without coordination:
const D i18n.Domain = "gitlab.com/zygoon/mymodule"
A Domain is a lazy reference. Its methods look up translations registered for it via Register; a domain with no registered catalogs translates nothing and every method returns its source-text fallback. The locale is read from the context on every call (see WithLocale), so translation binds as late as possible: per request in a server, once in main for a CLI.
func (Domain) Bind ¶
func (d Domain) Bind(ctx context.Context) Translator
Bind returns a Translator for d and the locale carried by ctx. When ctx carries no locale, the domain is unregistered, or no catalog matches, the Translator translates nothing and every method returns its source-text fallback — the same degradation as the Domain methods.
Example ¶
package main
import (
"context"
"fmt"
"testing/fstest"
"gitlab.com/zygoon/go-i18n"
"golang.org/x/text/language"
)
// In a real module the catalogs live in internal/l10n/po and are embedded
// with //go:embed po; a fstest.MapFS stands in for embed.FS here.
var exampleFS = fstest.MapFS{
"po/de.po": &fstest.MapFile{Data: []byte(`msgid ""
msgstr ""
"Language: de\n"
msgid "Hello, world!"
msgstr "Hallo, Welt!"
msgid "%d file"
msgid_plural "%d files"
msgstr[0] "%d Datei"
msgstr[1] "%d Dateien"
msgctxt "verb"
msgid "Open"
msgstr "Öffnen"
`)},
}
func main() {
const d i18n.Domain = "example.com/bind"
i18n.Register(d, exampleFS)
ctx := i18n.WithLocale(context.Background(), language.German)
// Bind resolves the catalog once; each call is then a single lookup.
// Use it where many messages render against the same context — a
// request handler, a template loop, a CLI bound in main.
tr := d.Bind(ctx)
fmt.Println(tr.Tr("Hello, world!"))
fmt.Println(fmt.Sprintf(tr.TrN("%d file", "%d files", 3), 3))
}
Output: Hallo, Welt! 3 Dateien
func (Domain) Errorf ¶
Errorf returns an error like fmt.Errorf whose message can additionally render translated: Error() is the plain fmt.Errorf text (source language, locale-stable for logs and tests), while LocalizedError translates format as a msgid in d's catalog and localizes any error-typed arguments recursively. %w wrapping works exactly as in fmt.Errorf, so errors.Is and errors.As see the wrapped chain:
return l10n.D.Errorf("merging %s: %w", path, err)
The i18n scan tool extracts the format string of Errorf calls into the catalog template. func(string, ...any) error is this recognition's second structural shape (Translator's Tr/TrN methods are the first, see Translator): any function or method with this signature that forwards its format argument to a translator-shaped sink — directly or through another recognized wrapper — is extracted the same way, without needing to import go-i18n or declare a Domain constant. Changing this signature is a breaking change for exactly this reason.
func (Domain) LocalizedError ¶
LocalizedError renders err for the locale carried by ctx, best effort:
- An error implementing LocalizedError anywhere in its chain renders itself.
- Known standard library error shapes (*fs.PathError, *os.LinkError, *os.SyscallError, *strconv.NumError and a few others) keep stdlib's own wrapper text shape while localizing the leaf error text.
- The full error text is looked up as a msgid: first in d's catalog (letting applications translate their own and third-party sentinel errors by adding the text to their catalog), then in go-i18n's built-in table of standard library error strings — the Go analogue of libc's translated strerror set. When that misses and the chain holds a syscall.Errno, its canonical POSIX text gets one more lookup, so Windows renderings ("The system cannot find the file specified.") translate through the same catalogs as their Unix counterparts.
Error wrapping concatenates text, so when a localizable error sits inside a wrapped chain its localized rendering substitutes for its source text within the full message — fmt.Errorf("loading config: %w", pathErr) keeps the "loading config: " prefix and localizes the rest.
Anything else returns err.Error() unchanged: like every translation path in this package it degrades to the source text, never fails. A nil err returns the empty string.
func (Domain) Tr ¶
Tr returns the translation of msgid for the locale carried by ctx. If ctx carries no locale, the domain is unregistered, or the message is untranslated, it returns msgid unchanged.
func (Domain) TrCtx ¶
TrCtx returns the translation of msgid within msgctxt for the locale carried by ctx. The context disambiguates identical msgids (e.g. "Open" as a verb vs. an adjective). On any miss it returns msgid unchanged, never a combination of msgctxt and msgid (pgettext semantics).
func (Domain) TrCtxN ¶
TrCtxN returns the plural translation of msgid/msgidPlural within msgctxt selected by n for the locale carried by ctx. It combines the semantics of Domain.TrCtx and Domain.TrN.
func (Domain) TrN ¶
TrN returns the plural translation of msgid/msgidPlural selected by n for the locale carried by ctx. The plural form is chosen by the CLDR rules of the matched catalog's language. Without a translation it falls back to source-language selection: msgid when n == 1, msgidPlural otherwise.
Example ¶
package main
import (
"context"
"fmt"
"testing/fstest"
"gitlab.com/zygoon/go-i18n"
"golang.org/x/text/language"
)
// In a real module the catalogs live in internal/l10n/po and are embedded
// with //go:embed po; a fstest.MapFS stands in for embed.FS here.
var exampleFS = fstest.MapFS{
"po/de.po": &fstest.MapFile{Data: []byte(`msgid ""
msgstr ""
"Language: de\n"
msgid "Hello, world!"
msgstr "Hallo, Welt!"
msgid "%d file"
msgid_plural "%d files"
msgstr[0] "%d Datei"
msgstr[1] "%d Dateien"
msgctxt "verb"
msgid "Open"
msgstr "Öffnen"
`)},
}
func main() {
const d i18n.Domain = "example.com/trn"
i18n.Register(d, exampleFS)
ctx := i18n.WithLocale(context.Background(), language.German)
// The plural form is selected by the CLDR rules of the matched catalog
// language. Untranslated messages fall back to source-language rules.
for _, n := range []int{1, 2} {
fmt.Println(fmt.Sprintf(d.TrN(ctx, "%d file", "%d files", n), n))
}
fmt.Println(d.TrN(ctx, "one goose", "many geese", 2))
}
Output: 1 Datei 2 Dateien many geese
func (Domain) WrapError ¶
WrapError returns an error that renders through Domain.LocalizedError at display time while leaving identity intact: Error() and the errors.Is chain are those of err, unchanged. It is the bridge to display boundaries that detect the LocalizedError interface structurally (such as go-cmdr's error printer) but cannot know the domain: wrapping once at the top of a program localizes every error that reaches the boundary, including plain standard library errors.
A nil err returns nil, and an err that already implements LocalizedError is returned as-is.
type LocalizedError ¶
LocalizedError is the interface of errors that can render themselves for the locale carried by ctx. Error() keeps returning the source-language text — logs, tests and errors.Is stay locale-stable — while display boundaries call LocalizedError for the user-facing message.
The interface is deliberately tiny so that boundaries outside this module (a CLI framework's error printer, an HTTP error handler) can detect it structurally, without importing go-i18n.
type Numbers ¶ added in v0.3.0
type Numbers struct {
// contains filtered or unexported fields
}
Numbers renders numbers the way one locale writes them: its decimal and group separators, its minus sign, its grouping sizes (three and three for 1,234,567; three and two for the Indian 12,34,567) and its minimum grouping digits (Polish writes 1234 but 12 345). The data is CLDR's.
Digits are always Latin, even for a locale whose default numbering system is another; a -u-nu- extension on the locale is ignored.
The zero value renders numbers like CLDR's root locale, which writes them the way English does: 1,234,567.
func NumbersFor ¶ added in v0.3.0
NumbersFor returns the Numbers of the locale carried by ctx. Like Domain.Bind for translation, it resolves the locale once, for call sites that render many numbers.
A locale with no number data of its own takes that of the nearest locale on its parent chain (pl-PL uses pl, es-AR uses es-419). A context without a locale, or a locale CLDR does not know, gets the zero Numbers. Which catalogs are registered plays no part: numbers follow the reader's locale even where no translation does.
Example ¶
package main
import (
"context"
"fmt"
"golang.org/x/text/language"
"gitlab.com/zygoon/go-i18n"
)
func main() {
for _, locale := range []string{"en", "de", "en-IN"} {
ctx := i18n.WithLocale(context.Background(), language.Make(locale))
n := i18n.NumbersFor(ctx)
fmt.Println(locale, n.Int(123456789), n.Fixed(12345, 1))
}
}
Output: en 123,456,789 1,234.5 de 123.456.789 1.234,5 en-IN 12,34,56,789 1,234.5
func (Numbers) Fixed ¶ added in v0.3.0
Fixed renders v divided by ten to the power of places, with exactly that many digits after the decimal separator: Fixed(13, 1) is 1.3 in English and 1,3 in Polish. Taking a scaled integer rather than a float keeps the rounding where the caller decided it. A negative places counts as zero.
type Translator ¶
type Translator struct {
// contains filtered or unexported fields
}
Translator is a Domain bound to the locale of one context. Binding resolves the catalog once — context value, registry lookup, language matching — so each call is a single map lookup, where the Domain methods repeat the resolution every time. Use it where many messages translate against the same context: a rendering loop, a request handler, a CLI that bound its locale in main.
A Translator is an immutable value, cheap to copy and safe for concurrent use. It deliberately freezes the locale policy of the moment it was created: it does not observe later context changes or domain re-registration. Locale still lives in ctx — bind at the same points where the context itself is scoped (per request, per run), not globally.
The method set matches translator interfaces of consuming frameworks (e.g. go-cmdr's Translator), so a bound domain plugs in directly:
cmdr.RunMain(cmd, os.Args, cmdr.WithTranslator(d.Bind(ctx)), ...)
Tr and TrN's signatures — func(string) string and func(string, string, int) string — are a stable, structural contract: the i18n scan tool recognizes any type with this method set as translator-shaped, the same way it recognizes the Domain methods, without requiring that type to import go-i18n at all (this is how frameworks like go-cmdr and go-make plug in their own local Translator-shaped proxy). Changing either signature is a breaking change for exactly this reason, independent of Translator's own compatibility promise as a struct.
func BindAll ¶
func BindAll(ctx context.Context, domains ...Domain) Translator
BindAll returns a Translator chaining several domains: each lookup tries the domains' catalogs in order and the first translation wins. Put the application's domain first so its catalog can override the others.
Chaining is how a program hands one Translator to a framework while covering both its own strings and those of libraries that ship their catalogs as data:
i18n.Register(cmdrl10n.Domain, cmdrl10n.Catalogs()) tr := i18n.BindAll(ctx, l10n.D, cmdrl10n.Domain)
func (Translator) Tr ¶
func (t Translator) Tr(msgid string) string
Tr returns the translation of msgid, or msgid unchanged when untranslated.
func (Translator) TrCtx ¶
func (t Translator) TrCtx(msgctxt, msgid string) string
TrCtx returns the translation of msgid within msgctxt. On any miss it returns msgid unchanged, never a combination of msgctxt and msgid (pgettext semantics).
func (Translator) TrCtxN ¶
func (t Translator) TrCtxN(msgctxt, msgid, msgidPlural string, n int) string
TrCtxN returns the plural translation of msgid/msgidPlural within msgctxt selected by n. It combines the semantics of Translator.TrCtx and Translator.TrN.
func (Translator) TrN ¶
func (t Translator) TrN(msgid, msgidPlural string, n int) string
TrN returns the plural translation of msgid/msgidPlural selected by n, using the CLDR rules of the bound catalog's language. Without a translation it falls back to source-language selection: msgid when n == 1, msgidPlural otherwise.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
i18n
module
|
|
|
Package i18ntest provides assertions for testing embedded PO catalogs.
|
Package i18ntest provides assertions for testing embedded PO catalogs. |
|
internal
|
|
|
check
Package check contains shared PO/POT verification rules used by the CLI and by the public i18ntest package.
|
Package check contains shared PO/POT verification rules used by the CLI and by the public i18ntest package. |
|
gettext
Package gettext provides types and utilities for working with GNU gettext text catalog formats (PO, POT).
|
Package gettext provides types and utilities for working with GNU gettext text catalog formats (PO, POT). |
|
merge
Package merge provides functionality for merging PO translation files with POT templates.
|
Package merge provides functionality for merging PO translation files with POT templates. |
|
number
Package number renders integers and fixed-point numbers the way a locale writes them: its decimal and group separators, its minus sign, its grouping sizes and its minimum grouping digits, all taken from the CLDR data in tables_gen.go.
|
Package number renders integers and fixed-point numbers the way a locale writes them: its decimal and group separators, its minus sign, its grouping sizes and its minimum grouping digits, all taken from the CLDR data in tables_gen.go. |
|
plural
Package plural implements plural form selection rules for various languages as defined by the CLDR plural rules.
|
Package plural implements plural form selection rules for various languages as defined by the CLDR plural rules. |
|
scan
Package scan walks Go source files and extracts translatable strings by looking for calls to translation functions and methods (Tr, TrN, TrCtx, TrCtxN, Errorf, MarkForTranslation, MarkForTranslationPlural, MarkForTranslationCtx, MarkForTranslationCtxPlural).
|
Package scan walks Go source files and extracts translatable strings by looking for calls to translation functions and methods (Tr, TrN, TrCtx, TrCtxN, Errorf, MarkForTranslation, MarkForTranslationPlural, MarkForTranslationCtx, MarkForTranslationCtxPlural). |
|
scananalysis
Package scananalysis finds functions that forward their arguments to a go-i18n translation sink — go-cmdr's free Tr(ctx, msgid) forwarding through a local Translator interface, or go-make's free Errorf(format, args...) forwarding to fmt.Errorf — without either consumer importing go-i18n at all.
|
Package scananalysis finds functions that forward their arguments to a go-i18n translation sink — go-cmdr's free Tr(ctx, msgid) forwarding through a local Translator interface, or go-make's free Errorf(format, args...) forwarding to fmt.Errorf — without either consumer importing go-i18n at all. |