// Package i18n provides immutable, instance-scoped message catalogs. It has no // dependency on HTTP, application configuration, business errors or storage. package i18n import ( "context" "encoding/json" "fmt" ) const Version = 1 // Kind describes portable message arguments, independently of Go identifiers. type Kind string const ( String Kind = "string" Boolean Kind = "boolean" Integer Kind = "integer" // Signed int64; browser callers may use bigint/string. Number Kind = "number" // Finite IEEE-754 float64. ) // Pattern is a text leaf or a select/plural branch. Branches require "other". // Text uses {name}; {{ and }} escape braces. Literal disables interpolation. // Plural is cardinal, with optional exact integer cases such as "=0". type Pattern struct { Text string `json:"text,omitempty"` Literal bool `json:"literal,omitempty"` Select string `json:"select,omitempty"` Plural string `json:"plural,omitempty"` Cases map[string]Pattern `json:"cases,omitempty"` } type Message struct { Params map[string]Kind `json:"params,omitempty"` Value Pattern `json:"value"` } func (m *Message) UnmarshalJSON(data []byte) error { type plain Message var raw map[string]json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { return err } if value, ok := raw["value"]; !ok || string(value) == "null" { return fmt.Errorf("i18n: message requires a value") } for key := range raw { if key != "value" && key != "params" { return fmt.Errorf("i18n: unknown message field") } } var value plain // Pattern's fields are checked by the strict decoder, including nested cases. if err := DecodeJSON(data, &value); err != nil { return err } *m = Message(value) return nil } // Document is the versioned interchange format. Source-locale documents own // parameter schemas; translations inherit them and may repeat identical schemas. type Document struct { Version int `json:"version"` Namespace string `json:"namespace"` Locale string `json:"locale"` Messages map[string]Message `json:"messages"` } // Config distinguishes source text from the default selection and enabled locales. // Fallbacks are explicit, ordered edges; cycles are rejected. The default locale // must be enabled. The source locale can remain a fallback-only language. type Config struct { SourceLocale string `json:"sourceLocale"` DefaultLocale string `json:"defaultLocale"` Locales []string `json:"locales"` Fallbacks map[string][]string `json:"fallbacks,omitempty"` } type Ref struct{ Namespace, Key string } // Result reports the actual dictionary language, which may differ from Requested. type Result struct { Text string Requested string Locale string Fallback bool } // Diagnostic deliberately excludes argument values and message contents. type Diagnostic struct { Kind string `json:"kind"` Namespace string `json:"namespace,omitempty"` Key string `json:"key,omitempty"` Locale string `json:"locale,omitempty"` Param string `json:"param,omitempty"` Problem string `json:"problem"` } func (d *Diagnostic) Error() string { return fmt.Sprintf("i18n: %s namespace=%q key=%q locale=%q param=%q: %s", d.Kind, d.Namespace, d.Key, d.Locale, d.Param, d.Problem) } type localeKey struct{} // WithLocale records a preference; rendering still matches it against the bundle. func WithLocale(ctx context.Context, locale string) context.Context { if ctx == nil { ctx = context.Background() } return context.WithValue(ctx, localeKey{}, locale) } func Locale(ctx context.Context) string { if ctx == nil { return "" } value, _ := ctx.Value(localeKey{}).(string) return value }