1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
// 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"
// this blank/dummy import is here to make `go mod tidy` work
// as `go mod tidy` otherwise removes this import that is needed for `go generate`
_ "golang.org/x/text/message/pipeline"
)
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))
}
}
|