feat(12-01): add Laravel request validation to lagoon

- lagoon.ValidateRequest ports Laravel 9 request validation: wildcard
  expansion, implicit-rule stop, bail, sometimes/nullable/blank skipping,
  size messages split by type and character-counted string lengths
- ParseRules, In, CustomRule, UploadedFile and ErrorKeys for rule tables
- pl/en lagoon::validation catalogs ported verbatim from WinterCMS
- lagoon.Validate answers a numeric range failure with the bound that
  failed (min, max or numeric between) instead of always max
This commit is contained in:
Jakub Zych
2026-10-02 11:25:36 +02:00
parent cfe568a214
commit f9b7f2ea33
11 changed files with 2598 additions and 37 deletions

View File

@@ -12,7 +12,7 @@ phrasebook owns every translatable string in a SummerCMS application. At boot, `
- YAML catalogs laid out as `lang/<locale>/<group>.yaml`; nested maps flatten into keys such as `acme.blog::posts.title` (`phrasebook.Catalog.Load`). Duplicate keys, duplicate namespaces, malformed paths and non-string leaves fail at load time.
- Overrides laid out as `lang/<locale>/<namespace>/<group>.yaml` that replace keys of any loaded namespace, including the framework admin strings, and may add locales (`phrasebook.Catalog.Override`).
- Built-in framework namespaces: `lagoon::validate.*` (validation messages) and `backend::lang.*` (admin UI strings), shipped for `en` and `pl`.
- Built-in framework namespaces: `lagoon::validate.*` (the messages of `lagoon.Validate`), `lagoon::validation.*` (the WinterCMS/Laravel validator catalog used by `lagoon.ValidateRequest`, with the size messages split into `numeric`, `file`, `string` and `array`; a key the Polish file lacks, such as `after_or_equal`, falls back to English as in WinterCMS) and `backend::lang.*` (admin UI strings), shipped for `en` and `pl`.
- Lookups by request locale (`phrasebook.Translator.Get`, read from the context through `towel.Locale`) or by explicit locale (`phrasebook.Translator.GetIn`), walking the locale, its parent tags (`pt-BR` to `pt`) and then the fallback locale. A missing key returns the key itself.
- Laravel-style placeholders: `:name`, `:Name` (first letter upper-cased) and `:NAME` (upper-cased).
- Pluralization with `phrasebook.Translator.Choice` and `phrasebook.Translator.ChoiceIn`: CLDR plural maps (`one`, `few`, `many`, `other`, ...) validated against the locale's categories, or Laravel pipes with exact (`{0}`) and range (`[2,*]`) conditions. `:count` is filled in automatically.

View File

@@ -0,0 +1,102 @@
# Port of WinterCMS modules/system/lang/en/validation.php (Laravel validator
# messages). Text is copied verbatim; keys absent from this locale fall back to
# app.fallback_locale as in Winter. Loaded as lagoon::validation.*
accepted: "The :attribute must be accepted."
active_url: "The :attribute is not a valid URL."
after: "The :attribute must be a date after :date."
after_or_equal: "The :attribute must be a date after or equal to :date."
alpha: "The :attribute may only contain letters."
alpha_dash: "The :attribute may only contain letters, numbers, and dashes."
alpha_num: "The :attribute may only contain letters and numbers."
array: "The :attribute must be an array."
before: "The :attribute must be a date before :date."
before_or_equal: "The :attribute must be a date before or equal to :date."
between:
numeric: "The :attribute must be between :min and :max."
file: "The :attribute must be between :min and :max kilobytes."
string: "The :attribute must be between :min and :max characters."
array: "The :attribute must have between :min and :max items."
boolean: "The :attribute field must be true or false."
confirmed: "The :attribute confirmation does not match."
date: "The :attribute is not a valid date."
date_equals: "The :attribute must be a date equal to :date."
date_format: "The :attribute does not match the format :format."
different: "The :attribute and :other must be different."
digits: "The :attribute must be :digits digits."
digits_between: "The :attribute must be between :min and :max digits."
dimensions: "The :attribute has invalid image dimensions."
distinct: "The :attribute field has a duplicate value."
email: "The :attribute must be a valid email address."
ends_with: "The :attribute must end with one of the following: :values."
exists: "The selected :attribute is invalid."
file: "The :attribute must be a file."
filled: "The :attribute field must have a value."
gt:
numeric: "The :attribute must be greater than :value."
file: "The :attribute must be greater than :value kilobytes."
string: "The :attribute must be greater than :value characters."
array: "The :attribute must have more than :value items."
gte:
numeric: "The :attribute must be greater than or equal :value."
file: "The :attribute must be greater than or equal :value kilobytes."
string: "The :attribute must be greater than or equal :value characters."
array: "The :attribute must have :value items or more."
image: "The :attribute must be an image."
in: "The selected :attribute is invalid."
in_array: "The :attribute field does not exist in :other."
integer: "The :attribute must be an integer."
ip: "The :attribute must be a valid IP address."
ipv4: "The :attribute must be a valid IPv4 address."
ipv6: "The :attribute must be a valid IPv6 address."
json: "The :attribute must be a valid JSON string."
lt:
numeric: "The :attribute must be less than :value."
file: "The :attribute must be less than :value kilobytes."
string: "The :attribute must be less than :value characters."
array: "The :attribute must have less than :value items."
lte:
numeric: "The :attribute must be less than or equal :value."
file: "The :attribute must be less than or equal :value kilobytes."
string: "The :attribute must be less than or equal :value characters."
array: "The :attribute must not have more than :value items."
max:
numeric: "The :attribute may not be greater than :max."
file: "The :attribute may not be greater than :max kilobytes."
string: "The :attribute may not be greater than :max characters."
array: "The :attribute may not have more than :max items."
mimes: "The :attribute must be a file of type: :values."
mimetypes: "The :attribute must be a file of type: :values."
min:
numeric: "The :attribute must be at least :min."
file: "The :attribute must be at least :min kilobytes."
string: "The :attribute must be at least :min characters."
array: "The :attribute must have at least :min items."
not_in: "The selected :attribute is invalid."
not_regex: "The :attribute format is invalid."
numeric: "The :attribute must be a number."
present: "The :attribute field must be present."
regex: "The :attribute format is invalid."
required: "The :attribute field is required."
required_if: "The :attribute field is required when :other is :value."
required_unless: "The :attribute field is required unless :other is in :values."
required_with: "The :attribute field is required when :values is present."
required_with_all: "The :attribute field is required when :values is present."
required_without: "The :attribute field is required when :values is not present."
required_without_all: "The :attribute field is required when none of :values are present."
same: "The :attribute and :other must match."
size:
numeric: "The :attribute must be :size."
file: "The :attribute must be :size kilobytes."
string: "The :attribute must be :size characters."
array: "The :attribute must contain :size items."
starts_with: "The :attribute must start with one of the following: :values."
string: "The :attribute must be a string."
timezone: "The :attribute must be a valid zone."
unique: "The :attribute has already been taken."
uploaded: "The :attribute failed to upload."
url: "The :attribute format is invalid."
uuid: "The :attribute must be a valid UUID."
custom:
attribute-name:
rule-name: "custom-message"
attributes: {}

View File

@@ -0,0 +1,69 @@
# Port of WinterCMS modules/system/lang/pl/validation.php (Laravel validator
# messages). Text is copied verbatim; keys absent from this locale fall back to
# app.fallback_locale as in Winter. Loaded as lagoon::validation.*
accepted: ":attribute musi zostać zaakceptowany."
active_url: ":attribute jest nieprawidłowym adresem URL."
after: ":attribute musi być datą późniejszą od :date."
alpha: ":attribute może zawierać jedynie litery."
alpha_dash: ":attribute może zawierać jedynie litery, cyfry i myślniki."
alpha_num: ":attribute może zawierać jedynie litery i cyfry."
array: ":attribute musi być tablicą."
before: ":attribute musi być datą wcześniejszą od :date."
between:
numeric: ":attribute musi zawierać się w granicach :min - :max."
file: ":attribute musi zawierać się w granicach :min - :max kilobajtów."
string: ":attribute musi zawierać się w granicach :min - :max znaków."
array: ":attribute musi składać się z :min - :max elementów."
boolean: ":attribute musi mieć wartość prawda albo fałsz"
confirmed: "Potwierdzenie :attribute nie zgadza się."
date: ":attribute nie jest prawidłową datą."
date_format: ":attribute nie jest w formacie :format."
different: ":attribute oraz :other muszą się różnić."
digits: ":attribute musi składać się z :digits cyfr."
digits_between: ":attribute musi mieć od :min do :max cyfr."
dimensions: "The :attribute has invalid image dimensions."
distinct: "The :attribute field has a duplicate value."
email: "Format :attribute jest nieprawidłowy."
exists: "Zaznaczony :attribute jest nieprawidłowy."
file: "The :attribute must be a file."
filled: "Pole :attribute jest wymagane."
image: ":attribute musi być obrazkiem."
in: "Zaznaczony :attribute jest nieprawidłowy."
in_array: "The :attribute field does not exist in :other."
integer: ":attribute musi być liczbą całkowitą."
ip: ":attribute musi być prawidłowym adresem IP."
json: "The :attribute must be a valid JSON string."
max:
numeric: ":attribute nie może być większy niż :max."
file: ":attribute nie może być większy niż :max kilobajtów."
string: ":attribute nie może być dłuższy niż :max znaków."
array: ":attribute nie może mieć więcej niż :max elementów."
mimes: ":attribute musi być plikiem typu :values."
min:
numeric: ":attribute musi być nie mniejszy od :min."
file: ":attribute musi mieć przynajmniej :min kilobajtów."
string: ":attribute musi mieć przynajmniej :min znaków."
array: ":attribute musi mieć przynajmniej :min elementów."
not_in: "Zaznaczony :attribute jest nieprawidłowy."
numeric: ":attribute musi być liczbą."
present: "The :attribute field must be present."
regex: "Format :attribute jest nieprawidłowy."
required: "Pole :attribute jest wymagane."
required_if: "Pole :attribute jest wymagane gdy :other jest :value."
required_unless: "The :attribute field is required unless :other is in :values."
required_with: "Pole :attribute jest wymagane gdy :values jest obecny."
required_with_all: "Pole :attribute jest wymagane gdy :values jest obecny."
required_without: "Pole :attribute jest wymagane gdy :values nie jest obecny."
required_without_all: "Pole :attribute jest wymagane gdy żadne z :values nie są obecne."
same: "Pole :attribute i :other muszą się zgadzać."
size:
numeric: ":attribute musi mieć :size."
file: ":attribute musi mieć :size kilobajtów."
string: ":attribute musi mieć :size znaków."
array: ":attribute musi zawierać :size elementów."
string: "The :attribute must be a string."
timezone: ":attribute musi być prawidłową strefą czasową."
unique: "Taki :attribute już występuje."
url: "Format :attribute jest nieprawidłowy."
custom: {}
attributes: {}