From 8bc36b460a70d041d17f12f287ee2f1349917a4b Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Tue, 26 May 2026 16:18:11 +0200 Subject: [PATCH] fix(admin): unbreak ci:check on PR2b (TS2454 + missing fr translation) (#237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Follow-up fix on [#236](https://git.unespace.com/julien/apf_portal/pulls/236) (ADR-0026 PR 2b). Two issues that `nx run-many -t build` exposed but `nx run-many -t test` masked: 1. **TS2454 in `user-scopes.service.ts`** — webpack's strict-mode build flagged `let exists: boolean` as "used before assigned". jest's `--transpile-only` doesn't enforce definite-assignment so it slipped through local checks. 2. **Missing FR translation** for `route.user-scopes.title` — Angular's `@angular/localize` build target requires every `$localize` ID to have a target in `messages.fr.xlf`. The route I added in #236 referenced the ID but I didn't add the trans-unit. ## What lands | File | Change | | --- | --- | | `apps/portal-bff/src/admin/user-scopes.service.ts` | Initialise `let exists = false` instead of `let exists: boolean`. The `VALUE_BEARING_KINDS.has(kind)` check earlier in the function does narrow `kind` semantically, but `Set.has` doesn't propagate type narrowing the way `Array.includes` or a custom type guard would. Initialising to `false` is the smallest fix; the next `if (!exists)` still throws the friendly "does not match" message in the (unreachable-by-construction) fallback. | | `apps/portal-admin/src/locale/messages.fr.xlf` | New `` — FR target `Périmètres utilisateur — Administration APF Portal`. | ## Why these failed locally - **TS2454**: jest runs with `--transpile-only` so it skips definite-assignment analysis (and most type-checking). Webpack's production build runs full `tsc` and catches it. - **Missing translation**: the dev server `pnpm nx serve portal-admin` doesn't run the localize pass; only `--configuration=production` does. My local `nx test portal-admin` runs in JIT mode against the source locale, so the missing FR target slid through. For the future: `pnpm nx run-many -t build --projects=portal-bff,portal-admin` would have caught both pre-PR. Worth a `pnpm ci:check:fast` shorthand that runs a subset of the CI gates locally before push — but that's a separate PR. ## Test plan - [x] `pnpm exec nx run-many -t build --projects=portal-bff,portal-admin` — both pass. - [x] No application logic changed in either fix (initialise vs declare; add a missing translation row). - [ ] CI green on this PR. --------- Co-authored-by: Julien Gautier Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/237 --- apps/portal-admin/src/locale/messages.fr.xlf | 4 ++++ apps/portal-bff/src/admin/user-scopes.service.ts | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/portal-admin/src/locale/messages.fr.xlf b/apps/portal-admin/src/locale/messages.fr.xlf index 07ce70e..ea3fc4a 100644 --- a/apps/portal-admin/src/locale/messages.fr.xlf +++ b/apps/portal-admin/src/locale/messages.fr.xlf @@ -21,6 +21,10 @@ Users — APF Portal Admin Utilisateurs — Administration APF Portal + + User scopes — APF Portal Admin + Périmètres utilisateur — Administration APF Portal + Profile — APF Portal Admin Profil — Administration APF Portal diff --git a/apps/portal-bff/src/admin/user-scopes.service.ts b/apps/portal-bff/src/admin/user-scopes.service.ts index 1a9a8ac..8f5b965 100644 --- a/apps/portal-bff/src/admin/user-scopes.service.ts +++ b/apps/portal-bff/src/admin/user-scopes.service.ts @@ -194,7 +194,15 @@ export class UserScopesService { // ADR-0026 PR 1's "no FK on UserScope.value" rationale lets the // value persist even after the target is decommissioned, so we // validate only at the write path. - let exists: boolean; + // + // Initialise `exists = false` so TypeScript sees it as definitely + // assigned regardless of how the switch lands (the type-system + // can't see that `VALUE_BEARING_KINDS.has` already narrowed + // `kind` to the three branches). If somehow a non-value-bearing + // kind reached this code, the false default makes the next `if` + // throw with the same friendly "does not match" message — safe + // by construction. + let exists = false; switch (kind) { case 'etablissement': exists =