152 lines
4.5 KiB
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
|
|
}
|