Files
go-omni/i18n/errori18n/errors.go

152 lines
4.5 KiB
Go

// Package errori18n adapts public errors to independent i18n dictionaries.
package errori18n
import (
"context"
"fmt"
"reflect"
"sort"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
"git.yuntuo.hk/k2safe/go-omni/i18n"
)
type Localizer struct {
bundle *i18n.Bundle
report func(error)
}
// New borrows an immutable Bundle. report is optional, must be concurrency-safe,
// and receives diagnostics without argument values or internal error causes.
func New(bundle *i18n.Bundle, report func(error)) *Localizer {
return &Localizer{bundle: bundle, report: report}
}
func kind(t string) i18n.Kind {
switch t {
case "string":
return i18n.String
case "bool":
return i18n.Boolean
case "int", "int32", "int64":
return i18n.Integer
case "float64":
return i18n.Number
}
return ""
}
// Bind returns a detached catalog, assigning references only to entries which
// do not already have one. Different catalogs can bind to different namespaces.
func Bind(c apierrors.Catalog, namespace string) apierrors.Catalog {
c = c.Clone()
for key, d := range c.Definitions {
if d.Translation == nil {
d.Translation = &apierrors.Translation{Namespace: namespace, Key: key}
c.Definitions[key] = d
}
}
return c
}
// Source exports only explicitly referenced public messages. Codes, statuses,
// examples and causes are not included. Fixed legacy braces remain literal.
func Source(c apierrors.Catalog, locale string) ([]i18n.Document, error) {
r, err := apierrors.NewRegistry(c)
if err != nil {
return nil, err
}
groups := map[string]i18n.Document{}
for _, d := range r.Catalog().Definitions {
if d.Translation == nil {
continue
}
ref := d.Translation
doc, ok := groups[ref.Namespace]
if !ok {
doc = i18n.Document{Version: i18n.Version, Locale: locale, Namespace: ref.Namespace, Messages: map[string]i18n.Message{}}
}
m := i18n.Message{Params: map[string]i18n.Kind{}, Value: i18n.Pattern{Text: d.Message, Literal: d.Params == nil}}
for _, p := range d.Params {
m.Params[p.Name] = kind(p.Type)
}
if prior, exists := doc.Messages[ref.Key]; exists && !reflect.DeepEqual(prior, m) {
return nil, fmt.Errorf("errori18n: conflicting source messages for one translation reference")
}
doc.Messages[ref.Key] = m
groups[ref.Namespace] = doc
}
names := []string{}
for ns := range groups {
names = append(names, ns)
}
sort.Strings(names)
docs := []i18n.Document{}
for _, ns := range names {
docs = append(docs, groups[ns])
}
_, err = i18n.New(i18n.Config{SourceLocale: locale, DefaultLocale: locale, Locales: []string{locale}}, docs...)
return docs, err
}
// Validate can be used during startup or generation to check explicit references
// and type compatibility. A runtime mismatch always preserves the original error.
func (l *Localizer) Validate(c apierrors.Catalog) error {
r, err := apierrors.NewRegistry(c)
if err != nil {
return err
}
for _, d := range r.Catalog().Definitions {
if d.Translation != nil {
if err := l.check(d); err != nil {
return err
}
}
}
return nil
}
func (l *Localizer) check(d apierrors.Definition) error {
ref := d.Translation
if l == nil || l.bundle == nil {
return &i18n.Diagnostic{Kind: "missing", Problem: "error localizer has no bundle"}
}
schema, ok := l.bundle.Schema(i18n.Ref{Namespace: ref.Namespace, Key: ref.Key})
if !ok {
return &i18n.Diagnostic{Kind: "missing", Namespace: ref.Namespace, Key: ref.Key, Problem: "error translation reference is not defined"}
}
expected := map[string]i18n.Kind{}
for _, p := range d.Params {
expected[p.Name] = kind(p.Type)
}
if !reflect.DeepEqual(schema, expected) {
return &i18n.Diagnostic{Kind: "parameter", Namespace: ref.Namespace, Key: ref.Key, Problem: "error parameter schema differs from message schema"}
}
return nil
}
func (l *Localizer) Localize(ctx context.Context, registry *apierrors.Registry, failure *apierrors.APIError) (string, bool) {
if l == nil || failure == nil {
return "", false
}
d, ok := registry.Definition(failure.Code)
if !ok || d.Translation == nil {
return "", false
}
if err := l.check(d); err != nil {
if l.report != nil {
l.report(err)
}
return "", false
}
result, err := l.bundle.RenderContext(ctx, i18n.Ref{Namespace: d.Translation.Namespace, Key: d.Translation.Key}, failure.Parameters())
if err != nil {
if l.report != nil {
l.report(err)
}
return "", false
}
if result.Fallback && l.report != nil {
l.report(&i18n.Diagnostic{Kind: "fallback", Namespace: d.Translation.Namespace, Key: d.Translation.Key, Locale: result.Requested, Problem: "used a fallback dictionary"})
}
return result.Text, true
}