A Go internationalization library with ICU MessageFormat support, deterministic fallbacks, and optional net/http locale detection
For development guidelines, see AGENTS.md. For internal contracts and design rules, start with SPECS/00-overview.md.
- ICU MessageFormat: Use plural, select, and ordinal formatting through
github.com/kaptinlin/messageformat-go/v1. - Flexible loading: Load translations from maps, files, glob patterns, or embedded filesystems.
- Deterministic fallbacks: Root fallback chains in the configured default locale.
- Lookup details: Use
Lookupto get the rendered text, resolved locale, and result source. - Text and token keys: Use token keys like
hello_worldor literal text keys withGetXcontext disambiguation. - HTTP integration: Detect locales from query, cookie, header, or
Accept-Language, then inject a request-scoped localizer. - Custom unmarshalers: Keep JSON as the default, or plug in YAML, TOML, or INI parsing.
Requires Go 1.26+.
go get github.com/kaptinlin/go-i18n@latestpackage main
import (
"fmt"
"log"
"github.com/kaptinlin/go-i18n"
)
func main() {
bundle := i18n.NewBundle(
i18n.WithDefaultLocale("en"),
i18n.WithLocales("en", "zh-Hans"),
)
err := bundle.LoadMessages(map[string]map[string]string{
"en": {"hello": "Hello, {name}!"},
"zh-Hans": {"hello": "你好,{name}!"},
})
if err != nil {
log.Fatal(err)
}
localizer := bundle.NewLocalizer("zh-CN")
fmt.Println(localizer.Get("hello", i18n.Vars{"name": "World"}))
}NewBundleconstructs the shared translation bundle.WithDefaultLocale,WithLocales,WithFallback,WithUnmarshaler,WithMessageFormatOptions,WithCustomFormatters, andWithStrictModeconfigure bundle behavior.NewLocalizerpicks the first matching locale from its arguments, then falls back to the default locale.SupportedLocalesandIsLanguageSupportedexpose the configured locale matcher state.
Use the loader that matches your source of truth:
LoadMessagesfor in-memory mapsLoadFilesfor explicit filesLoadGlobfor file pattern expansionLoadFSforgo:embedand otherfs.FSimplementations
Translation file names are normalized to locales, so names like zh_Hans.json and zh-Hans.user.json still resolve to zh-Hans.
Use Get for the normal translation path, GetX for context-disambiguated text keys, and Format for dynamic messages that are not stored in translation files.
localizer := bundle.NewLocalizer("zh-Hans")
fmt.Println(localizer.Get("hello", i18n.Vars{"name": "Lin"}))
fmt.Println(localizer.GetX("Post", "verb"))Use Lookup when you need to know where a translation came from.
result := localizer.Lookup("hello", i18n.Vars{"name": "Lin"})
fmt.Println(result.Text)
fmt.Println(result.Locale)
fmt.Println(result.Source)TranslationSource reports one of direct, fallback, or missing.
Use Has and Keys for direct locale contents only. They do not include inherited fallback keys.
Use NewDetector to resolve the best locale from HTTP request inputs.
detector := i18n.NewDetector(
bundle,
i18n.WithDetectorPriority(
i18n.DetectorSourceQuery,
i18n.DetectorSourceCookie,
i18n.DetectorSourceHeader,
i18n.DetectorSourceAccept,
),
)
locale := detector.DetectLocale(r)
localizer := bundle.NewLocalizer(locale)Use WithDetectorQueryParam, WithDetectorCookieName, and WithDetectorHeaderName to customize source names.
Use MatchAvailableLocale when you only need Accept-Language matching.
Use the optional middleware package to attach a request-scoped localizer to the request context.
package main
import (
"fmt"
"net/http"
"github.com/kaptinlin/go-i18n"
"github.com/kaptinlin/go-i18n/middleware"
)
func main() {
bundle := i18n.NewBundle(i18n.WithDefaultLocale("en"), i18n.WithLocales("en", "zh-Hans"))
_ = bundle.LoadMessages(map[string]map[string]string{
"en": {"hello": "Hello"},
})
handler := middleware.HTTPMiddleware(bundle)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
localizer, ok := i18n.LocalizerFromContext(r.Context())
if !ok {
http.Error(w, "missing localizer", http.StatusInternalServerError)
return
}
fmt.Fprint(w, localizer.Get("hello"))
}))
http.ListenAndServe(":8080", handler)
}JSON is the default. Override it when your translation files use another format.
import "github.com/goccy/go-yaml"
bundle := i18n.NewBundle(
i18n.WithDefaultLocale("en"),
i18n.WithLocales("en", "zh-Hans"),
i18n.WithUnmarshaler(yaml.Unmarshal),
)See examples/yml, examples/toml, and examples/ini for complete loaders.
Run the package examples directly:
go run ./examples/basic
go run ./examples/files
go run ./examples/embed
go run ./examples/glob
go run ./examples/icu
go run ./examples/source_locale
go run ./examples/textAdditional examples cover nested files and custom unmarshalers under examples/.
See pkg.go.dev/github.com/kaptinlin/go-i18n for the exported API.
Run the project commands from the repository root:
task test
task test-coverage
task bench
task fmt
task vet
task lint
task verify- Keep examples runnable and user-facing.
- Run
task fmt,task vet,task lint,task testbefore shipping changes. - Keep
README.md,CLAUDE.md, and SPECS/00-overview.md aligned when public behavior changes.
This project is licensed under the MIT License - see the LICENSE file for details.