Skip to content

feat(localization): module catalogs and localized messages (2/4) - #1361

Open
marcelo-maciel wants to merge 1 commit into
fullstackhero:mainfrom
marcelo-maciel:feat/i18n-modules
Open

feat(localization): module catalogs and localized messages (2/4)#1361
marcelo-maciel wants to merge 1 commit into
fullstackhero:mainfrom
marcelo-maciel:feat/i18n-modules

Conversation

@marcelo-maciel

@marcelo-maciel marcelo-maciel commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Module slice of the i18n work, split out of #1344. 229 files, and mechanical: one catalog per module plus the MessageKey / ResourceSource wiring at each throw and validation site.

This PR does not build until the framework slice merges, and the CI failure below is that and nothing else. It uses SharedResources, SupportedCultures and the MessageKey / MessageArgs / ResourceSource members on CustomException, none of which exist on main yet. A PR opened from a fork cannot be based on another PR's branch, so there is no way to stack it. It is up now so you can see the whole shape at once; rebasing it on main after the framework PR lands turns it green with no content change. Verified rather than assumed: framework + this slice together build clean and run the full suite green, and the combined tree is byte-identical to #1344 on every backend path.

The split is four PRs rather than three, because these ~220 files fit none of your three and would have put the framework PR back at ~285.

Slice Files PR
Framework 56 #1360review this one first
Module catalogs and wiring 229 this one
clients/admin 111 #1362 — independent
clients/dashboard 134 #1363 — independent

⚠️ Touches protected src/BuildingBlocks (Golden Rule #4)

One file: Web/Validation/PagedQueryValidator.cs. Its constructor now takes IStringLocalizer<SharedResources> so the paging messages localize. All three subclasses are in modules — GetAuditsQueryValidator, GetTenantsQueryValidator, SearchUsersQueryValidator — so the base class travels with its callers. Leaving it in the framework PR would either break that PR's build (its subclasses on main call the old constructor) or drag AuditingResources and GetAuditsQueryHandler in with it, for no review benefit. The other seventeen BuildingBlocks files are declared in the framework PR.

What is in here

  • One {Module}Resources catalog per module, en-US neutral plus *.pt-BR.resx, for Auditing, Billing, Catalog, Chat, Files, Multitenancy, Notifications, Tickets and Webhooks, plus the Identity messages outside the Locale plumbing.
  • Throw sites carry MessageKey and ResourceSource. Message stays English, so logs and audit records remain culture-independent while the response body localizes. This is where the 41 Conflict throws across Billing and Catalog live — the ones that #1344 had briefly answering 409 with "an unexpected error occurred" until TitleKeyFor was fixed in the framework slice.
  • Validators resolve their messages through IStringLocalizer. Message arguments are kept culture-insensitive: GetAuditsQueryHandler.MaxWindowDays is a new int constant, replacing MaxWindow.TotalDays as a double in the two audit-window validators. The localizer formats with string.Format under CurrentCulture, and a double in a localized message is culture-sensitive by construction.
  • CatalogParityTests discovers every catalog by reflection and compares each culture's own key set with tryParents: false, plus the placeholder-index set per key. {1} present in one culture and not the other throws FormatException at render time, in that culture only. New module catalogs are covered without a new test — Notifications had shipped a catalog with no parity test and has no test project to hold one.
  • Tickets.Tests joins the solution, which had no test project for that module.
  • A built-in-validation test that asserted only the absence of the English string (satisfied equally by a blank message or a leaked resource key) now pins the Portuguese text.

Testing

Because this slice cannot build alone, the numbers below are framework + this slice, which is what will be on main once both land.

  • dotnet restore src/FSH.Starter.slnx with the audit on: exit 0, no NU1903.
  • dotnet build -warnaserror: exit 0.
  • Full suite: 15 test assemblies, 1891 passed / 0 failed / 1 skipped, including Integration (Testcontainers/Postgres, Docker) at 746 passed / 1 skipped. Identical to what feat(i18n): internationalization across the API and both React apps #1344 reported for the unsplit branch, and the framework slice alone is 14 assemblies / 1868 passed — so this slice is exactly the 23-test, one-assembly difference, with nothing gained or lost in the cut.
  • The combined tree is byte-identical to #1344 on every path under src/, .agents/ and AGENTS.md: git diff between them is empty there.

The verdict above is aggregated per assembly rather than taken from the process exit code: dotnet test on this solution has been observed exiting 0 while reporting failures, and zero assemblies reporting is itself treated as red.

dotnet restore passes with the audit on: src/Directory.Packages.props carries the SSH.NET pin byte-identical to #1333, same as the other three slices. See the framework PR for why.

Docs (Golden Rule #10)

fullstackhero/docs#238, kept as a single PR covering all four slices. The String resources section of internationalization.mdx is this slice: the per-module {Module}Resources catalogs and how a localized message is wired at a throw or validation site. That PR should merge after the last of the four.

Module slice of the i18n work (split of fullstackhero#1344 as requested in review).
Depends on the framework slice: it uses `SharedResources`, `SupportedCultures`
and the `MessageKey` / `MessageArgs` / `ResourceSource` plumbing on
`CustomException`, none of which exist on `main` yet.

- One `{Module}Resources` catalog per module (en + pt-BR) for Auditing,
  Billing, Catalog, Chat, Files, Multitenancy, Notifications, Tickets and
  Webhooks, plus the Identity messages that were not part of the framework
  slice.
- Domain and handler throws carry `MessageKey` and `ResourceSource` so the
  response body is localized while `Message` stays English for the logs.
- Validators resolve their messages through `IStringLocalizer`, with message
  arguments kept culture-insensitive (`MaxWindowDays` as `int` rather than
  `MaxWindow.TotalDays` as `double`, which `string.Format` would render with a
  culture-dependent decimal separator).
- `CatalogParityTests` pins key parity and placeholder parity across every
  catalog, so a missing or malformed translation fails the build rather than
  reaching a user.
- `Tickets.Tests` joins the solution, which previously had no test project.

Protected code (Golden Rule fullstackhero#4): this slice touches one file under
`src/BuildingBlocks` — `Web/Validation/PagedQueryValidator.cs`. Its constructor
now takes `IStringLocalizer<SharedResources>` so paging messages localize. All
three subclasses live in modules (`GetAuditsQueryValidator`,
`GetTenantsQueryValidator`, `SearchUsersQueryValidator`), so the base class
travels with its callers rather than breaking them from another PR. Every other
`BuildingBlocks` change is in the framework slice.

The `SSH.NET` pin (`2026.0.0`) is byte-identical to fullstackhero#1333, so `dotnet restore`
passes while that PR is open.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

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