У публічній формі ICanUp серверна валідація Laravel працювала правильно, але повідомлення про помилку виглядало неправильно для користувача.
Замість назви поля він міг побачити технічний шлях на кшталт answers.request_type.
Для розробника це зрозумілий ключ. Для людини, яка просто заповнює форму, він нічого не означає.
Мені потрібно було залишити структуру запиту і правила Laravel без змін, але навчити валідатор використовувати реальну локалізовану назву динамічного поля.

Проблема стала видимою лише на серверній валідації
Форма могла містити поле з ключем:
request_typeУ запиті воно природно приходило як вкладене значення:
{ "answers": { "request_type": null }}Відповідне правило Laravel працювало з повним шляхом:
'answers.request_type' => [ 'required',],Тому без додаткової інформації Laravel використовував технічний шлях як назву атрибута.
Переклад validation.php не вирішує динамічну назву поля
Спочатку тут легко подумати, що достатньо додати або виправити переклади в lang/uk/validation.php.
Але шаблон повідомлення і назва поля - це дві різні частини.
Частина | Приклад |
|---|---|
Шаблон повідомлення |
|
Технічний атрибут |
|
Назва для користувача |
|
Laravel може правильно перекласти весь текст, але якщо :attribute залишається технічним, результат усе одно поганий.
Я не хотіла змінювати answers.{key}
Сам формат answers.{field_key} у цьому випадку був правильним.
Він дає стабільний зв’язок між визначенням поля форми та відповіддю користувача, незалежно від мови назви.
Наприклад, request_type може мати різні підписи:
Locale | Назва | Ключ |
|---|---|---|
UK | Тип запиту |
|
EN | Request type |
|
Технічний ключ не повинен змінюватися разом із перекладом. Потрібно змінити лише те, як цей ключ показується у validation error.
FormRequest::attributes() виявився правильним місцем
Laravel FormRequest уже має механізм для власних назв атрибутів: метод attributes().
Саме його я використала у запиті публічної форми.
Ідея проста: для кожного активного поля форми побудувати відповідність між технічним шляхом і локалізованою назвою.
public function attributes(): array{ return [ 'answers.request_type' => 'Тип запиту', 'answers.email' => 'Електронна пошта', ];}Для статичної форми цього було б достатньо.
Але у мене reusable Forms, тому список полів заздалегідь невідомий.
Мапа атрибутів має будуватися з визначення форми
Правила і назви полів уже існують у визначенні форми. Тому немає сенсу дублювати їх у FormRequest.
Спрощено логіка виглядає так:
public function attributes(): array{ $locale = app()->getLocale(); $attributes = []; foreach ($this->formFields() as $field) { $attributes["answers.{$field->key}"] = $this->localizedFieldLabel( $field, $locale, ); } return $attributes;}Поточна locale повинна визначати назву поля
Одна й та сама форма може відкриватися українською або англійською.
Тому validation error повинен використовувати назву поля тієї самої мови, якою користувач зараз заповнює форму.
UK requestanswers.request_type ->Тип запиту EN requestanswers.request_type ->Request typeЯ не хочу отримати український текст помилки з англійською назвою поля або навпаки.
Тому назва береться через чинний механізм перекладів форми для активної locale запиту.
Запасна назва теж не повинна показувати answers.*
Є ще один важливий крайовий випадок: поле існує, але його назва для поточної мови відсутня або пошкоджена.
У такій ситуації я все одно не хочу показувати користувачу:
answers.request_typeКраще використати нейтральну зрозумілу назву.
Locale | Запасна назва |
|---|---|
UK | Поле форми |
EN | Field |
$label = $this->localizedFieldLabel( $field, $locale,); return filled($label) ? $label : __('forms.field');attributes() не змінює самі правила валідації
Це одна з причин, чому мені подобається таке рішення.
Метод attributes() не змінює структуру даних і не послаблює правила. Він впливає лише на назву, яка підставляється у повідомлення.
$rules["answers.{$field->key}"] = [ 'required', 'email', 'max:255',]; $attributes["answers.{$field->key}"] = $localizedLabel;Laravel і далі перевіряє answers.email. Користувач просто бачить Електронна пошта.
Одна назва автоматично працює для різних validation rules
Після того як Laravel знає правильну назву атрибута, її не потрібно окремо додавати для кожного правила.
Правило | Приклад результату |
|---|---|
| Поле «Електронна пошта» є обов’язковим. |
| Поле «Електронна пошта» має містити коректну електронну адресу. |
| Повідомлення використовує ту саму назву поля. |
| Повідомлення використовує ту саму назву поля. |
Варіанти вибору | Помилка використовує назву select / option поля, а не answers.* |
Checkbox | Помилка використовує назву checkbox-поля. |
Особливо важливо не хардкодити назви конкретних полів
У reusable Forms адміністратор може створити нове поле без зміни PHP-коду.
Якби я додавала кожне ім’я вручну:
return [ 'answers.request_type' => 'Тип запиту', 'answers.email' => 'Електронна пошта', 'answers.company' => 'Компанія',];то кожне нове поле вимагало б зміни застосунку.
Це суперечить самій ідеї конструктора форм.
Назва має походити з того самого FormField, що й поле на сторінці
Я хочу, щоб підпис у формі та назва у validation error мали одне джерело.
FormField translation | +-- public form label | +-- validation attribute nameТоді перейменування поля в адмінці автоматично змінює і повідомлення про помилку.
Окремої синхронізації не потрібно.
Клієнтська перевірка не замінює серверну
Навіть якщо Vue вже показує required або перевіряє формат email до надсилання, сервер не може покладатися на це як на джерело істини.
Клієнтську перевірку можна обійти, а запит можна надіслати напряму.
Тому Form definition продовжує формувати серверні правила Laravel, а клієнтська частина лише покращує зручність.
attributes() не повинен дозволяти несподівані поля
Ще один важливий момент: наявність красивої назви атрибута не повинна змінювати контракт безпеки форми.
Якщо користувач надсилає поле, якого немає у визначенні форми, воно не стає дозволеним лише тому, що вкладене в answers.
{ "answers": { "request_type": "support", "unexpected_admin_field": "1" }}Перелік допустимих ключів і правила валідації все одно повинні походити з актуального визначення Form.
Я перевіряла не один required
Такий bug легко виправити для одного прикладу і випадково залишити інші типи полів із технічними назвами.
Тому перевірка охоплює різні правила і типи динамічних полів.
Сценарій | Що перевіряється |
|---|---|
UK required | Українська назва поля |
EN required | Англійська назва поля |
Той самий localized attribute | |
min / max | Назва не повертається до answers.* |
select / options | Назва динамічного поля зберігається |
checkbox | Повідомлення використовує назву FormField |
Відсутній переклад | Людська запасна назва замість технічного ключа |
Тест повинен перевіряти не тільки 422
Сам факт відповіді 422 Unprocessable Entity не доводить, що користувач отримав хороший текст.
Я хочу перевіряти і конкретне поле помилки, і те, що технічний ключ не потрапив у повідомлення.
$response->assertUnprocessable(); $response->assertJsonValidationErrors([ 'answers.request_type',]); $response->assertJsonMissing([ 'The answers.request type field is required.',]);Ключ помилки і текст помилки мають різні задачі
Я не намагалася замінити сам ключ answers.request_type у validation response.
Клієнтській частині цей шлях корисний: за ним вона розуміє, біля якого поля показати помилку.
Людським має бути саме повідомлення.
Частина помилки | Для кого |
|---|---|
| Vue / application code |
«Тип запиту» | Користувач |
Що я не роблю після такого бага
Не перейменовую
answers.{key}залежно від мови.Не хардкодити назви кожного FormField у FormRequest.
Не створюю окрему систему validation messages лише для Forms.
Не замінюю серверну валідацію клієнтською.
Не показую raw
answers.*як запасну назву.Не використовую назву з іншої locale, якщо поточний переклад відсутній.
Не змінюю правила валідації лише заради красивішого повідомлення.
Не вважаю відповідь 422 достатньою перевіркою якості validation UX.
Мій короткий checklist для динамічної валідації
- Залишити стабільний технічний ключ поля.
- Будувати серверні rules із визначення Form.
- Будувати
attributes()із тих самих FormField. - Використовувати назву активної locale.
- Мати зрозумілу запасну назву при відсутньому перекладі.
- Не показувати користувачу raw
answers.*. - Перевірити required, email, min, max і поля вибору.
- Перевірити UK та EN окремо.
- Залишити validation key стабільним для клієнтського коду.
- Не змінювати контракт persistence або Communication integration через локалізацію повідомлення.
Технічний ключ потрібен застосунку. Назва поля потрібна користувачу. Laravel дозволяє зберегти обидва, не змішуючи їх.
Висновок
У випадку з формами ICanUp серверна валідація не була неправильною. Вона просто знала поле за технічним шляхом answers.{key} і не знала, як це поле називається для людини.
Переклад стандартних Laravel validation messages вирішує лише частину задачі. Для динамічних форм потрібно окремо визначити відображувані назви атрибутів.
FormRequest::attributes() дозволив залишити структуру запиту, правила валідації та validation bag незмінними, але підставляти назву реального FormField для поточної мови.
Якщо перекладу немає, користувач отримує нейтральну назву Поле форми, а не внутрішній шлях answers.request_type.
Для мене правильна локалізація валідації означає не тільки перекласти слово required. Вона означає, що користувач узагалі не повинен бачити внутрішню структуру даних застосунку.



