Skip to content

feat: localize validation failures and append per-entry codes - #697

Merged
thomasluizon merged 3 commits into
redesign/mainfrom
feature/ticket-606-validation-error-codes
Oct 4, 2026
Merged

thomasluizon merged 3 commits into
redesign/mainfrom
feature/ticket-606-validation-error-codes

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

A five-digit verification code now returns localized validation copy and a stable code for each failure. The existing errors dictionary remains a dictionary of ordered string arrays; an appended errorDetails dictionary contains matching { code, message } entries.

Refs thomasluizon/orbit-tickets#606

Intended approach

  • Replace all 149 WithMessage sites under src/ with WithCopy, including shared title and bulk-operation rules. src/Orbit.Application/Common/ValidationErrorCodes.cs names the custom codes, and ErrorCopy.Validation.cs supplies English and pt-BR copy for custom and built-in failures.
  • ValidationCopyExtensions.cs retains counted and dynamic arguments in FluentValidation state and fills its message placeholders after calling the existing ErrorCopy.TryResolve.
  • src/Orbit.Api/Middleware/ValidationExceptionHandler.cs obtains the existing scoped IRequestLanguageResolver from the request services. Stored account language therefore wins over Accept-Language, using the same resolver as LocalizedErrorResultFilter.
  • Unit coverage in ValidationLocalizationTests, ValidationExceptionHandlerTests, ValidationCopyTests and ErrorCopyTests verifies all 22 validator families, request-language precedence, message/code alignment, placeholders and catalog completeness. No domain rules or DTO fields were removed or retyped.

Assumptions

  • Append errorDetails beside errors; rejected changing the existing string arrays to objects, which would break installed clients. Field and array order are preserved.
  • Retain FluentValidation's validator-name codes for built-in rules and assign semantic constants to custom rules; rejected inventing a separate code per use of an identical built-in constraint.
  • Keep the paired UI ticket as a handoff for the orchestrator to file and link; rejected touching another repository, following the latest work-order note.

Test evidence

  1. Before implementation, dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~ValidationExceptionHandlerTests --verbosity quiet passed both unchanged tests while the defect was present.
  2. Added PortugueseRequest_LocalizesTheRealFiveDigitCodeFailure, using the real VerifyCodeCommandValidator. Before implementation, dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~PortugueseRequest_LocalizesTheRealFiveDigitCodeFailure --logger 'console;verbosity=normal' failed: actual Code must be a 6-digit number, expected O código deve ter 6 dígitos.
  3. With implementation present, temporarily replaced message = e.LocalizedMessage(isPtBr) with message = e.ErrorMessage in the handler. dotnet test tests/Orbit.Infrastructure.Tests --filter 'FullyQualifiedName~EveryValidatorFamily_ReturnsPortugueseCopyAndAlignedCodes|FullyQualifiedName~PortugueseRequest_LocalizesTheRealFiveDigitCodeFailure' --logger 'console;verbosity=normal' failed all 23 cases. Restored the resolver and ran dotnet test tests/Orbit.Infrastructure.Tests --filter 'FullyQualifiedName~ValidationLocalizationTests|FullyQualifiedName~ValidationExceptionHandlerTests' --verbosity quiet: all 32 cases passed. The bypass was never committed.
  4. dotnet test tests/Orbit.Application.Tests --filter 'FullyQualifiedName~ValidationCopyTests|FullyQualifiedName~ErrorCopyTests' --logger 'console;verbosity=normal': all 1,093 cases passed.
  5. env -u LANG dotnet build Orbit.slnx and env -u LANG dotnet test: zero build errors, all 9,041 tests passed.
  6. env LC_ALL=en_US.UTF-8 dotnet build Orbit.slnx and env LC_ALL=en_US.UTF-8 dotnet test: zero build errors, all 9,041 tests passed.
  7. rg -n 'WithMessage\("' src --glob '*.cs' found no literal sites. No added bare narration comments. Pre-commit formatting, dash, timeless, root and suppression checks passed.

External interface evidence

FluentValidation 12.1.1 was invoked directly rather than modeled with fabricated failures. The family test logs serialize the complete failures produced by the real validators. Reproduce the verification-code and audio examples below with dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~EveryValidatorFamily_ReturnsPortugueseCopyAndAlignedCodes --logger 'console;verbosity=detailed'.

The observed fields consumed here are ErrorCode, ErrorMessage, PropertyName, CustomState and FormattedMessagePlaceholderValues. Dynamic and counted state is our own ValidationCopyArguments type. Built-in Length(6) emits ExactLengthValidator and carries MaxLength and TotalLength; these names were read from a real failure. A direct installed-package invocation also confirmed descriptor components as (IPropertyValidator, IRuleComponent) tuples, their Name/ErrorCode values, and ChildValidatorAdaptor as a composition node; the guard does not treat that node as a failure.

[
  {
    "PropertyName": "Code",
    "ErrorMessage": "'Code' must be 6 characters in length. You entered 5 characters.",
    "AttemptedValue": "12345",
    "CustomState": null,
    "Severity": 0,
    "ErrorCode": "ExactLengthValidator",
    "FormattedMessagePlaceholderValues": {
      "MinLength": 6,
      "MaxLength": 6,
      "TotalLength": 5,
      "PropertyName": "Code",
      "PropertyValue": "12345",
      "PropertyPath": "Code"
    }
  },
  {
    "PropertyName": "Code",
    "ErrorMessage": "Code must be a 6-digit number",
    "AttemptedValue": "12345",
    "CustomState": {
      "Values": []
    },
    "Severity": 0,
    "ErrorCode": "VALIDATION_VERIFICATION_CODE_FORMAT",
    "FormattedMessagePlaceholderValues": {
      "RegularExpression": "^\\d{6}$",
      "PropertyName": "Code",
      "PropertyValue": "12345",
      "PropertyPath": "Code"
    }
  },
  {
    "PropertyName": "FileName",
    "ErrorMessage": "Audio format '.xyz' is not supported.",
    "AttemptedValue": "voice.xyz",
    "CustomState": {
      "Values": [
        ".xyz"
      ]
    },
    "Severity": 0,
    "ErrorCode": "VALIDATION_AUDIO_FORMAT",
    "FormattedMessagePlaceholderValues": {
      "PropertyName": "File Name",
      "PropertyValue": "voice.xyz",
      "PropertyPath": "FileName"
    }
  }
]

Manual steps

  • Deploy the API before releasing the paired UI change. After the redesign code reaches main, Thomas uses GitHub Actions > Release API > Run workflow, dispatched from main, with environment=production and branch=main. Confirm the selected commit is deployed and a pt-BR validation response contains ordered errors strings and matching errorDetails entries before releasing the consumer.
  • The orchestrator files the paired repo:ui ticket from the handoff and links it to Aggregate reminder scheduler probe #606 and this PR. That ticket must consume errorDetails, retain the legacy response fallback, and send the selected request language for signed-out validation. Its release follows the API deploy.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Reviewed the complete validation-localization change and checked the existing web/mobile error-response consumers.

  • Localized validation responses: The exception handler uses the existing request-language resolver, preserving stored-account language precedence over Accept-Language.
  • Additive error details: Ordered legacy errors string arrays remain intact, with matching per-entry codes and messages appended in errorDetails.
  • Shared validation copy: Custom validator messages move into the bilingual catalog; built-in failures retain validator-name codes, and counted/dynamic placeholders are preserved.
  • Verification: All 32 targeted Infrastructure tests and 1,093 Application catalog/copy tests passed locally. The full suite was not rerun for this review.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

The NullValidator fallback said "deve estar null" in a Portuguese sentence; it now says
"deve estar vazio", which matches its English text. The friend request identifier message
now says "exatamente um", which matches "exactly one" in English.

Refs thomasluizon/orbit-tickets#606

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thomasluizon

Copy link
Copy Markdown
Owner Author

Copy approval for the new en and pt-BR validation strings (/second-opinion, Claude Opus 5.5, framed as a claimed copy defect against ca416a91).

Verdict: AGREE, confidence medium. Two defects were named:

  1. NullValidator pt-BR said '{PropertyName}' deve estar null., an English word inside a Portuguese sentence that also did not match its English text ("must be empty"). FluentValidation 12.1.1 ships that text itself, but that does not make it correct copy.
  2. FriendIdentifierExclusive pt-BR said Informe apenas um destes, which means "only one" and drops the "exactly one" requirement of the English text.

Both are fixed in c05ef71d: '{PropertyName}' deve estar vazio. and Informe exatamente um destes: handle ou referralCode. Every other string stands as approved. The focused suites pass on c05ef71d (Orbit.Application.Tests 1093 of 1093 for the copy filter, Orbit.Infrastructure.Tests 73 of 73 for the validation filter), run with LANG unset and LC_ALL=en_US.UTF-8.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Reviewed c05ef71d against the previous review at ca416a91, with the full PR diff as context.

  • Corrected identifier guidance: Changed the Portuguese friend-request message to require exactly one identifier, matching the existing validation rule.
  • Corrected empty-value copy: Replaced the literal null in the Portuguese NullValidator message with natural Portuguese wording, without changing validation behavior or error codes.
  • Verified compatibility and tests: Checked the sibling shared error consumer and ran the Application catalog/copy and friend-request validator tests; all 1,102 passed. The full suite was not rerun.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@sonarqubecloud

sonarqubecloud Bot commented Oct 4, 2026

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit bbb555a into redesign/main Oct 4, 2026
26 checks passed
@thomasluizon
thomasluizon deleted the feature/ticket-606-validation-error-codes branch October 4, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant