diff options
Diffstat (limited to 'i18n/err/i18nerr.go')
| -rw-r--r-- | i18n/err/i18nerr.go | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/i18n/err/i18nerr.go b/i18n/err/i18nerr.go new file mode 100644 index 0000000..8254dd4 --- /dev/null +++ b/i18n/err/i18nerr.go @@ -0,0 +1,168 @@ +// Package i18nerr implements errors with internationalization using gotext +package i18nerr + +import ( + "context" + "errors" + "fmt" + "log/slog" + "sync" + + "codeberg.org/eduVPN/eduvpn-common/internal/http" + + "golang.org/x/text/language" + "golang.org/x/text/message" +) + +var ( + printers sync.Map + once sync.Once +) + +// TranslatedInner defines errors that are used as inner causes but are still translated because they can happen frequently +func TranslatedInner(inner error) (string, bool) { + unwrapped := inner + for errors.Unwrap(unwrapped) != nil { + unwrapped = errors.Unwrap(unwrapped) + } + + var tErr *http.TimeoutError + switch { + case errors.As(inner, &tErr): + return printerOrNew(language.English).Sprintf("Timeout reached contacting URL: '%s'", tErr.URL), false + case errors.Is(inner, context.Canceled): + return unwrapped.Error(), true + } + return unwrapped.Error(), false +} + +// Error wraps an actual error with the translation key +// This translation key is later used to lookup translation +// The inner error always consists of the translation key and some formatting +type Error struct { + key message.Reference + args []any + wrapped *Error + Misc bool +} + +func (e *Error) translated(t language.Tag) string { + once.Do(func() { + inititializeLangs() + }) + msg := printerOrNew(t).Sprintf(e.key, e.args...) + if e.wrapped != nil { + return printerOrNew(t).Sprintf("%s. The cause of the error is: %s.", msg, e.wrapped.Error()) + } + return msg +} + +// Error gets the error string +// it does this by simply forwarding the error method from the actual inner error +func (e *Error) Error() string { + return e.translated(language.English) +} + +// Translations returns all the translations for the error including the source translation (english) +func (e *Error) Translations() map[string]string { + translations := make(map[string]string) + // add the source transltaion first + source := e.Error() + translations[language.English.String()] = source + for _, t := range message.DefaultCatalog.Languages() { + // already added + if t == language.English { + continue + } + // get the final translation string for the tag + // and add it if it's not equal to the english version + f := e.translated(t) + if f != source { + translations[t.String()] = f + } + } + return translations +} + +// Unwrap returns the unwrapped error +// it does this by unwrapping the inner error +func (e *Error) Unwrap() error { + if e.wrapped == nil { + return nil + } + return e.wrapped.Unwrap() +} + +// printerOrNew gets a message printer from the global printers map using the tag 'tag' +// If the printer cannot be found in the sync map, we return a new printer +func printerOrNew(tag language.Tag) *message.Printer { + v, ok := printers.Load(tag) + if !ok { + slog.Debug("i18n could not load printer from map", "tag", tag) + return message.NewPrinter(tag) + } + p, ok := v.(*message.Printer) + if !ok { + slog.Debug("i18n could not load printer from map with incorrect type", "tag", tag, "type", fmt.Sprintf("%T", p)) + return message.NewPrinter(tag) + } + return p +} + +// New creates a new i18n error using a message reference +func New(key message.Reference) *Error { + _ = printerOrNew(language.English).Sprint(key) + return &Error{key: key} +} + +// Newf creates a new i18n error using a message reference and arguments. +// It formats this with fmt.Errorf +func Newf(key message.Reference, args ...any) *Error { + _ = printerOrNew(language.English).Sprintf(key, args...) + return &Error{key: key, args: args} +} + +// Wrap creates a new i18n error using an error to be wrapped 'err' and a prefix message reference 'key'. +// It formats this with fmt.Errorf +func Wrap(err error, key message.Reference) *Error { + _ = printerOrNew(language.English).Sprintf(key) + t, misc := TranslatedInner(err) + return &Error{key: key, wrapped: &Error{key: t, Misc: misc}, Misc: misc} +} + +// Wrapf creates a new i18n error using an error to be wrapped 'err' and a prefix message reference 'key' with format arguments 'args'. +// It formats this with fmt.Errorf +func Wrapf(err error, key message.Reference, args ...any) *Error { + _ = printerOrNew(language.English).Sprintf(key, args...) + t, misc := TranslatedInner(err) + return &Error{key: key, args: args, wrapped: &Error{key: t, Misc: misc}, Misc: misc} +} + +// NewInternal creates an internal localised error from a display string +func NewInternal(disp string) *Error { + return Wrap(errors.New(disp), "An internal error occurred") +} + +// NewInternalf creates an internal localised error from a display string and arguments +func NewInternalf(disp string, args ...any) *Error { + return NewInternal(fmt.Sprintf(disp, args...)) +} + +// WrapInternal wraps an error and a display string into a localised internal error +func WrapInternal(err error, disp string) *Error { + return Wrap(fmt.Errorf("%s with internal cause: %w", disp, err), "An internal error occurred") +} + +// WrapInternalf wraps an error and a display string with args into a localised internal error +func WrapInternalf(err error, disp string, args ...any) *Error { + return WrapInternal(err, fmt.Sprintf(disp, args...)) +} + +// initializeLangs initializes the printers from the default catalog into the sync map +// we cannot do this in init() because this is too early +func inititializeLangs() { + slog.Debug("i18n initializing languages") + for _, t := range message.DefaultCatalog.Languages() { + printers.Store(t, message.NewPrinter(t)) + } +} |
