Translate form validation errors, per field - #145
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ValidationPipeused to reject requests with raw English messages joined into one string — the ugliest, highest-traffic error surface in the app, and the one no businessErrorCodecould ever cover.ValidationExceptionnow 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@Matchesregex never leaks).apps/webtranslates 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.ListFormModal,ReviewFormModal) had no error handling at all before this — fixed along the way.NetworkTimeout(declared and translated, never actually thrown) and splitsmessages/{fr,en}.jsonintocommon/errors/otherso 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 registeredvalidation.exception.spec.ts(API) — params extraction, RegExp sanitizationvalidation-messages.spec.ts(web) — i18n coverage +fieldError/bannerMessagebehaviorerrors.spec.ts(web) — updated for the removed transition fallbackpnpm check(typecheck across api/web/shared) — passed on push🤖 Generated with Claude Code