cli/docs/pkg/i18n
Snider e9be13bcd3 docs(i18n): add comprehensive package documentation
- README.md: Quick start, all features, configuration options
- GRAMMAR.md: Verb conjugation, pluralisation, articles, templates
- EXTENDING.md: Custom loaders, handlers, framework integration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:18:43 +00:00
..
EXTENDING.md docs(i18n): add comprehensive package documentation 2026-01-30 19:18:43 +00:00
GRAMMAR.md docs(i18n): add comprehensive package documentation 2026-01-30 19:18:43 +00:00
README.md docs(i18n): add comprehensive package documentation 2026-01-30 19:18:43 +00:00

i18n Package

The pkg/i18n package provides internationalisation and localisation for Go CLI applications. It features a grammar engine for automatic verb conjugation and noun pluralisation, CLDR plural support, and an extensible handler chain for dynamic key patterns.

Quick Start

import "github.com/host-uk/core/pkg/i18n"

func main() {
    // Initialise with embedded locales
    svc, err := i18n.New()
    if err != nil {
        log.Fatal(err)
    }
    i18n.SetDefault(svc)

    // Translate messages
    fmt.Println(i18n.T("cli.success"))                    // "Operation completed"
    fmt.Println(i18n.T("i18n.count.file", 5))             // "5 files"
    fmt.Println(i18n.T("i18n.progress.build"))            // "Building..."
    fmt.Println(i18n.T("i18n.done.delete", "config.yaml")) // "Config.yaml deleted"
}

Table of Contents

Basic Translation

The T() function translates message keys:

// Simple translation
msg := i18n.T("cli.success")

// With template variables
msg := i18n.T("error.not_found", map[string]any{
    "Name": "config.yaml",
})

Use Raw() to bypass magic namespace handling:

// T() handles i18n.* magic
i18n.T("i18n.label.status")  // "Status:"

// Raw() does direct lookup only
i18n.Raw("i18n.label.status") // Returns key as-is (not in JSON)

Template Variables

Translation strings support Go templates:

{
  "greeting": "Hello, {{.Name}}!",
  "summary": "Found {{.Count}} {{if eq .Count 1}}item{{else}}items{{end}}"
}
i18n.T("greeting", map[string]any{"Name": "World"})  // "Hello, World!"
i18n.T("summary", map[string]any{"Count": 3})        // "Found 3 items"

Available Template Functions

Function Description Example
title Title case {{title .Name}}
lower Lowercase {{lower .Name}}
upper Uppercase {{upper .Name}}
past Past tense {{past "delete"}} → "deleted"
gerund -ing form {{gerund "build"}} → "building"
plural Pluralise {{plural "file" .Count}}
article Add article {{article "apple"}} → "an apple"
quote Add quotes {{quote .Name}}"name"

Pluralisation

The package supports full CLDR plural categories:

{
  "item_count": {
    "zero": "No items",
    "one": "{{.Count}} item",
    "two": "{{.Count}} items",
    "few": "{{.Count}} items",
    "many": "{{.Count}} items",
    "other": "{{.Count}} items"
  }
}
i18n.T("item_count", map[string]any{"Count": 0})  // "No items" (if zero defined)
i18n.T("item_count", map[string]any{"Count": 1})  // "1 item"
i18n.T("item_count", map[string]any{"Count": 5})  // "5 items"

For simple cases, use i18n.count.*:

i18n.T("i18n.count.file", 1)  // "1 file"
i18n.T("i18n.count.file", 5)  // "5 files"

Magic Namespaces

The i18n.* namespace provides automatic message composition:

Labels (i18n.label.*)

i18n.T("i18n.label.status")   // "Status:"
i18n.T("i18n.label.version")  // "Version:"

Progress (i18n.progress.*)

i18n.T("i18n.progress.build")                // "Building..."
i18n.T("i18n.progress.check", "config")      // "Checking config..."

Counts (i18n.count.*)

i18n.T("i18n.count.file", 1)   // "1 file"
i18n.T("i18n.count.file", 5)   // "5 files"
i18n.T("i18n.count.repo", 10)  // "10 repos"

Done (i18n.done.*)

i18n.T("i18n.done.delete", "file")     // "File deleted"
i18n.T("i18n.done.create", "project")  // "Project created"

Fail (i18n.fail.*)

i18n.T("i18n.fail.delete", "file")  // "Failed to delete file"
i18n.T("i18n.fail.save", "config")  // "Failed to save config"

Numeric (i18n.numeric.*)

i18n.N("number", 1234567)   // "1,234,567"
i18n.N("percent", 0.85)     // "85%"
i18n.N("bytes", 1536000)    // "1.46 MB"
i18n.N("ordinal", 1)        // "1st"

Subjects

Subjects provide typed context for translations:

// Create a subject
subj := i18n.S("file", "config.yaml")

// Chain methods for additional context
subj := i18n.S("file", files).
    Count(len(files)).
    In("workspace").
    Formal()

// Use in translations
i18n.T("i18n.done.delete", subj.String())

Subject Methods

Method Description
Count(n) Set count for pluralisation
Gender(g) Set grammatical gender
In(loc) Set location context
Formal() Set formal address
Informal() Set informal address

Grammar Engine

The grammar engine handles verb conjugation and noun forms:

// Verb conjugation
i18n.PastTense("delete")   // "deleted"
i18n.PastTense("run")      // "ran" (irregular)
i18n.Gerund("build")       // "building"
i18n.Gerund("run")         // "running"

// Noun pluralisation
i18n.Pluralize("file", 1)  // "file"
i18n.Pluralize("file", 5)  // "files"
i18n.Pluralize("child", 2) // "children" (irregular)

// Articles
i18n.Article("apple")      // "an apple"
i18n.Article("banana")     // "a banana"

// Composed messages
i18n.Label("status")               // "Status:"
i18n.Progress("build")             // "Building..."
i18n.ProgressSubject("check", "cfg") // "Checking cfg..."
i18n.ActionResult("delete", "file")  // "File deleted"
i18n.ActionFailed("save", "config")  // "Failed to save config"

Customising Grammar

Add irregular forms in your locale JSON:

{
  "gram": {
    "verb": {
      "deploy": { "past": "deployed", "gerund": "deploying" }
    },
    "noun": {
      "repository": { "one": "repository", "other": "repositories" }
    },
    "punct": {
      "label": ":",
      "progress": "..."
    }
  }
}

Formality

For languages with formal/informal address (German Sie/du, French vous/tu):

// Set service-wide formality
svc.SetFormality(i18n.FormalityFormal)

// Per-translation formality via Subject
i18n.T("greeting", i18n.S("user", name).Formal())
i18n.T("greeting", i18n.S("user", name).Informal())

// Per-translation via TranslationContext
i18n.T("greeting", i18n.C("customer support").Formal())

Define formality variants in JSON:

{
  "greeting": "Hello",
  "greeting._formal": "Good morning, sir",
  "greeting._informal": "Hey there"
}

Modes

Three modes control missing key behaviour:

// Normal (default): Returns key as-is
i18n.SetMode(i18n.ModeNormal)
i18n.T("missing.key")  // "missing.key"

// Strict: Panics on missing keys (dev/CI)
i18n.SetMode(i18n.ModeStrict)
i18n.T("missing.key")  // panic!

// Collect: Dispatches to handler (QA testing)
i18n.SetMode(i18n.ModeCollect)
i18n.OnMissingKey(func(m i18n.MissingKey) {
    log.Printf("MISSING: %s at %s:%d", m.Key, m.CallerFile, m.CallerLine)
})

Custom Loaders

Implement the Loader interface for custom storage:

type Loader interface {
    Load(lang string) (map[string]Message, *GrammarData, error)
    Languages() []string
}

Example database loader:

type DBLoader struct {
    db *sql.DB
}

func (l *DBLoader) Languages() []string {
    // Query available languages from database
}

func (l *DBLoader) Load(lang string) (map[string]i18n.Message, *i18n.GrammarData, error) {
    // Load translations from database
}

// Use custom loader
svc, err := i18n.NewWithLoader(&DBLoader{db: db})

Custom Handlers

Add custom key handlers for dynamic patterns:

type MyHandler struct{}

func (h MyHandler) Match(key string) bool {
    return strings.HasPrefix(key, "my.prefix.")
}

func (h MyHandler) Handle(key string, args []any, next func() string) string {
    // Handle the key or call next() to delegate
    return "custom result"
}

// Add to handler chain
svc.AddHandler(MyHandler{})      // Append (lower priority)
svc.PrependHandler(MyHandler{})  // Prepend (higher priority)

Locale File Format

Locale files use nested JSON with dot-notation access:

{
  "cli": {
    "success": "Operation completed",
    "error": {
      "not_found": "{{.Name}} not found"
    }
  },
  "cmd": {
    "build": {
      "short": "Build the project",
      "long": "Build compiles source files into an executable"
    }
  },
  "gram": {
    "verb": {
      "build": { "past": "built", "gerund": "building" }
    },
    "noun": {
      "file": { "one": "file", "other": "files" }
    },
    "punct": {
      "label": ":",
      "progress": "..."
    }
  }
}

Access keys with dot notation:

i18n.T("cli.success")           // "Operation completed"
i18n.T("cli.error.not_found")   // "{{.Name}} not found"
i18n.T("cmd.build.short")       // "Build the project"

Configuration Options

Use functional options when creating a service:

svc, err := i18n.New(
    i18n.WithFallback("de-DE"),           // Fallback language
    i18n.WithFormality(i18n.FormalityFormal),  // Default formality
    i18n.WithMode(i18n.ModeStrict),       // Missing key mode
    i18n.WithDebug(true),                 // Show [key] prefix
)

Thread Safety

The package is fully thread-safe:

  • Service uses sync.RWMutex for state
  • Global Default() uses atomic.Pointer
  • OnMissingKey uses atomic.Value
  • FSLoader.Languages() uses sync.Once

Safe for concurrent use from multiple goroutines.

Debug Mode

Enable debug mode to see translation keys:

i18n.SetDebug(true)
i18n.T("cli.success")  // "[cli.success] Operation completed"

Useful for identifying which keys are used where.