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":
|
case "unique":
|
||||||
uniqueTable = arg
|
uniqueTable = arg
|
||||||
case "boolean":
|
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":
|
case "email":
|
||||||
tags = append(tags, "email")
|
tags = append(tags, "email")
|
||||||
case "confirmed":
|
case "confirmed":
|
||||||
@@ -149,14 +151,43 @@ func validateField(ctx context.Context, tx *gorm.DB, model any, field, rule stri
|
|||||||
}
|
}
|
||||||
tags = withoutTag(tags, "numeric")
|
tags = withoutTag(tags, "numeric")
|
||||||
}
|
}
|
||||||
|
if required && isEmptyValue(val) {
|
||||||
|
return []string{validateMessage(ctx, tr, "required", field, nil)}, nil
|
||||||
|
}
|
||||||
|
usedBetween := betweenMin != "" && betweenMax != ""
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
tag := strings.Join(tags, ",")
|
msgs := make([]string, 0, len(tags))
|
||||||
if err := validateOnce.Var(val, tag); err != nil {
|
for _, t := range tags {
|
||||||
ruleName := "required"
|
name, _, _ := strings.Cut(t, "=")
|
||||||
if !required {
|
if name == "required" || name == "min" || name == "max" {
|
||||||
ruleName = firstNonOmit(tags)
|
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 != "" {
|
if uniqueTable != "" {
|
||||||
@@ -368,52 +399,79 @@ func withoutTag(tags []string, name string) []string {
|
|||||||
return out
|
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 {
|
func validateMessage(ctx context.Context, tr *phrasebook.Translator, rule, field string, params map[string]string) string {
|
||||||
if params == nil {
|
if params == nil {
|
||||||
params = map[string]string{}
|
params = map[string]string{}
|
||||||
}
|
}
|
||||||
params["attribute"] = field
|
params["attribute"] = laravelAttribute(field)
|
||||||
key := "lagoon.validate." + rule
|
key := "lagoon::validate." + rule
|
||||||
if tr != nil {
|
if tr != nil {
|
||||||
s := tr.Get(ctx, key, params)
|
s := tr.Get(ctx, key, params)
|
||||||
if s != "" && s != key {
|
if s != "" && s != key {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
attr := params["attribute"]
|
||||||
switch rule {
|
switch rule {
|
||||||
case "required":
|
case "required":
|
||||||
return "The " + field + " field is required."
|
return "The " + attr + " field is required."
|
||||||
case "integer":
|
case "integer":
|
||||||
return "The " + field + " must be an integer."
|
return "The " + attr + " must be an integer."
|
||||||
case "numeric":
|
case "numeric":
|
||||||
return "The " + field + " must be a number."
|
return "The " + attr + " must be a number."
|
||||||
case "unique":
|
case "unique":
|
||||||
return "The " + field + " has already been taken."
|
return "The " + attr + " has already been taken."
|
||||||
case "max":
|
case "max":
|
||||||
return "The " + field + " may not be greater than " + params["max"] + "."
|
return "The " + attr + " may not be greater than " + params["max"] + "."
|
||||||
case "min":
|
case "min":
|
||||||
return "The " + field + " must be at least " + params["min"] + "."
|
return "The " + attr + " must be at least " + params["min"] + "."
|
||||||
case "oneof":
|
case "oneof":
|
||||||
return "The selected " + field + " is invalid."
|
return "The selected " + attr + " is invalid."
|
||||||
case "email":
|
case "email":
|
||||||
return "The " + field + " must be a valid email address."
|
return "The " + attr + " must be a valid email address."
|
||||||
case "confirmed":
|
case "confirmed":
|
||||||
return "The " + field + " confirmation does not match."
|
return "The " + attr + " confirmation does not match."
|
||||||
case "different":
|
case "different":
|
||||||
return "The " + field + " and " + params["other"] + " must be different."
|
return "The " + attr + " and " + params["other"] + " must be different."
|
||||||
case "mimes":
|
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:
|
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")
|
return fmt.Errorf("phrasebook: app is nil")
|
||||||
}
|
}
|
||||||
cat := NewCatalog()
|
cat := NewCatalog()
|
||||||
|
if err := cat.Load("lagoon", systemLangFS); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for _, p := range plugins {
|
for _, p := range plugins {
|
||||||
hl, ok := any(p).(pact.HasLang)
|
hl, ok := any(p).(pact.HasLang)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
Reference in New Issue
Block a user