i18n

package module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2026 License: Apache-2.0 Imports: 20 Imported by: 10

README

go-i18n

GNU gettext-style internationalization for Go modules.

Each module owns and embeds its own translations as plain .po files, committed to version control, so everything works for modules fetched through the Go module proxy.

Lookups bind late: locale lives in context.Context, catalogs register themselves at init, and every failure degrades to source text (msgid), never an error.

msg := l10n.D.Tr(ctx, "Hello, world!")

Why it is shaped this way is in DESIGN.md. Implementation details are in ARCHITECTURE.md.

Design properties

  • No compile step for catalogs: runtime uses committed .po files.
  • Proxy-safe distribution: works with normal Go module proxy fetches.
  • Predictable failure mode: missing locale/catalog/entry returns msgid.
  • Small runtime API: one Domain, one context locale policy.

Start here in 60 seconds

  1. Add the CLI as a module tool dependency.
go get -tool gitlab.com/zygoon/go-i18n/cmd/i18n

Then run it through go tool i18n from that module.

  1. Create one translation owner package in your module, usually internal/l10n.
// internal/l10n/l10n.go
package l10n

import (
	"embed"

	"gitlab.com/zygoon/go-i18n"
)

//go:embed po
var poFS embed.FS

// D must match your module path in go.mod.
const D i18n.Domain = "example.com/mymodule"

func init() { i18n.Register(D, poFS) }
  1. Translate from anywhere in the module.
import "example.com/mymodule/internal/l10n"

func greet(ctx context.Context) string {
	return l10n.D.Tr(ctx, "Hello, world!")
}
  1. Set locale at your entry point (or per request in middleware).
func main() {
	ctx := i18n.WithLocale(context.Background(), i18n.DetectLocale())
	...
}
  1. Run the translation workflow.
go tool i18n scan
go tool i18n merge
go tool i18n check

If locale is missing from context, translations intentionally fall back to source text so the problem is visible.

Choose your integration mode

Application or binary module (hard dependency)

Use internal/l10n and import go-i18n directly as shown above.

Reusable library (soft dependency)

Ship catalogs as plain data without importing go-i18n, so callers are not forced to link this library.

// package l10n in your library, no go-i18n import
package l10n

import (
	"embed"
	"io/fs"
)

//go:embed po
var poFS embed.FS

const Domain = "example.com/mylib"

func Catalogs() fs.FS { return poFS }

The embedding application registers both domains and can bind one translator across them:

i18n.Register(l10n.Domain, l10n.Catalogs())
ctx := i18n.WithLocale(context.Background(), i18n.DetectLocale())
tr := i18n.BindAll(ctx, appL10n.D, l10n.Domain)
mylib.WithTranslator(tr)

See the real wiring in cmd/i18n/main.go.

API quick reference

l10n.D.Tr(ctx, "Hello")                             // simple
l10n.D.TrN(ctx, "%d file", "%d files", n)         // plural (CLDR rules)
l10n.D.TrCtx(ctx, "verb", "Open")                 // context disambiguation
l10n.D.TrCtxN(ctx, "menu", "entry", "entries", n) // context + plural

Formatting composes with the standard library:

fmt.Sprintf(l10n.D.Tr(ctx, "Connected as %s"), username)

Numbers are written the way the context's locale writes them (CLDR data: separators, grouping, minimum grouping digits):

i18n.FormatInt(ctx, 1234567) // 1,234,567 (en), 1 234 567 (pl), 12,34,567 (hi)

n := i18n.NumbersFor(ctx)    // resolve once for a rendering loop
n.Fixed(13, 1)               // 1.3 (en), 1,3 (pl)

Mark for extraction without translating yet (gettext_noop):

key := i18n.MarkForTranslation("pending")

Translation workflow

go tool i18n scan      # extract strings -> po/<module-name>.pot (from go.mod)
go tool i18n merge     # create/update po/<lang>.po, preserving existing work
# ... translators edit .po files ...
go tool i18n check     # fail on domain mismatch, missing, or fuzzy entries

There is no catalog compile step. .po files are both translator artifact and runtime data, parsed lazily on first lookup for each language.

New strings appear after merge. Renamed strings may be fuzzy-matched and marked #, fuzzy; fuzzy entries are not served at runtime and fail i18n check until resolved.

To start a new language, create an empty po/<lang>.po (for example po/pl.po) and run merge. It fills in the header: the charset, Language taken from the file name, and a Plural-Forms that matches the runtime's CLDR plural rules. Header fields already in the file are left alone.

Strings removed from the source stay behind as #~ obsolete entries, so their translations can come back if the string returns. Run go tool i18n merge -no-obsolete to drop them.

Extraction features

i18n scan does more than grep for string literals:

  • Recognizes by bare name: Tr, TrN, TrCtx, TrCtxN, Errorf, MarkForTranslation/MarkForTranslationPlural/MarkForTranslationCtx/ MarkForTranslationCtxPlural are matched by name alone, so a package that defines its own no-import wrapper functions around them (Tr(msgid string) string { return D.Tr(ctx, msgid) }) still gets extracted — the mechanism reusable libraries use to ship translatable strings without a hard go-i18n dependency (see "Reusable library" above).
  • Const-value resolution: when the scanned module resolves via go/packages (the common case — anything with a go.mod), a msgid/msgctxt argument may be a named const identifier instead of a literal: MarkForTranslation(myFmt) with const myFmt = "%s: %s" extracts exactly as the inline literal would. This lets a msgid repeated across call sites share one Go source declaration instead of a copy pasted into every call. A var, or any other non-constant expression, still isn't resolvable and warns.
  • Errorf wrapper tracing across packages: beyond the direct i18n.Domain.Errorf receiver check, the scanner traces free functions and methods anywhere in the scanned module that forward their format argument to a recognized Errorf-shaped sink, and extracts calls to those too — so a package-local Errorf(format string, args ...any) error wrapper living in a different package of the same module is picked up transitively, without that package importing go-i18n.
  • Implicit c-format detection: Tr/TrN/TrCtx/TrCtxN results handed directly, or through one level of variable indirection, to fmt.Sprintf/Printf/Fprintf are marked c-format automatically — no // c-format comment required for that case.
  • // c-format / // sh-format and translator comments: comments in the five lines above a recognized call are attached to the extracted msgid.
  • Non-literal call warnings: an argument that isn't a literal (or a resolvable const) is warned about and dropped rather than silently mistranslated; so is a string literal sitting in the leading context.Context position of a Tr-family call, a common copy-paste mistake.
  • Multi-GOOS scanning: i18n scan -goos windows,plan9 loads and scans additional platform builds beyond the host's own default, so platform-gated strings aren't missed by a single-OS CI run.

Const-value resolution and cross-package Errorf wrapper tracing both need type information, so they're only available when the scanned root resolves as a real Go module; a bare directory outside any module falls back to name-only, literal-only syntactic scanning.

CLI path detection details

Without -C, CLI commands climb to nearest go.mod, then auto-target:

  • internal/l10n/po/ for hard-dependency layout.
  • l10n/po/ for soft-dependency layout.

If both exist, CLI errors instead of guessing. Use -C to disambiguate or override auto-detection.

Build tags and multi-OS scanning

scan only sees files visible to the selected GOOS set. If CI scans on Linux, Windows-only strings are missed unless you include Windows.

go tool i18n scan -goos windows,plan9

This scans host-visible files plus those GOOS targets in one pass.

Locale behavior

  • WithLocale(ctx, tag) sets locale policy in context.
  • DetectLocale() reads OS locale (LC_ALL/LC_MESSAGES/LANG on POSIX, platform APIs on macOS and Windows).
  • Matching uses exact tag, then parent chain (de-AT -> de).
  • Any miss returns msgid.
  • TrN fallback uses source-language plural choice (n == 1 ? msgid : msgidPlural).

Common pitfalls

  1. Everything stays in source language: Locale was not set in context. Call WithLocale(...) before translation.
  2. i18n check fails on fuzzy: Translator must confirm and remove fuzzy markers.
  3. Some strings never reach POT: Scanner only extracts recognized call shapes and visible build-tag files.
  4. Domain mismatch failure: Ensure const D i18n.Domain equals the module path from go.mod.

CI suggestions

For a module using go-i18n:

go vet ./...
go test ./...
go tool i18n check

For this repository specifically (library module + CLI module):

# repo root
go vet ./...
go test ./...

# cmd/i18n module
cd cmd/i18n
go vet ./...
go test ./...
go tool i18n check -C .

Gettext interoperability

.pot and .po are standard and work with existing translation tools (Poedit, Weblate, and others).

There is no .mo support. .mo files are build artifacts, and workflows that depend on generating them are incompatible with normal Go module proxy usage (proxy-fetched modules contain committed files only, and module cache is read-only).

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

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func DetectLocale

func DetectLocale() language.Tag

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

func FormatInt(ctx context.Context, v int64) string

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

func LocaleFromContext(ctx context.Context) (language.Tag, bool)

LocaleFromContext returns the locale set by WithLocale and whether one was set.

func MarkForTranslation

func MarkForTranslation(msgid string) string

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

func MarkForTranslationCtx(msgctxt, msgid string) (string, string)

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

func MarkForTranslationCtxPlural(msgctxt, msgid, msgidPlural string) (string, string, string)

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

func MarkForTranslationPlural(msgid, msgidPlural string) (string, string)

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

func Register(d Domain, fsys fs.FS)

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

func SetLogger(l *slog.Logger)

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

func WithLocale(ctx context.Context, tag language.Tag) context.Context

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

func (d Domain) Errorf(format string, args ...any) error

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

func (d Domain) LocalizedError(ctx context.Context, err error) string

LocalizedError renders err for the locale carried by ctx, best effort:

  1. An error implementing LocalizedError anywhere in its chain renders itself.
  2. 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.
  3. 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

func (d Domain) Tr(ctx context.Context, msgid string) string

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

func (d Domain) TrCtx(ctx context.Context, msgctxt, msgid string) string

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

func (d Domain) TrCtxN(ctx context.Context, msgctxt, msgid, msgidPlural string, n int) string

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

func (d Domain) TrN(ctx context.Context, msgid, msgidPlural string, n int) string

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

func (d Domain) WrapError(err error) error

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

type LocalizedError interface {
	error
	LocalizedError(ctx context.Context) string
}

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

func NumbersFor(ctx context.Context) Numbers

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

func (n Numbers) Fixed(v int64, places int) string

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.

func (Numbers) Int added in v0.3.0

func (n Numbers) Int(v int64) string

Int renders v with its digits grouped: 1234567 is 1,234,567 in English and 1 234 567 in Polish.

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.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL