package util import ( "errors" "fmt" "gitea.dooplex.hu/admin/felhom-controller/internal/i18n" ) // ── An error that carries the KEY of the sentence it is, not only the sentence ──────────────────── // // Localisation slice 2 release B (R-557). 179 Hungarian sentences in this product are built deep // inside a package with `fmt.Errorf` and printed by whoever catches them — a page, a JSON answer, a // flash. They cannot be translated where they are SHOWN, because by then they are a finished string; // and they must not be translated where they are MADE, because that code has no request and no // language. So the error carries its key and the display end renders it. // // return util.MsgErrorf(stacks.ErrRequiredField, "deploy.field_required", label) // ... // http.Error(w, s.errText(r, err), 400) // the household's language // if errors.Is(err, stacks.ErrRequiredField) { … } // still true — R-553's signal is intact // // THREE PROPERTIES, each earned: // // 1. `Error()` RETURNS THE HUNGARIAN TEXT, byte for byte what `fmt.Errorf` produced. Every printer // that has not been converted — a log line, a third-party wrapper, `%v` in a Printf — keeps // printing exactly what it printed before. That is what makes converting 179 producers safe // without converting all their printers in the same commit, and it is what the parity gate // measures. // 2. `errors.Is` STILL WORKS, for the kind AND for a wrapped cause. `Unwrap() []error` returns the // kind first and then any error among the arguments, so `errors.Is(err, ErrRequiredField)` and // `errors.Is(err, os.ErrNotExist)` are both answerable. The predecessor `KindErrorf` returned the // kind alone and dropped the cause; this does not. // 3. AN ERROR ARGUMENT IS RENDERED RECURSIVELY. `fmt.Errorf("formázás sikertelen: %w", err)` is a // sentence wrapping a sentence. Passing the inner error as an ARGUMENT (not as pre-rendered text) // lets `Text(lang, …)` translate the whole chain — the outer in the household's language and the // inner too, when the inner also carries a key. An inner error with no key prints itself, which // is the right answer for a restic or docker message that is not ours to translate. // MsgErrorf builds an error whose message is the bundle's `key` filled with `args`. // // `kind` may be nil. When it is not, it is the sentinel `errors.Is` tests — the same contract // KindErrorf established (R-553), and the reason a converted producer does not break a decision. func MsgErrorf(kind error, key string, args ...interface{}) error { return &msgError{kind: kind, key: key, args: args} } // MsgError is MsgErrorf with no kind, for a producer nothing branches on. func MsgError(key string, args ...interface{}) error { return &msgError{key: key, args: args} } type msgError struct { kind error key string args []interface{} } // Error returns the Hungarian sentence — the default language, and the bytes the literal carried. func (e *msgError) Error() string { return e.Text(i18n.Default) } // Text renders the message in lang, rendering any error argument that also carries a key. func (e *msgError) Text(lang string) string { b, err := i18n.Shared() if err != nil { return e.key } if len(e.args) == 0 { return b.Msg(lang, e.key) } out := make([]interface{}, len(e.args)) for i, a := range e.args { if inner, ok := a.(error); ok { out[i] = ErrText(lang, inner) continue } out[i] = a } return b.Msgf(lang, e.key, out...) } // Key returns the bundle key, so a caller can name it without rendering it. func (e *msgError) Key() string { return e.key } // Args returns the message's parameters as strings, for a carrier that can only hold text — the // `fa` parameters of a flash in a redirect URL. // // An ERROR argument is rendered in HUNGARIAN here, deliberately and with a known cost: the carrier is // written by one request and read by another, so the writer cannot know the reader's language, and a // nested foreign sentence (restic, docker) is not translatable anyway. The OUTER message still // follows the reader's language, which is the sentence that carries the meaning. func (e *msgError) Args() []string { out := make([]string, 0, len(e.args)) for _, a := range e.args { if inner, ok := a.(error); ok { out = append(out, ErrText(i18n.Default, inner)) continue } out = append(out, fmt.Sprintf("%v", a)) } return out } // Unwrap returns the kind FIRST and then every error among the arguments, so `errors.Is` answers for // the decision signal and for the cause alike. func (e *msgError) Unwrap() []error { var out []error if e.kind != nil { out = append(out, e.kind) } for _, a := range e.args { if inner, ok := a.(error); ok && inner != nil { out = append(out, inner) } } return out } // Msg is the interface a message-carrying error satisfies. Declared so a caller can test for the // capability rather than for this concrete type — the point is "does this error know its key", not // "was it built by this constructor". type Msg interface { error Text(lang string) string Key() string Args() []string } // AsMsg reports whether err (or anything it wraps) carries a message key, and returns it. func AsMsg(err error) (Msg, bool) { var m Msg if errors.As(err, &m) { return m, true } return nil, false } // ErrText is the one function every display end calls: the error's sentence in lang. // // An error that carries a key is rendered in lang. **Anything else is returned verbatim** — a restic // message, a docker message, an ssh message, a Go stdlib error. That is not a gap: those sentences // are not written here and are not translated here (10-localisation.md §9, the rule R-553 // established). A customer seeing restic's own English is seeing what restic said. func ErrText(lang string, err error) string { if err == nil { return "" } if m, ok := AsMsg(err); ok { return m.Text(lang) } return err.Error() } // Text renders a bundle message in a KNOWN language — for a producer that has no request. // // Release C (v0.254.0) uses it for the sentences a background run SAVES: the note under last night's // backup, the last error, the proof result. Those are written at 03:00 and read days later, so there // is no reader to ask; the operator ruling (slice 2 §16, option 1) is that they are written in the // box's language AT WRITE TIME and shown verbatim afterwards. // // The consequence, stated rather than hidden: a household that switches language sees last night's // note in the old language until the next run rewrites it. The alternative — storing a code and // rendering live — needs a dozen new persisted fields and a legacy path for each, which is the // R-570 shape a dozen times over. func Text(lang, key string, args ...interface{}) string { b, err := i18n.Shared() if err != nil { return key } if len(args) == 0 { return b.Msg(lang, key) } return b.Msgf(lang, key, args...) }