feat(07-07): localize lagoon validation with Laravel-shaped Polish messages
Replay of the user-api corpus needs lagoon::validate.* catalogs and underscore-to-space attribute names so Go 422 bodies match Winter. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -81,7 +81,9 @@ func validateField(ctx context.Context, tx *gorm.DB, model any, field, rule stri
|
||||
case "unique":
|
||||
uniqueTable = arg
|
||||
case "boolean":
|
||||
// Go's bool field type already enforces this; treat as a type-check no-op.
|
||||
if !isLaravelBoolean(val) {
|
||||
return []string{validateMessage(ctx, tr, "boolean", field, nil)}, nil
|
||||
}
|
||||
case "email":
|
||||
tags = append(tags, "email")
|
||||
case "confirmed":
|
||||
@@ -149,14 +151,43 @@ func validateField(ctx context.Context, tx *gorm.DB, model any, field, rule stri
|
||||
}
|
||||
tags = withoutTag(tags, "numeric")
|
||||
}
|
||||
if required && isEmptyValue(val) {
|
||||
return []string{validateMessage(ctx, tr, "required", field, nil)}, nil
|
||||
}
|
||||
usedBetween := betweenMin != "" && betweenMax != ""
|
||||
if len(tags) > 0 {
|
||||
tag := strings.Join(tags, ",")
|
||||
if err := validateOnce.Var(val, tag); err != nil {
|
||||
ruleName := "required"
|
||||
if !required {
|
||||
ruleName = firstNonOmit(tags)
|
||||
msgs := make([]string, 0, len(tags))
|
||||
for _, t := range tags {
|
||||
name, _, _ := strings.Cut(t, "=")
|
||||
if name == "required" || name == "min" || name == "max" {
|
||||
continue
|
||||
}
|
||||
return []string{validateMessage(ctx, tr, ruleName, field, nil)}, nil
|
||||
if err := validateOnce.Var(val, t); err != nil {
|
||||
ruleName := name
|
||||
if name == "oneof" {
|
||||
ruleName = "oneof"
|
||||
}
|
||||
msgs = append(msgs, validateMessage(ctx, tr, ruleName, field, map[string]string{
|
||||
"min": betweenMin, "max": betweenMax,
|
||||
}))
|
||||
}
|
||||
}
|
||||
if usedBetween && !numericRange {
|
||||
n := 0
|
||||
if s, ok := val.(string); ok {
|
||||
n = len(s)
|
||||
} else if !isEmptyValue(val) {
|
||||
n = len(fmt.Sprint(val))
|
||||
}
|
||||
minN, maxN := atoiOr(betweenMin, 0), atoiOr(betweenMax, 0)
|
||||
if n < minN || (maxN > 0 && n > maxN) {
|
||||
msgs = append(msgs, validateMessage(ctx, tr, "between", field, map[string]string{
|
||||
"min": betweenMin, "max": betweenMax,
|
||||
}))
|
||||
}
|
||||
}
|
||||
if len(msgs) > 0 {
|
||||
return msgs, nil
|
||||
}
|
||||
}
|
||||
if uniqueTable != "" {
|
||||
@@ -368,52 +399,79 @@ func withoutTag(tags []string, name string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func firstNonOmit(tags []string) string {
|
||||
for _, t := range tags {
|
||||
name, _, _ := strings.Cut(t, "=")
|
||||
if name != "omitempty" && name != "required" {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return "invalid"
|
||||
}
|
||||
|
||||
func validateMessage(ctx context.Context, tr *phrasebook.Translator, rule, field string, params map[string]string) string {
|
||||
if params == nil {
|
||||
params = map[string]string{}
|
||||
}
|
||||
params["attribute"] = field
|
||||
key := "lagoon.validate." + rule
|
||||
params["attribute"] = laravelAttribute(field)
|
||||
key := "lagoon::validate." + rule
|
||||
if tr != nil {
|
||||
s := tr.Get(ctx, key, params)
|
||||
if s != "" && s != key {
|
||||
return s
|
||||
}
|
||||
}
|
||||
attr := params["attribute"]
|
||||
switch rule {
|
||||
case "required":
|
||||
return "The " + field + " field is required."
|
||||
return "The " + attr + " field is required."
|
||||
case "integer":
|
||||
return "The " + field + " must be an integer."
|
||||
return "The " + attr + " must be an integer."
|
||||
case "numeric":
|
||||
return "The " + field + " must be a number."
|
||||
return "The " + attr + " must be a number."
|
||||
case "unique":
|
||||
return "The " + field + " has already been taken."
|
||||
return "The " + attr + " has already been taken."
|
||||
case "max":
|
||||
return "The " + field + " may not be greater than " + params["max"] + "."
|
||||
return "The " + attr + " may not be greater than " + params["max"] + "."
|
||||
case "min":
|
||||
return "The " + field + " must be at least " + params["min"] + "."
|
||||
return "The " + attr + " must be at least " + params["min"] + "."
|
||||
case "oneof":
|
||||
return "The selected " + field + " is invalid."
|
||||
return "The selected " + attr + " is invalid."
|
||||
case "email":
|
||||
return "The " + field + " must be a valid email address."
|
||||
return "The " + attr + " must be a valid email address."
|
||||
case "confirmed":
|
||||
return "The " + field + " confirmation does not match."
|
||||
return "The " + attr + " confirmation does not match."
|
||||
case "different":
|
||||
return "The " + field + " and " + params["other"] + " must be different."
|
||||
return "The " + attr + " and " + params["other"] + " must be different."
|
||||
case "mimes":
|
||||
return "The " + field + " must be a file of the allowed types."
|
||||
return "The " + attr + " must be a file of the allowed types."
|
||||
case "between":
|
||||
return "The " + attr + " must be between " + params["min"] + " and " + params["max"] + " characters."
|
||||
case "boolean":
|
||||
return "The " + attr + " field must be true or false."
|
||||
default:
|
||||
return "The " + field + " is invalid."
|
||||
return "The " + attr + " is invalid."
|
||||
}
|
||||
}
|
||||
|
||||
func laravelAttribute(field string) string {
|
||||
return strings.ReplaceAll(field, "_", " ")
|
||||
}
|
||||
|
||||
func isLaravelBoolean(val any) bool {
|
||||
switch v := val.(type) {
|
||||
case bool:
|
||||
return true
|
||||
case string:
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case "0", "1", "true", "false":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case float64:
|
||||
return v == 0 || v == 1
|
||||
case int:
|
||||
return v == 0 || v == 1
|
||||
default:
|
||||
s := strings.TrimSpace(fmt.Sprint(val))
|
||||
return s == "0" || s == "1" || s == "true" || s == "false"
|
||||
}
|
||||
}
|
||||
|
||||
func atoiOr(s string, fallback int) int {
|
||||
n, err := strconv.Atoi(strings.TrimSpace(s))
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
6
phrasebook/lang.go
Normal file
6
phrasebook/lang.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package phrasebook
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed lang
|
||||
var systemLangFS embed.FS
|
||||
13
phrasebook/lang/en/validate.yaml
Normal file
13
phrasebook/lang/en/validate.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
required: "The :attribute field is required."
|
||||
email: "The :attribute must be a valid email address."
|
||||
between: "The :attribute must be between :min and :max characters."
|
||||
boolean: "The :attribute field must be true or false."
|
||||
unique: "The :attribute has already been taken."
|
||||
confirmed: "The :attribute confirmation does not match."
|
||||
different: "The :attribute and :other must be different."
|
||||
integer: "The :attribute must be an integer."
|
||||
numeric: "The :attribute must be a number."
|
||||
max: "The :attribute may not be greater than :max."
|
||||
min: "The :attribute must be at least :min."
|
||||
oneof: "The selected :attribute is invalid."
|
||||
mimes: "The :attribute must be a file of the allowed types."
|
||||
13
phrasebook/lang/pl/validate.yaml
Normal file
13
phrasebook/lang/pl/validate.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
required: "Pole :attribute jest wymagane."
|
||||
email: "Format :attribute jest nieprawidłowy."
|
||||
between: ":attribute musi zawierać się w granicach :min - :max znaków."
|
||||
boolean: ":attribute musi mieć wartość prawda albo fałsz"
|
||||
unique: "Taki :attribute już występuje."
|
||||
confirmed: "Potwierdzenie :attribute nie zgadza się."
|
||||
different: ":attribute oraz :other muszą się różnić."
|
||||
integer: ":attribute musi być liczbą całkowitą."
|
||||
numeric: ":attribute musi być liczbą."
|
||||
max: ":attribute nie może być dłuższy niż :max znaków."
|
||||
min: ":attribute musi mieć przynajmniej :min znaków."
|
||||
oneof: "Zaznaczony :attribute jest nieprawidłowy."
|
||||
mimes: ":attribute musi być plikiem typu :values."
|
||||
@@ -88,6 +88,9 @@ func Activate[P interface{ ID() string }](app *backpack.App, plugins []P) error
|
||||
return fmt.Errorf("phrasebook: app is nil")
|
||||
}
|
||||
cat := NewCatalog()
|
||||
if err := cat.Load("lagoon", systemLangFS); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range plugins {
|
||||
hl, ok := any(p).(pact.HasLang)
|
||||
if !ok {
|
||||
|
||||
Reference in New Issue
Block a user