go-i18n/docs/forward-api.md
Virgil 12bbe48970
All checks were successful
Security Scan / security (push) Successful in 15s
Test / test (push) Successful in 2m29s
docs(i18n): document location and locale providers
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 11:57:39 +00:00

9.9 KiB

title description
Forward Composition API The consumer-facing API for composing grammatically correct text from base forms.

Forward Composition API

The forward API composes grammatically correct text from base forms. This is the consumer-facing side of go-i18n -- applications call these functions to build messages.

Service Setup

// Option 1: Default service (lazy-initialised, English)
svc := i18n.Default()

// Option 2: Explicit creation with options
svc, err := i18n.New(
    i18n.WithLanguage("en-GB"),
    i18n.WithFallback("en"),
    i18n.WithDefaultHandlers(),
)
i18n.SetDefault(svc)

// Option 3: Custom loader (your own filesystem)
loader := i18n.NewFSLoader(myFS, "locales")
svc, err := i18n.NewWithLoader(loader)

// Option 4: Load from an arbitrary fs.FS
svc, err := i18n.NewWithFS(myFS, "locales")

The service automatically detects the system language from LANG, LC_ALL, or LC_MESSAGES environment variables using BCP 47 tag matching.

Options

Option Effect
WithFallback("en") Set fallback language for missing translations
WithLanguage("fr") Set the initial language before the service starts serving
WithLocation("workspace") Set the default location context
WithDefaultHandlers() Register the six built-in i18n.* namespace handlers
WithHandlers(h...) Replace handlers entirely
WithMode(ModeStrict) Panic on missing keys (useful in CI)
WithFormality(FormalityFormal) Default formality level
WithDebug(true) Prefix output with key path for debugging

Translation Modes

Mode Behaviour
ModeNormal Returns key as-is when missing (production)
ModeStrict Panics on missing key (dev/CI)
ModeCollect Dispatches MissingKey events, returns [key] (QA)

Grammar Primitives

PastTense(verb) -> string

Returns the past tense of a verb using a 3-tier lookup: JSON grammar data, then irregular Go map, then regular morphology rules.

i18n.PastTense("delete")  // "deleted"
i18n.PastTense("commit")  // "committed"
i18n.PastTense("go")      // "went"       (irregular)
i18n.PastTense("run")     // "ran"        (irregular)
i18n.PastTense("copy")    // "copied"     (regular rule: consonant+y -> ied)

Regular rules applied in order:

  1. Already ends in -ed with non-vowel, non-e third-from-end -- return as-is
  2. Ends in -e -- append d
  3. Ends in consonant + y -- replace y with ied
  4. CVC doubling applies -- double final consonant + ed
  5. Default -- append ed

Gerund(verb) -> string

Returns the present participle (-ing form) of a verb.

i18n.Gerund("delete")  // "deleting"
i18n.Gerund("commit")  // "committing"
i18n.Gerund("run")     // "running"
i18n.Gerund("die")     // "dying"       (ie -> ying)

Pluralize(noun, count) -> string

Returns singular for count=1, plural otherwise.

i18n.Pluralize("file", 1)     // "file"
i18n.Pluralize("file", 5)     // "files"
i18n.Pluralize("person", 3)   // "people"    (irregular)
i18n.Pluralize("child", 2)    // "children"  (irregular)

PluralForm(noun) -> string

Always returns the plural form (no count check).

i18n.PluralForm("repository")  // "repositories"
i18n.PluralForm("child")       // "children"
i18n.PluralForm("wolf")        // "wolves"

Article(word) -> string

Returns "a" or "an" based on phonetic rules, not spelling.

i18n.Article("file")     // "a"
i18n.Article("error")    // "an"
i18n.Article("user")     // "a"   (sounds like "yoo-zer")
i18n.Article("hour")     // "an"  (silent h)
i18n.Article("SSH")      // "an"  (sounds like "ess-ess-aitch")

Uses consonant/vowel sound exception maps, falling back to first-letter vowel check.

Utility Functions

i18n.Title("hello world")  // "Hello World"
i18n.Quote("config.yaml")  // "\"config.yaml\""
i18n.Progress("build")     // "Building..."
i18n.ProgressSubject("build", "project")  // "Building project..."
i18n.ActionResult("delete", "file")       // "File deleted"
i18n.ActionFailed("push", "commits")      // "Failed to push commits"
i18n.Label("status")                      // "Status:"

T() -- Core Translation Function

T() resolves message IDs through a handler chain, then falls back to direct key lookup with language fallback.

i18n.T("greeting")                    // Direct key lookup
i18n.T("i18n.label.status")           // Via LabelHandler -> "Status:"
i18n.T("i18n.progress.build")         // Via ProgressHandler -> "Building..."
i18n.T("i18n.count.file", 5)          // Via CountHandler -> "5 files"

Resolution order:

  1. Run through handler chain (stops at first match)
  2. Look up key in current language messages
  3. Look up key in fallback language messages
  4. Try common.action.{verb} and common.{verb} variants
  5. Handle missing key according to current mode

Raw()

Raw() translates without running the i18n.* namespace handler chain. Useful when you want direct key lookup only.

i18n.Raw("my.custom.key")  // Direct lookup, no handler magic

Magic Namespace Handlers

Six built-in handlers are registered by WithDefaultHandlers(). They intercept keys matching their i18n.* prefix and compose output from grammar primitives.

i18n.label.* -- LabelHandler

Produces labelled output with locale-specific suffix.

T("i18n.label.status")    // "Status:"
T("i18n.label.progress")  // "Progress:"

The suffix is language-specific: English uses :, French uses : (space before colon).

i18n.progress.* -- ProgressHandler

Produces gerund-form progress messages.

T("i18n.progress.build")              // "Building..."
T("i18n.progress.build", "project")   // "Building project..."
T("i18n.progress.delete", "cache")    // "Deleting cache..."

i18n.count.* -- CountHandler

Produces pluralised count messages.

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

i18n.done.* -- DoneHandler

Produces past-tense completion messages.

T("i18n.done.delete", "config.yaml")  // "Config.yaml deleted"
T("i18n.done.push", "commits")        // "Commits pushed"
T("i18n.done.delete")                 // "Deleted"

i18n.fail.* -- FailHandler

Produces failure messages.

T("i18n.fail.push", "commits")   // "Failed to push commits"
T("i18n.fail.delete")            // "Failed to delete"

i18n.numeric.* -- NumericHandler

Locale-aware number formatting.

T("i18n.numeric.number", 1234567)   // "1,234,567"
T("i18n.numeric.decimal", 3.14)     // "3.14"
T("i18n.numeric.percent", 0.85)     // "85%"
T("i18n.numeric.bytes", 1536000)    // "1.46 MB"
T("i18n.numeric.ordinal", 3)        // "3rd"
T("i18n.numeric.ago", 5, "minutes") // "5 minutes ago"

The shorthand N() function wraps this namespace:

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"

Subject Builder -- S()

Builds semantic context for complex translations:

s := i18n.S("file", "config.yaml")
s.Count(3)          // Set plural count
s.Gender("neuter")  // Grammatical gender (for gendered languages)
s.In("workspace")   // Location context
s.Formal()          // Formal register

// All methods chain:
i18n.S("file", "config.yaml").Count(3).In("workspace").Formal()

The Subject carries metadata that gendered/formal language systems (French, German, Japanese) use to select correct grammatical forms. English mostly ignores gender/formality, but the API is language-agnostic.

Translation Context -- C()

Provides disambiguation for translations where the same key has different meanings in different contexts:

i18n.T("direction.right", i18n.C("navigation"))  // "rechts" (German)
i18n.T("status.right", i18n.C("correctness"))    // "richtig" (German)

Context can carry gender and formality:

ctx := i18n.C("greeting").WithGender("feminine").Formal()
i18n.T("welcome", ctx)

Custom Handlers

Implement KeyHandler to add your own namespace handlers:

type KeyHandler interface {
    Match(key string) bool
    Handle(key string, args []any, next func() string) string
}

Register them on the service:

i18n.AddHandler(myHandler)       // Append to chain
i18n.PrependHandler(myHandler)   // Insert at start

Each handler receives a next function to delegate to the rest of the chain -- this is a middleware pattern.

Registering External Locales

Packages can register their own locale files to be loaded when the default service initialises:

//go:embed locales/*.json
var localeFS embed.FS

func init() {
    i18n.RegisterLocales(localeFS, "locales")
}

If the service is already initialised, the locales are loaded immediately. Otherwise they are queued and loaded during Init().

CLDR Plural Rules

The service supports CLDR plural categories with rules for English, German, French, Spanish, Russian, Polish, Arabic, Chinese, Japanese, and Korean:

Category Example Languages
PluralZero Arabic (n=0)
PluralOne Most languages (n=1)
PluralTwo Arabic, Welsh (n=2)
PluralFew Slavic (2-4), Arabic (3-10)
PluralMany Slavic (5+), Arabic (11-99)
PluralOther Default/fallback

Messages can define plural forms in locale JSON:

{
  "item_count": {
    "one": "{{.Count}} item",
    "other": "{{.Count}} items",
    "zero": "No items"
  }
}

Time and Relative Dates

i18n.TimeAgo(time.Now().Add(-5 * time.Minute))  // "5 minutes ago"
i18n.FormatAgo(3, "hour")                        // "3 hours ago"

Template Functions

All grammar functions are available as Go template functions via TemplateFuncs():

template.New("").Funcs(i18n.TemplateFuncs())

Available functions: title, lower, upper, past, gerund, plural, pluralForm, article, quote, label, progress, progressSubject, actionResult, actionFailed, timeAgo, formatAgo.