Skip to content

Translate form validation errors, per field - #145

Merged
Logan2234 merged 5 commits into
mainfrom
feat/translate-validation-errors
Aug 27, 2026
Merged

Translate form validation errors, per field#145
Logan2234 merged 5 commits into
mainfrom
feat/translate-validation-errors

Conversation

@Logan2234

Copy link
Copy Markdown
Owner

Summary

  • class-validator's ValidationPipe used to reject requests with raw English messages joined into one string — the ugliest, highest-traffic error surface in the app, and the one no business ErrorCode could ever cover.
  • ValidationException now also carries each failing constraint's raw, positional arguments (e.g. [8] for @MinLength(8)), read directly from class-validator's own metadata storage — zero DTO decorators touched, values never parsed out of English prose, and sanitized before they leave the API (primitives only, arrays truncated, a @Matches regex never leaks).
  • apps/web translates each field's constraint into the visitor's locale and shows it under the relevant input (fieldError()); the generic banner (bannerMessage()) is now suppressed only when every detail is already covered by a per-field slot, so small forms don't show the same error twice while anything without a slot (network/5xx/429, a nested path, an untracked field) still surfaces there.
  • VALIDATION_CONSTRAINT_NAMES (packages/shared) is a registry both sides check against via dedicated specs, so a DTO using an untranslated constraint fails a test before it ships.
  • Rolled out to ~17 forms: auth, settings (profile/security/MFA/danger zone), list/review/reading-goal modals, admin (user deletion, report take-down, communications). Two modals (ListFormModal, ReviewFormModal) had no error handling at all before this — fixed along the way.
  • Drops NetworkTimeout (declared and translated, never actually thrown) and splits messages/{fr,en}.json into common/errors/other so the error-translation files aren't buried in ~700 unrelated UI strings.

Test plan

  • validation-constraint-names.spec.ts (API) — every class-validator constraint used across all DTOs is registered
  • validation.exception.spec.ts (API) — params extraction, RegExp sanitization
  • validation-messages.spec.ts (web) — i18n coverage + fieldError/bannerMessage behavior
  • errors.spec.ts (web) — updated for the removed transition fallback
  • Manual check: forgot-password with a malformed email shows both the field message and the banner as expected
  • pnpm check (typecheck across api/web/shared) — passed on push

🤖 Generated with Claude Code

Logan2234 and others added 4 commits August 27, 2026 22:22
class-validator's ValidationPipe rejected requests with raw English
messages ("email must be an email"), joined and dumped as one string in
a single global banner — the ugliest, highest-traffic error surface in
the app, and the one no business ErrorCode could ever cover.

ValidationException now also carries each failing constraint's raw,
positional arguments (e.g. [8] for @minlength(8)) by reading them
straight from class-validator's own metadata storage, matched by
validator name rather than .type (every built-in decorator registers as
CUSTOM_VALIDATION) — zero DTO decorators touched, and values are never
parsed out of English prose. Sanitized before they ever leave the API:
primitives only, arrays truncated, a RegExp (@matches) never leaks.

apps/web translates each field's constraint name into the visitor's
locale and renders it under the relevant input (fieldError()), while
resolveApiError()'s generic banner is now suppressed only when every
detail already has a per-field slot (bannerMessage()) — so a small form
with one tracked field doesn't show the same error twice, but anything
without a slot (a nested path, an untracked field, a network/5xx/429)
still surfaces there, unconditionally.

VALIDATION_CONSTRAINT_NAMES (packages/shared) is the registry both
sides check against: validation-constraint-names.spec.ts fails if a DTO
starts using a constraint missing from it, validation-messages.spec.ts
fails if the translation table doesn't cover it.

Reference forms only (register/login/forgot-password) — the rest of
the app's forms follow in a mechanical pass. Also drops NetworkTimeout,
an ErrorCode declared and translated but never actually thrown anywhere,
and splits messages/{fr,en}.json into common/errors/other so the
error-translation files stop being buried in ~700 unrelated UI strings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pre-commit already lints/formats every staged file and pre-push already
typechecks the whole project — running lint/format/typecheck myself
mid-task duplicates that gate for nothing. Same for tests: CI runs the
full suite plus e2e on every PR, so a reflexive test run after every
batch of edits is wasted effort too — reserve it for cases where a
specific change plausibly broke something.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"Only for a bug fix or a risky refactor" was too narrow — the actual
call is whether the change is substantial enough to plausibly break
something. A style tweak, a Paraglide message wording change, or a
variable/route rename doesn't warrant a test run either way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mechanical rollout of fieldError()/the fieldError-then-resolveApiError
fallback chain (see the previous commit) across settings, admin, and the
list/review/reading-goal modals — every form with a text/number input
bound to a validated DTO field now shows the specific reason under that
field instead of only ever the generic banner.

ListFormModal and ReviewFormModal previously had no catch block at all
(a failed save/delete just silently did nothing) — both now surface an
error like every other form.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation api apps/api (NestJS) web apps/web (SvelteKit) shared packages/shared labels Aug 27, 2026
@Logan2234
Logan2234 merged commit a15c767 into main Aug 27, 2026
14 of 15 checks passed
@Logan2234
Logan2234 deleted the feat/translate-validation-errors branch August 27, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api apps/api (NestJS) documentation Improvements or additions to documentation shared packages/shared web apps/web (SvelteKit)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant