feat: release v0.26 portable i18n and presentation adapters
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# 统一错误目录
|
||||
|
||||
v0.26 新增的通用多语言适配见 [i18n](../i18n/README.md)。目录条目可显式引用 `translation: {namespace, key}`;`errori18n.Bind/Source` 支持从同一份 Go 目录生成源词典。翻译只影响公开展示,不改变业务码、HTTP 状态、失败语义、参数契约或 cause。没有引用或没有启用适配时沿用现有行为;幂等重放保留首次语言。该能力需要 v0.26.0-preview 或更新版本。
|
||||
|
||||
从 `v0.10.0-preview` 起,业务可以只返回逻辑错误标识,框架从项目目录取得 HTTP 状态、对外业务码与公开提示。目录不依赖具体业务、数据库、身份模型或网络层,也不定义固定返回体。
|
||||
|
||||
## 基本规则
|
||||
|
||||
@@ -9,16 +9,19 @@ import (
|
||||
"math"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Definition belongs to the project. Code is the public string/integer code;
|
||||
// the Catalog map key is the distinct logical identifier used by ByCode.
|
||||
// Nil Code retains legacy response code mapping (useful when migrating).
|
||||
type Definition struct {
|
||||
Status int `json:"status,omitempty"`
|
||||
Code any `json:"code,omitempty"`
|
||||
Message string `json:"message"`
|
||||
Params []Parameter `json:"params,omitempty"`
|
||||
Status int `json:"status,omitempty"`
|
||||
Code any `json:"code,omitempty"`
|
||||
Message string `json:"message"`
|
||||
Params []Parameter `json:"params,omitempty"`
|
||||
Translation *Translation `json:"translation,omitempty"`
|
||||
}
|
||||
|
||||
type Catalog struct {
|
||||
@@ -99,6 +102,10 @@ func (c Catalog) Clone() Catalog {
|
||||
out := c
|
||||
out.Definitions = make(map[string]Definition, len(c.Definitions))
|
||||
for key, d := range c.Definitions {
|
||||
if d.Translation != nil {
|
||||
ref := *d.Translation
|
||||
d.Translation = &ref
|
||||
}
|
||||
if d.Params != nil {
|
||||
params := make([]Parameter, len(d.Params))
|
||||
copy(params, d.Params)
|
||||
@@ -126,6 +133,13 @@ func NewRegistry(c Catalog) (*Registry, error) {
|
||||
}
|
||||
messages := map[string][]messagePart{}
|
||||
for key, d := range c.Definitions {
|
||||
if d.Translation != nil {
|
||||
for _, value := range []string{d.Translation.Namespace, d.Translation.Key} {
|
||||
if value == "" || len(value) > 256 || !utf8.ValidString(value) || strings.TrimSpace(value) != value || strings.IndexFunc(value, unicode.IsControl) >= 0 {
|
||||
return nil, fmt.Errorf("errors: %s has invalid translation reference", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(key) == "" || strings.TrimSpace(key) != key || strings.ContainsAny(key, "\r\n\t") {
|
||||
return nil, fmt.Errorf("errors: invalid logical identifier %q", key)
|
||||
}
|
||||
|
||||
46
errors/localization.go
Normal file
46
errors/localization.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package apierrors
|
||||
|
||||
import "context"
|
||||
|
||||
// Translation is an explicit reference into an independently owned dictionary.
|
||||
// It does not change the logical error key, status, public code or parameters.
|
||||
type Translation struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// Localizer is an optional presentation adapter. false preserves the original
|
||||
// public message. Implementations must support concurrent calls and must not
|
||||
// mutate registry or failure, or expose causes/parameter values in diagnostics.
|
||||
type Localizer interface {
|
||||
Localize(context.Context, *Registry, *APIError) (string, bool)
|
||||
}
|
||||
|
||||
// Definition returns detached metadata for the effective catalog scope.
|
||||
func (r *Registry) Definition(key string) (Definition, bool) {
|
||||
if r == nil {
|
||||
return Definition{}, false
|
||||
}
|
||||
d, ok := r.catalog.Definitions[key]
|
||||
if d.Params != nil {
|
||||
d.Params = append([]Parameter{}, d.Params...)
|
||||
}
|
||||
if d.Translation != nil {
|
||||
ref := *d.Translation
|
||||
d.Translation = &ref
|
||||
}
|
||||
return d, ok
|
||||
}
|
||||
|
||||
// Parameters returns detached public template arguments. This is for explicit
|
||||
// presentation adapters, not automatic serialization into an API response.
|
||||
func (e *APIError) Parameters() map[string]any {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]any, len(e.params))
|
||||
for k, v := range e.params {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user