121 lines
3.7 KiB
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
|
|
}
|