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