i18n

package module
v0.0.0-...-df47ab9 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 6 Imported by: 0

README

go-ruby-i18n/i18n

i18n — go-ruby-i18n

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's i18n gem core — the interpreter-independent translate/localize logic of I18n::Backend::Simple. It does dotted-key lookup over an in-memory translation store, %{name} / %<count>d interpolation, :count pluralization, :default and fallback-locale chains, the Translation missing: … behaviour, :scope, and strftime-style localization of a host-supplied Date/Time — matching the i18n gem byte-for-byte (validated by a differential MRI oracle).

It is the I18n backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-yaml, go-ruby-erb and go-ruby-regexp.

What it is — and isn't. Looking a key up in a store, selecting a plural form, interpolating placeholders, walking a :default / fallback chain, and substituting a strftime pattern are fully deterministic and need no interpreter, so they live here as pure Go. Two things are seams the host fills: parsing translation files (YAML / Ruby / JSON) into the nested Hash the store holds, and constructing the Date / Time values Localize formats. rbgo loads the files and calls StoreTranslations, and hands Localize a Temporal; everything the gem computes deterministically is here.

Features

Faithful port of I18n.translate / I18n.localize, validated against the real i18n gem on every supported platform:

  • Dotted-key lookup over a per-locale nested symbol-keyed store ("foo.bar.baz"), with :scope prefixing.
  • Interpolation%{name} (and the %{name|word} form), the literal %%, and sprintf-style %<count>d / %<x>05.2f; reserved-key collisions raise ReservedInterpolationKey; a missing placeholder is left literal by default (the gem's out-of-the-box behaviour) or raises via a configurable handler.
  • Pluralization:count selects :zero / :one / :other exactly as the Simple backend's pluralization_key does, with a missing form raising InvalidPluralizationData (message byte-identical to the gem). CLDR :one/:few/:many/:zero/:two rules for ru/uk/cs/sk/pl/ar/fr/pt ship in a registry, and any locale's rule is overridable.
  • Fallbacks:default chains (a literal string returned as-is, a symbol looked up as a key) and fallback-locale chains (I18n.fallbacks), always falling through to the default locale.
  • Missing-translation — the Translation missing: <locale>.<key> string, or a *MissingTranslation error with :raise / the T!-style flag.
  • I18n.localize<date|time>.formats.<name> lookup plus strftime substitution, with %a/%A/%b/%B/%p/%P (and %^ upcased variants) drawn from the locale's day_names / month_names / am / pm tables, and a built-in strftime covering the numeric / zone / fractional-second directives.
  • Store modelI18n::Backend::Simple-style nested Hash with deep-merge of stored translations, plus Locale / DefaultLocale / AvailableLocales and Exists.

CGO-free, dependency-free, 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) and three OSes (Linux, macOS, Windows).

Install

go get github.com/go-ruby-i18n/i18n

Usage

package main

import (
	"fmt"
	"time"

	"github.com/go-ruby-i18n/i18n"
)

func main() {
	I := i18n.New("en") // default + current locale

	// The host loads a translation file and hands the nested Hash to the store
	// (the file-parsing seam). Here we store it directly.
	I.Backend().StoreTranslations("en", map[string]i18n.Value{
		"greeting": "Hello, %{name}!",
		"apples": map[string]i18n.Value{
			"zero":  "no apples",
			"one":   "one apple",
			"other": "%{count} apples",
		},
		"date": map[string]i18n.Value{
			"formats": map[string]i18n.Value{"long": "%B %d, %Y"},
			"month_names": []i18n.Value{nil, "January", "February", "March",
				"April", "May", "June", "July", "August", "September",
				"October", "November", "December"},
		},
	})

	g, _ := I.Translate("greeting", &i18n.Options{Values: map[string]i18n.Value{"name": "World"}})
	fmt.Println(g) // Hello, World!

	n := 2
	a, _ := I.Translate("apples", &i18n.Options{Count: &n})
	fmt.Println(a) // 2 apples

	miss, _ := I.Translate("nope", nil)
	fmt.Println(miss) // Translation missing: en.nope

	// Localize a host-supplied Date/Time (the clock-value seam). A Go time.Time
	// adapts through DateValue / TimeValue.
	d := i18n.DateValue{T: time.Date(2026, 6, 30, 0, 0, 0, 0, time.UTC)}
	l, _ := I.Localize(d, "long", true, nil) // named format -> date.formats.long
	fmt.Println(l)                           // June 30, 2026
}

The seams

Two boundaries the host (rbgo) crosses; everything else is pure Go here.

Seam Host provides This library does
Translation files parses YAML/Ruby/JSON to a nested map[string]Value and calls StoreTranslations (deep-merged) lookup, interpolation, pluralization, fallback, missing
Date / Time values a Temporal (Ruby Date/Time/DateTime); a Go time.Time adapts via DateValue / TimeValue format lookup + locale name substitution + strftime

API

type I18n struct{ /* … */ }
func New(defaultLocale string) *I18n

func (i *I18n) Translate(key string, opts *Options) (Value, error) // I18n.t
func (i *I18n) T(key string, opts *Options) (Value, error)         // alias
func (i *I18n) Localize(obj Temporal, format string, named bool, opts *Options) (string, error) // I18n.l
func (i *I18n) L(obj Temporal, format string, named bool, opts *Options) (string, error)        // alias
func (i *I18n) Exists(key string, locale ...string) bool           // I18n.exists?

func (i *I18n) Locale() string;        func (i *I18n) SetLocale(string)
func (i *I18n) DefaultLocale() string; func (i *I18n) SetDefaultLocale(string)
func (i *I18n) AvailableLocales() []string
func (i *I18n) SetFallbacks(locale string, chain ...string)
func (i *I18n) SetMissingInterpolationHandler(MissingInterpolationHandler)
func (i *I18n) Backend() *Simple

type Options struct {
	Locale  string
	Scope   []string
	Count   *int
	Default []DefaultEntry
	Values  map[string]Value
	Raise   bool
}
func Lit(string) DefaultEntry  // a literal default
func Key(string) DefaultEntry  // a symbol default (looked up)

type Simple struct{ /* … */ }
func NewSimple() *Simple
func (s *Simple) StoreTranslations(locale string, data map[string]Value) // deep-merge
func (s *Simple) RegisterPluralRule(locale string, rule PluralRule)
func (s *Simple) AvailableLocales() []string

type Temporal interface{ /* Year/Month/Day/Hour/…/HasTime */ }
type TimeValue struct{ T time.Time } // Temporal with a clock
type DateValue struct{ T time.Time } // Temporal without a clock

type MissingTranslation struct{ Locale, Key string; Scope []string }
type InvalidPluralizationData struct{ /* … */ }
type MissingInterpolationArgument struct{ /* … */ }
type ReservedInterpolationKey struct{ /* … */ }

Tests & coverage

The suite pairs deterministic, ruby-free tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential MRI oracle: each translate / pluralize / interpolate / fallback / missing / localize case is run through the real i18n gem and compared byte-for-byte. The oracle scripts $stdout.binmode so Windows text-mode never pollutes the bytes, and skip themselves where the gem / ruby is absent.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-i18n/i18n authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package i18n is a pure-Go (no cgo) port of Ruby's I18n core — the interpreter-independent translate/localize logic of the `i18n` gem's I18n::Backend::Simple. It does dotted-key lookup over an in-memory translation store, %{name} / %<count>d interpolation, :count pluralization, :default and fallback-locale chains, the "Translation missing: …" behaviour, :scope, and strftime-style localization of a host-supplied Date/Time — matching the gem byte-for-byte.

Loading translation *files* (YAML/Ruby) and constructing Date/Time values are seams the host (go-embedded-ruby / rbgo) fills: it parses a file to a nested Hash and calls StoreTranslations, and it hands Localize a Temporal. Everything the gem computes deterministically lives here, with no Ruby runtime.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AsString

func AsString(v Value) string

AsString coerces a resolved Value to text for hosts that always want a string from Translate (the gem's t always returns a String for a found scalar). It renders the same way Ruby's #to_s would for the translate result shapes.

Types

type DateValue

type DateValue struct{ T time.Time }

DateValue adapts a Go time.Time to Temporal as a bare Date (no clock): HasTime is false, so the "date" format tree is used and clock directives read zero.

func (DateValue) Day

func (d DateValue) Day() int

func (DateValue) HasTime

func (d DateValue) HasTime() bool

func (DateValue) Hour

func (d DateValue) Hour() int

func (DateValue) Min

func (d DateValue) Min() int

func (DateValue) Month

func (d DateValue) Month() int

func (DateValue) Nsec

func (d DateValue) Nsec() int

func (DateValue) Sec

func (d DateValue) Sec() int

func (DateValue) Wday

func (d DateValue) Wday() int

func (DateValue) Yday

func (d DateValue) Yday() int

func (DateValue) Year

func (d DateValue) Year() int

func (DateValue) ZoneName

func (d DateValue) ZoneName() string

func (DateValue) ZoneOffset

func (d DateValue) ZoneOffset() int

type DefaultEntry

type DefaultEntry struct {
	Literal string
	IsKey   bool
	KeyName string
}

DefaultEntry is one element of a :default chain: either a literal string to return as-is, or a key to look up (Key set). Use Lit / Key helpers.

func Key

func Key(k string) DefaultEntry

Key builds a symbol default (looked up as a translation key if reached).

func Lit

func Lit(s string) DefaultEntry

Lit builds a literal default (returned verbatim if reached).

type I18n

type I18n struct {
	// contains filtered or unexported fields
}

I18n bundles the mutable global state the gem keeps in I18n.config: the active backend, the current and default locale, and the fallback chain. The package exposes a default instance through the top-level functions (I18n.t etc.), and New lets a host hold an isolated one.

func New

func New(defaultLocale string) *I18n

New returns an I18n with an empty Simple backend and the given default locale, which is also the initial current locale.

func (*I18n) AvailableLocales

func (i *I18n) AvailableLocales() []string

AvailableLocales returns the locales with stored translations.

func (*I18n) Backend

func (i *I18n) Backend() *Simple

Backend returns the underlying Simple store so a host can load translations into it.

func (*I18n) DefaultLocale

func (i *I18n) DefaultLocale() string

DefaultLocale returns the default locale (I18n.default_locale).

func (*I18n) Exists

func (i *I18n) Exists(key string, locale ...string) bool

Exists reports whether key resolves to a translation in locale (defaulting to the current locale), mirroring I18n.exists?. It does not consult :default or fallback locales — only the asked-for locale.

func (*I18n) L

func (i *I18n) L(obj Temporal, format string, named bool, opts *Options) (string, error)

L is a short alias for Localize.

func (*I18n) Locale

func (i *I18n) Locale() string

Locale returns the current locale (I18n.locale).

func (*I18n) Localize

func (i *I18n) Localize(obj Temporal, format string, named bool, opts *Options) (string, error)

Localize formats a Temporal (a host-supplied Date/Time) for a locale, the way I18n.localize / I18n.l does:

  • a named format (e.g. "long") is looked up under <type>.formats.<name>, where <type> is "time" when the value carries a clock else "date"; an unknown named format yields a *MissingTranslation (matching the gem, which raises there);
  • a literal format string is used as-is;
  • %a/%A/%b/%B/%p/%P (and their %^ upcased variants) are substituted from the locale's date.day_names / month_names / time.am|pm before the value is strftime'd, exactly as I18n::Backend::Base#translate_localization_format.

opts.Locale overrides the current locale; opts.Values supply nothing here but are accepted for symmetry.

func (*I18n) SetDefaultLocale

func (i *I18n) SetDefaultLocale(l string)

SetDefaultLocale sets the default locale (I18n.default_locale=).

func (*I18n) SetFallbacks

func (i *I18n) SetFallbacks(locale string, chain ...string)

SetFallbacks sets the fallback chain for a locale (the list of locales tried, in order, when a key is missing in it). The locale itself need not be listed; it is always tried first.

func (*I18n) SetLocale

func (i *I18n) SetLocale(l string)

SetLocale sets the current locale (I18n.locale=).

func (*I18n) SetMissingInterpolationHandler

func (i *I18n) SetMissingInterpolationHandler(h MissingInterpolationHandler)

SetMissingInterpolationHandler installs the handler used when a %{...} placeholder has no value. The default keeps the placeholder literal.

func (*I18n) T

func (i *I18n) T(key string, opts *Options) (Value, error)

T is a short alias for Translate.

func (*I18n) Translate

func (i *I18n) Translate(key string, opts *Options) (Value, error)

Translate looks up key and returns its translation, applying scope, count pluralization, interpolation, :default chains and fallback locales — the behaviour of I18n.translate / I18n.t.

A missing key yields the gem's "Translation missing: <locale>.<key>" string, unless opts.Raise is set, in which case a *MissingTranslation is returned.

type InvalidPluralizationData

type InvalidPluralizationData struct {
	Entry map[string]Value // the plural sub-hash
	Count int
	Key   string // the plural key that was expected but absent
}

InvalidPluralizationData mirrors I18n::InvalidPluralizationData: a :count was given but the entry lacks the plural key the rule selected (e.g. no :other).

func (*InvalidPluralizationData) Error

func (e *InvalidPluralizationData) Error() string

type MissingInterpolationArgument

type MissingInterpolationArgument struct {
	Key    string
	Values map[string]Value
	String string
}

MissingInterpolationArgument mirrors I18n::MissingInterpolationArgument: a placeholder in the string has no matching value. The Simple backend's default handler raises this; the default config installed here keeps the placeholder literal instead (matching the gem's out-of-the-box behaviour), so this is only produced when a raising handler is configured.

func (*MissingInterpolationArgument) Error

type MissingInterpolationHandler

type MissingInterpolationHandler func(key string, values map[string]Value, s string) (Value, error)

MissingInterpolationHandler decides what to substitute for a placeholder whose value is absent. Returning an error aborts interpolation. The default keeps the placeholder text literal, matching the i18n gem's out-of-the-box behaviour (its default handler is overridable but, as shipped, leaves the text in place because the un-configured handler returns the string unchanged for the gsub).

type MissingTranslation

type MissingTranslation struct {
	Locale string   // the locale the lookup ran in
	Key    string   // the requested key (may be dotted)
	Scope  []string // any :scope prefix that was applied
}

MissingTranslation is the error MRI's I18n raises (as I18n::MissingTranslationData / its MissingTranslation mixin) when a key does not resolve. Its Error() reproduces the gem's "Translation missing: <keys>" message verbatim, and Translate falls back to that same text (rather than raising) unless WithRaise / T! is used.

func (*MissingTranslation) Error

func (e *MissingTranslation) Error() string

func (*MissingTranslation) Message

func (e *MissingTranslation) Message() string

Message returns the human string the gem substitutes for a missing key, e.g. "Translation missing: en.foo.bar". MRI capitalises the leading "Translation".

type Options

type Options struct {
	// Locale overrides the current locale for this call (:locale).
	Locale string
	// Scope is a key prefix prepended to the lookup (:scope); each entry may be
	// dotted and is split on ".".
	Scope []string
	// Count drives pluralization and is also exposed to interpolation as
	// "count" (:count). Use a non-nil *int.
	Count *int
	// Default is the :default chain: each entry is tried in order. A string that
	// names no key is returned literally; a *Symbol entry is looked up as a key.
	Default []DefaultEntry
	// Values are the interpolation variables (the remaining keyword args).
	Values map[string]Value
	// Raise makes a missing translation return a *MissingTranslation error
	// instead of the "Translation missing: …" string (:raise / I18n.t!).
	Raise bool
}

Options configures a single Translate / Localize call, mirroring the gem's keyword options (:scope, :default, :count, :locale, interpolation values…).

type PluralRule

type PluralRule func(count int) string

PluralRule maps a count to one of the CLDR plural category names ("zero", "one", "two", "few", "many", "other"). The Simple backend ships only the basic English-style rule (zero/one/other); richer rules — the CLDR categories the i18n-spec / rails-i18n pluralization data uses — are registered per locale and consulted by Translate when a :count is given.

type ReservedInterpolationKey

type ReservedInterpolationKey struct {
	Key    string
	String string
}

ReservedInterpolationKey mirrors I18n::ReservedInterpolationKey: the string uses a %{...} placeholder whose name collides with a reserved option key (:scope, :default, …).

func (*ReservedInterpolationKey) Error

func (e *ReservedInterpolationKey) Error() string

type Simple

type Simple struct {
	// contains filtered or unexported fields
}

Simple is a port of I18n::Backend::Simple: the in-memory translation store (a per-locale nested symbol-keyed Hash) plus the lookup, pluralization and interpolation logic the default I18n backend runs over it. Loading translation *files* into the store is the host's job (the seam) — StoreTranslations takes the already-parsed nested data and deep-merges it, exactly as the gem's store_translations does after its YAML/Ruby loader returns.

func NewSimple

func NewSimple() *Simple

NewSimple returns an empty Simple backend.

func (*Simple) AvailableLocales

func (s *Simple) AvailableLocales() []string

AvailableLocales returns the locales that have stored translations, sorted for determinism (I18n.available_locales).

func (*Simple) RegisterPluralRule

func (s *Simple) RegisterPluralRule(locale string, rule PluralRule)

RegisterPluralRule sets the plural-category rule for a locale, overriding the built-in table. It lets a host install the rails-i18n pluralization data's rule for any locale.

func (*Simple) StoreTranslations

func (s *Simple) StoreTranslations(locale string, data map[string]Value)

StoreTranslations deep-merges data (a nested symbol-keyed tree for one locale) into the store. data may use map[string]any/[]any (a generic loader's output) or the canonical Value model; it is normalized on the way in. This is the file-loading seam: rbgo parses a YAML/Ruby file to this shape and calls here.

type Temporal

type Temporal interface {
	Year() int
	Month() int // 1..12
	Day() int   // 1..31
	Hour() int  // 0..23
	Min() int   // 0..59
	Sec() int   // 0..59
	Nsec() int  // 0..999999999
	// Wday is the 0..6 day-of-week, Sunday == 0 (Ruby's Time#wday).
	Wday() int
	// Yday is the 1..366 day-of-year (Ruby's Time#yday).
	Yday() int
	// ZoneOffset is the offset east of UTC in seconds (Ruby's Time#utc_offset).
	ZoneOffset() int
	// ZoneName is the abbreviated zone name, "" when unknown (Ruby's Time#zone).
	ZoneName() string
	// HasTime reports whether this value carries a clock component (a Time /
	// DateTime) versus a bare Date; it selects the "time" vs "date" format tree
	// and whether %p is meaningful, mirroring object.respond_to?(:sec).
	HasTime() bool
}

Temporal is the date/time value Localize formats. rbgo's Date / Time / DateTime objects implement it (the seam): the library never constructs a clock value, it only reads the broken-down fields a host hands it. A plain Go time.Time satisfies the contract through TimeValue.

type TimeValue

type TimeValue struct{ T time.Time }

TimeValue adapts a Go time.Time to Temporal so callers can localize standard library times directly. It always reports HasTime() == true.

func (TimeValue) Day

func (t TimeValue) Day() int

func (TimeValue) HasTime

func (t TimeValue) HasTime() bool

func (TimeValue) Hour

func (t TimeValue) Hour() int

func (TimeValue) Min

func (t TimeValue) Min() int

func (TimeValue) Month

func (t TimeValue) Month() int

func (TimeValue) Nsec

func (t TimeValue) Nsec() int

func (TimeValue) Sec

func (t TimeValue) Sec() int

func (TimeValue) Wday

func (t TimeValue) Wday() int

func (TimeValue) Yday

func (t TimeValue) Yday() int

func (TimeValue) Year

func (t TimeValue) Year() int

func (TimeValue) ZoneName

func (t TimeValue) ZoneName() string

func (TimeValue) ZoneOffset

func (t TimeValue) ZoneOffset() int

type Value

type Value any

Value is a translation-tree node. The host (rbgo) loads YAML/Ruby translation files into this shape — the file parsing is the seam; this library does the lookup, interpolation, pluralization and localization over it. A node is one of: nil, bool, int, int64, float64, string, []Value, or map[string]Value (a nested sub-tree, the Ruby symbol-keyed Hash). Keys are stored as plain strings (the symbol name without the leading colon), matching how the Simple backend symbolizes loaded data.

func RaiseMissingInterpolation

func RaiseMissingInterpolation(key string, values map[string]Value, s string) (Value, error)

RaiseMissingInterpolation is the handler that reproduces the gem's documented I18n::MissingInterpolationArgument behaviour. Install it via SetMissingInterpolationHandler to make an absent placeholder an error instead of being left literal.

Jump to

Keyboard shortcuts

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