Skip to content

Advanced Usage

This guide covers the more advanced features of gotext: embedding translation files in your binary, named formatting, serialization for caching, language fallback behavior, and custom translation backends.

1. Embedding Files with embed.FS

Instead of loading .po/.mo files from disk, you can read them from an in-memory filesystem.

NewLocaleFS(lang string, filesystem fs.FS) creates a Locale whose translation files live at the root of the given filesystem:

package main

import (
    "embed"
    "fmt"

    "github.com/leonelquinteros/gotext"
)

//go:embed locales
var localeFS embed.FS

func main() {
    l := gotext.NewLocaleFS("en_US", localeFS)
    l.AddDomain("default")

    fmt.Println(l.Get("Hello, world!"))
}

NewLocaleFSWithPath(lang string, filesystem fs.FS, path string) works the same but places the locale files under a folder inside the filesystem — useful when the language directories are nested below the embed root:

//go:embed fixtures
var fixtureFS embed.FS

func main() {
    // Files live under fixtures/en_US/LC_MESSAGES/default.po
    l := gotext.NewLocaleFSWithPath("en_US", fixtureFS, "fixtures")
    l.AddDomain("default")
    fmt.Println(l.Get("Hello, world!"))
}

NewPoFS and NewMoFS provide the same filesystem support for standalone Po/Mo objects.

2. Named Formatting

The Get/GetN/GetC family inserts variables using standard fmt syntax (%s, %d, ...). If you prefer named placeholders, use the Sprintf helper with %(name)s style arguments:

fmt.Println(gotext.Sprintf("%(name)s is Type %(type)s", map[string]any{
    "name": "Gotext",
    "type": "struct",
}))
// Output: Gotext is Type struct

NPrintf is the same helper writing to standard output. Since translation messages go through the exact same formatting pipeline, you can combine both:

msg := gotext.Get("Hello %(name)s!")
fmt.Println(gotext.Sprintf(msg, map[string]any{"name": "Gopher"}))

3. Serialization for Caching

Locale objects implement the encoding.BinaryMarshaler/encoding.BinaryUnmarshaler interfaces (MarshalBinary/UnmarshalBinary). This lets you parse translation files once at build time and load the serialized result at runtime — useful in servers or CLIs where reading and parsing many files on every startup is too slow.

// Build/cache step: serialize once
l := gotext.NewLocale("locales", "en_US")
l.AddDomain("default")

data, err := l.MarshalBinary()
if err != nil {
    log.Fatal(err)
}

// Runtime step: restore from bytes
restored := new(gotext.Locale)
if err := restored.UnmarshalBinary(data); err != nil {
    log.Fatal(err)
}

fmt.Println(restored.Get("Hello, world!"))

The serialized bytes are self-contained (they include all parsed domains and translations), so they can be stored anywhere — a file, a database, a shared cache.

To use manually built Locale objects at the package level — for example, built from in-memory Po objects or from an embed.FS — replace the package configuration with SetLocales:

l := gotext.NewLocaleFS("de_DE", localeFS)
l.AddDomain("default")
gotext.SetLocales([]*gotext.Locale{l})

SetLocales derives the package-level language, library path, and domain from the first locale. SetStorage is a deprecated alias for a single locale.

4. Language and Domain Fallback

Simplified Locales

Locale codes are normalized automatically: en_US.UTF-8, en_US@euro, and en_US are all equivalent. Use SimplifiedLocale directly if you need the normalized form.

Multiple Languages

You can supply a colon-separated list of languages (package level) or configure several languages for the application. gotext returns the first language that provides a translation:

gotext.Configure("locales", "es_UY:es:en_US", "default")

// Falls back es_UY -> es -> en_US in that order
fmt.Println(gotext.Get("Hello, world!"))

Language Simplification

If a file for the full locale code (e.g., es_UY) is missing, a file for the base language (e.g., es) is used instead. GetActualLanguage reports which language code the filesystem actually resolved. Files may live under an LC_MESSAGES subdirectory or directly under the language code — both layouts are supported.

Domain Fallback

Domain-level fallback follows the same idea: when a domain is missing for a given key, the message ID itself is returned, formatted with the provided variables.

5. Custom Translation Backends

The Translator interface defines what a translation source must provide:

type Translator interface {
    ParseFile(f string)
    Parse(buf []byte)
    Get(str string, vars ...any) string
    GetN(str, plural string, n int, vars ...any) string
    GetC(str, ctx string, vars ...any) string
    GetNC(str, plural string, n int, ctx string, vars ...any) string
    MarshalBinary() ([]byte, error)
    UnmarshalBinary([]byte) error
    GetDomain() *Domain
}

You can implement this interface to plug in a different translation backend (e.g., a remote service or a database). Register your implementation on a Locale with AddTranslator(domain, translator) instead of AddDomain:

l := gotext.NewLocale("", "fr_FR")
l.AddTranslator("default", myCustomTranslator)
fmt.Println(l.Get("Hello, world!"))

The AppendTranslator interface extends Translator with Append/AppendN/AppendC/AppendNC buffer builders, used to stream serialized output.

In-Memory PO Manipulation

For tests or programmatic use, Po objects can be built entirely in memory:

po := gotext.NewPo()
po.Parse([]byte(`msgid "Hello, world!"
msgstr "Hola, mundo!"`))
fmt.Println(po.Get("Hello, world!"))

Set, SetN, SetC, and SetNC add or overwrite translations directly, and MarshalText produces a PO-format text representation for round-tripping. DropStaleTranslations, SetRefs, and GetRefs help maintain translation files generated by xgotext.