Files
go-omni/i18n/types.go

121 lines
3.7 KiB
Go

// 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
}