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:
Jakub Zych
2026-09-22 23:56:58 +02:00
parent dd3cb7ac29
commit 31634f706f
5 changed files with 124 additions and 31 deletions

View File

@@ -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
}