Files
apf_portal/apps/portal-shell/src/locale/messages.fr.xlf
T
julien 6f26bcdd65
CI / check (push) Successful in 3m41s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 1m44s
CI / a11y (push) Successful in 1m38s
CI / perf (push) Successful in 3m8s
feat(shared-ui): move the role label from the portal-shell sidebar into the user menu (#165)
## Summary

Moves the "Role: …" widget from the bottom of the `portal-shell` sidebar into the user-menu panel header. Result: the role chip appears only when the reader is authenticated (the user menu only renders in that state), removing the noise of "Role: Anonymous" on signed-out traffic and de-duplicating the surface that already carries displayName / username / capability-gated actions.

```
Before: sidebar bottom  →  "Role: Anonymous"  (always rendered, even signed-out)
After:  user-menu panel →  • <role chip>      (only when authenticated)
```

## What lands

### Shared component — [`UserMenu`](libs/shared/ui/src/lib/user-menu/) gets an optional `role` input

When set, a small brand-tinted pill (`data-testid="user-menu-role"`) renders inside the panel header below the username. When undefined, the entire chip is omitted — keeps backwards compat with any future caller that doesn't carry a role concept.

```ts
readonly role = input<string | undefined>(undefined);
```

Visually echoes the `.role-chip` already used on the admin `/profile` page (rounded brand-tinted pill), tightened for the menu header density and `align-self: flex-start` so the pill doesn't stretch.

### Portal-shell

- **Header** ([header.ts](apps/portal-shell/src/app/components/header/header.ts)) gains a `roleLabel` computed that derives `Administrator` / `User` from `CapabilitiesService.canAccessAdmin` — the same logic that previously lived in the sidebar. The template binds it as `[role]="roleLabel()"` on `<lib-user-menu>`.
- **Sidebar** ([sidebar.ts](apps/portal-shell/src/app/components/sidebar/sidebar.ts), [sidebar.html](apps/portal-shell/src/app/components/sidebar/sidebar.html)) loses the role widget entirely: HTML block, `roleLabel` + `roleAriaLabel` computeds, and the `AuthService` + `CapabilitiesService` injections (the sidebar no longer needs them).
- **Sidebar spec** drops its three "role label" tests, grows a guard test asserting `data-testid="sidebar-role"` no longer exists, and reverts to the pre-#151 setup (no Http testing infrastructure — the sidebar no longer fires `/auth/me` or `/me/capabilities`).
- **Header spec** gains two assertions for the role chip inside the open menu panel (`Administrator` when `canAccessAdmin: true`, `User` otherwise).

### Portal-admin

- **Header** ([header.ts](apps/portal-admin/src/app/components/header/header.ts)) passes a **hardcoded `'Administrator'`** through `[role]`. Every reader who reaches the admin app already carries `Portal.Admin` (it's the `AdminRoleGuard` precondition for `/api/admin/*`); no need to re-derive. No i18n marks per ADR-0020's source-locale-only chrome.

### i18n

Five translation units gone from [`messages.fr.xlf`](apps/portal-shell/src/locale/messages.fr.xlf):

```
sidebar.role.anonymous
sidebar.role.administrator
sidebar.role.user
sidebar.role.aria
sidebar.role.label
```

Replaced by two new units under a generic prefix (the new owners are the user menu in either app, not the sidebar):

```
common.role.administrator
common.role.user
```

The `Anonymous` string is gone too — the chip is hidden on anonymous traffic, the label served no purpose without the user menu rendering it.

## Notes for the reviewer

- **Why a generic `common.role.*` prefix rather than `userMenu.role.*`?** The strings are reusable in any context that needs a curated role display (profile page header in a future PR, an audit log "actor role" column, …). Generic prefix avoids the next rename when a second consumer lands.
- **Why hardcode "Administrator" for portal-admin rather than reading `roles`?** The admin SPA's `CurrentUser.roles` carries the raw Entra role string (`Portal.Admin`). Displaying that in the menu header would leak the internal nomenclature into the UI — fine for the `/profile` page (it's explicit context there), wrong for the casual menu glance. The hardcoded label keeps the menu consistent with portal-shell ("Administrator" in both surfaces). If a non-admin role ever reaches portal-admin (the guard rejects it today, but the surface could evolve), this is the one place to revisit.
- **No CapabilitiesService dependency on portal-admin.** The admin app doesn't import the service — its session already exposes `roles` on `/api/admin/auth/me`, and the chip is unconditional. Keeps the admin bundle untouched by the user-portal capabilities plumbing.
- **A11y check.** The role chip is plain text inside the menu panel; the menu panel itself carries `role="menu"` + `aria-label`. The chip doesn't need an extra label — it reads "Administrator" / "User" in context after the displayName and username, which conveys the meaning. No focus state needed (not interactive).

## Test plan

- [x] `pnpm nx run-many -t lint test build --projects=shared-ui,portal-shell,portal-admin` — green.
  - `shared-ui` — **10 specs pass** (was 8; +2 for the role chip).
  - `portal-shell` — **43 specs pass** (header +2, sidebar -3 + 1 guard = -2 net, balanced to +0 on the total → 43 unchanged).
  - `portal-admin` — **50 specs pass** (no spec edits; the `[role]` binding is exercised by the existing user-menu spec via the shared component).
- [x] `pnpm nx build portal-shell --configuration=production` — i18n-strict prod build passes; confirms the xlf cleanup + new `common.role.*` keys are consistent with usage.
- [ ] **Manual smoke**:
  - **portal-shell anonymous**: sidebar bottom shows only the collapse toggle (no role line). Click "Sign in" → land back signed in, sidebar bottom unchanged.
  - **portal-shell signed in (non-admin)**: avatar → open menu → header shows `Signed in as / displayName / username / [User]` chip.
  - **portal-shell signed in (admin)**: same, chip reads `Administrator`, and the menu carries the `Open Portal Admin` row above Sign out.
  - **portal-admin signed in**: avatar → open menu → `[Administrator]` chip regardless of which admin user signed in.
  - Tabbing across the sidebar bottom no longer pauses on a non-interactive `<p>` element — directly hits the collapse toggle button.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #165
2026-05-16 03:09:56 +02:00

308 lines
14 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="fr" datatype="plaintext" original="ng2.template">
<body>
<!--
French translations for portal-shell, per ADR-0019.
Source IDs are stable @@-prefixed values authored in the
templates / `$localize` calls. Trans-units are grouped by
feature (app, footer, header, sidebar, theme, page.*) and
alphabetical within each group so diffs stay readable.
-->
<!-- app -->
<trans-unit id="app.skipLink" datatype="html">
<source>Skip to main content</source>
<target>Aller au contenu principal</target>
</trans-unit>
<!-- footer -->
<trans-unit id="footer.aria.label" datatype="html">
<source>Page footer</source>
<target>Pied de page</target>
</trans-unit>
<trans-unit id="footer.aria.legal" datatype="html">
<source>Legal</source>
<target>Mentions légales</target>
</trans-unit>
<trans-unit id="footer.accessibilityLink" datatype="html">
<source>Accessibility statement</source>
<target>Déclaration daccessibilité</target>
</trans-unit>
<trans-unit id="footer.copyright" datatype="html">
<source>© <x id="INTERPOLATION" equiv-text="{{ year }}"/> APF France handicap</source>
<target>© <x id="INTERPOLATION" equiv-text="{{ year }}"/> APF France handicap</target>
</trans-unit>
<!-- header -->
<trans-unit id="header.action.help" datatype="html">
<source>Help</source>
<target>Aide</target>
</trans-unit>
<trans-unit id="header.action.notifications" datatype="html">
<source>Notifications</source>
<target>Notifications</target>
</trans-unit>
<trans-unit id="header.authError" datatype="html">
<source>Can't reach the server</source>
<target>Serveur injoignable</target>
</trans-unit>
<trans-unit id="header.signIn" datatype="html">
<source>Sign in</source>
<target>Se connecter</target>
</trans-unit>
<trans-unit id="header.signOut" datatype="html">
<source>Sign out</source>
<target>Se déconnecter</target>
</trans-unit>
<trans-unit id="header.userMenu.loading" datatype="html">
<source>Checking sign-in status…</source>
<target>Vérification de la session…</target>
</trans-unit>
<trans-unit id="header.userMenu.profile" datatype="html">
<source>Profile</source>
<target>Profil</target>
</trans-unit>
<trans-unit id="header.userMenu.settings" datatype="html">
<source>Settings</source>
<target>Paramètres</target>
</trans-unit>
<trans-unit id="header.userMenu.signedInAs" datatype="html">
<source>Signed in as</source>
<target>Connecté en tant que</target>
</trans-unit>
<trans-unit id="header.userMenu.signOut" datatype="html">
<source>Sign out</source>
<target>Se déconnecter</target>
</trans-unit>
<trans-unit id="header.userMenu.openAdmin" datatype="html">
<source>Open Portal Admin</source>
<target>Ouvrir Administration APF Portal</target>
</trans-unit>
<trans-unit id="header.userMenu.trigger.aria" datatype="html">
<source>User menu — signed in as <x id="displayName" equiv-text="displayName"/></source>
<target>Menu utilisateur — connecté en tant que <x id="displayName" equiv-text="displayName"/></target>
</trans-unit>
<trans-unit id="common.badge.soon" datatype="html">
<source>Soon</source>
<target>Bientôt</target>
</trans-unit>
<trans-unit id="header.search.label" datatype="html">
<source>Search the portal</source>
<target>Rechercher dans le portail</target>
</trans-unit>
<trans-unit id="header.search.placeholder" datatype="html">
<source>Search…</source>
<target>Rechercher…</target>
</trans-unit>
<trans-unit id="header.wordmark" datatype="html">
<source>APF Portal</source>
<target>Portail APF</target>
</trans-unit>
<!-- sidebar -->
<trans-unit id="sidebar.aria.label" datatype="html">
<source>Sidebar navigation</source>
<target>Navigation latérale</target>
</trans-unit>
<trans-unit id="sidebar.aria.sections" datatype="html">
<source>Sections</source>
<target>Sections</target>
</trans-unit>
<trans-unit id="sidebar.group.communication" datatype="html">
<source>Communication</source>
<target>Communication</target>
</trans-unit>
<trans-unit id="sidebar.group.establishments" datatype="html">
<source>Establishments</source>
<target>Établissements</target>
</trans-unit>
<trans-unit id="sidebar.group.general" datatype="html">
<source>General</source>
<target>Général</target>
</trans-unit>
<trans-unit id="sidebar.item.activities" datatype="html">
<source>Activities</source>
<target>Activités</target>
</trans-unit>
<trans-unit id="sidebar.item.calendar" datatype="html">
<source>Calendar</source>
<target>Calendrier</target>
</trans-unit>
<trans-unit id="sidebar.item.dashboard" datatype="html">
<source>Dashboard</source>
<target>Tableau de bord</target>
</trans-unit>
<trans-unit id="sidebar.item.directory" datatype="html">
<source>Directory</source>
<target>Annuaire</target>
</trans-unit>
<trans-unit id="sidebar.item.documents" datatype="html">
<source>Documents</source>
<target>Documents</target>
</trans-unit>
<trans-unit id="sidebar.item.messages" datatype="html">
<source>Messages</source>
<target>Messages</target>
</trans-unit>
<trans-unit id="common.role.administrator" datatype="html">
<source>Administrator</source>
<target>Administrateur</target>
</trans-unit>
<trans-unit id="common.role.user" datatype="html">
<source>User</source>
<target>Utilisateur</target>
</trans-unit>
<trans-unit id="sidebar.toggle.aria.collapse" datatype="html">
<source>Collapse sidebar</source>
<target>Replier la barre latérale</target>
</trans-unit>
<trans-unit id="sidebar.toggle.aria.expand" datatype="html">
<source>Expand sidebar</source>
<target>Déplier la barre latérale</target>
</trans-unit>
<trans-unit id="sidebar.toggle.collapse" datatype="html">
<source>Collapse</source>
<target>Replier</target>
</trans-unit>
<!-- theme -->
<trans-unit id="theme.menu.aria" datatype="html">
<source>Theme</source>
<target>Thème</target>
</trans-unit>
<trans-unit id="theme.mode.dark" datatype="html">
<source>Dark</source>
<target>Sombre</target>
</trans-unit>
<trans-unit id="theme.mode.light" datatype="html">
<source>Light</source>
<target>Clair</target>
</trans-unit>
<trans-unit id="theme.mode.system" datatype="html">
<source>System</source>
<target>Système</target>
</trans-unit>
<trans-unit id="theme.trigger.aria" datatype="html">
<source>Theme: <x id="mode" equiv-text="this.currentLabel()"/> (open menu)</source>
<target>Thème : <x id="mode" equiv-text="this.currentLabel()"/> (ouvrir le menu)</target>
</trans-unit>
<!-- route -->
<trans-unit id="route.accessibility.title" datatype="html">
<source>Accessibility statement · APF Portal</source>
<target>Déclaration daccessibilité · Portail APF</target>
</trans-unit>
<trans-unit id="route.home.title" datatype="html">
<source>APF Portal</source>
<target>Portail APF</target>
</trans-unit>
<trans-unit id="route.profile.title" datatype="html">
<source>My profile · APF Portal</source>
<target>Mon profil · Portail APF</target>
</trans-unit>
<!-- profile -->
<trans-unit id="profile.heading" datatype="html">
<source>My profile</source>
<target>Mon profil</target>
</trans-unit>
<trans-unit id="profile.intro" datatype="html">
<source>Identity served by the BFF from the active session.</source>
<target>Identité fournie par le BFF depuis la session active.</target>
</trans-unit>
<trans-unit id="profile.field.displayName" datatype="html">
<source>Display name</source>
<target>Nom daffichage</target>
</trans-unit>
<trans-unit id="profile.field.username" datatype="html">
<source>Username</source>
<target>Identifiant</target>
</trans-unit>
<trans-unit id="profile.field.oid" datatype="html">
<source>Entra object id</source>
<target>Identifiant Entra (oid)</target>
</trans-unit>
<trans-unit id="profile.field.tid" datatype="html">
<source>Tenant id</source>
<target>Identifiant du tenant</target>
</trans-unit>
<!-- locale switcher -->
<trans-unit id="locale.menu.aria" datatype="html">
<source>Language</source>
<target>Langue</target>
</trans-unit>
<trans-unit id="locale.trigger.aria" datatype="html">
<source>Language: <x id="locale" equiv-text="this.currentDisplayName"/> (change language)</source>
<target>Langue : <x id="locale" equiv-text="this.currentDisplayName"/> (changer de langue)</target>
</trans-unit>
<!-- page.accessibility -->
<trans-unit id="page.accessibility.intro.body" datatype="html">
<source> The APF Portal is committed to making its digital services accessible, in line with WCAG 2.2 level AA and the French RGAA 4.1 reference, with targeted AAA criteria on the items most impactful to the association&apos;s user base (see ADR-0016). </source>
<target> Le portail APF sengage à rendre ses services numériques accessibles, conformément au RGAA 4.1 et à la norme WCAG 2.2 niveau AA, avec des critères AAA ciblés sur les points à fort impact pour les usagers de lassociation (voir ADR-0016). </target>
</trans-unit>
<trans-unit id="page.accessibility.intro.title" datatype="html">
<source> About this statement </source>
<target> À propos de cette déclaration </target>
</trans-unit>
<trans-unit id="page.accessibility.panel.body" datatype="html">
<source> This statement is awaiting review and sign-off by the APF France handicap user panel. The final version will integrate the RGAA audit grid, the list of any identified gaps, the remediation plan, and the contact details for reporting accessibility issues. </source>
<target> Cette déclaration est en attente de revue et de validation par le panel dusagers dAPF France handicap. La version définitive intégrera la grille daudit RGAA, le recensement des écarts éventuels, le plan de mise en conformité, et les coordonnées de signalement. </target>
</trans-unit>
<trans-unit id="page.accessibility.panel.title" datatype="html">
<source> Status of this document </source>
<target> Statut du présent document </target>
</trans-unit>
<trans-unit id="page.accessibility.title" datatype="html">
<source> Accessibility statement </source>
<target> Déclaration daccessibilité </target>
</trans-unit>
<!-- page.home -->
<trans-unit id="page.home.intro" datatype="html">
<source> This is the placeholder home page for the APF Portal. Real content lands once the comms team and the accessibility review panel sign off on the copy. </source>
<target> Ceci est la page d'accueil provisoire du portail APF. Le contenu définitif arrivera après validation par l'équipe communication et le panel d'audit d'accessibilité. </target>
</trans-unit>
<trans-unit id="page.home.status.error" datatype="html">
<source> Backend unreachable. The BFF dev server may not be running, or CORS is misconfigured. </source>
<target> Backend inaccessible. Le serveur BFF de développement est peut-être arrêté, ou la configuration CORS est incorrecte. </target>
</trans-unit>
<trans-unit id="page.home.status.loading" datatype="html">
<source> Checking the backend… </source>
<target> Vérification du backend… </target>
</trans-unit>
<trans-unit id="page.home.status.service" datatype="html">
<source>Service</source>
<target>Service</target>
</trans-unit>
<trans-unit id="page.home.status.status" datatype="html">
<source>Status</source>
<target>Statut</target>
</trans-unit>
<trans-unit id="page.home.status.title" datatype="html">
<source> System status </source>
<target> État du système </target>
</trans-unit>
<trans-unit id="page.home.status.uptime" datatype="html">
<source>Uptime</source>
<target>Disponibilité</target>
</trans-unit>
<trans-unit id="page.home.status.uptimeValue" datatype="html">
<source><x id="INTERPOLATION" equiv-text="{{ h.uptimeSeconds }}"/> s</source>
<target><x id="INTERPOLATION" equiv-text="{{ h.uptimeSeconds }}"/> s</target>
</trans-unit>
<trans-unit id="page.home.status.version" datatype="html">
<source>Version</source>
<target>Version</target>
</trans-unit>
<trans-unit id="page.home.title" datatype="html">
<source> Welcome to APF Portal </source>
<target> Bienvenue sur Portail APF </target>
</trans-unit>
</body>
</file>
</xliff>