2772a918c2
## Summary Final piece of the AI relay chantier opened with [ADR-0024](docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md): a chatbot widget on `portal-shell` that consumes the BFF's `POST /api/ai/chat` SSE endpoint (shipped in #196). Built fresh against the WCAG 2.2 AA + targeted AAA bar set by [ADR-0016](docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md) rather than transcribing the stargate POC's React widget — the POC's drag UX, absent `aria-live`, and minimal screen-reader contract did not meet that bar. The widget renders as a floating launcher bottom-right; clicking opens a dialog panel that can be toggled into fullscreen; sending a message streams the assistant's reply token-by-token; citations render as inline footnote chips with an extracted side panel for the snippet/source/score detail. ## What lands ### Files ``` apps/portal-shell/src/app/features/chatbot/ ├── chatbot-host.ts Standalone component, mounted in app.html via @defer ├── chatbot-host.html Launcher + panel + suggestions + messages + form ├── chatbot-host.scss 7.6 KB compiled — under the new 8 KB budget ├── chatbot-host.spec.ts 12 cases: launcher / panel / log / input / citations ├── chatbot-citation-panel.ts Extracted sibling component so the host stays under budget ├── chatbot-citation-panel.html dialog with metadata + snippet, role=dialog aria-modal=false ├── chatbot-citation-panel.scss 2.6 KB ├── chatbot.service.ts Signals-based state (view / messages / streaming / citation) ├── chatbot.service.spec.ts 10 cases: view, send/stop, citations, errors ├── chatbot-api.service.ts fetch + ReadableStream + CSRF cookie + AbortSignal ├── chatbot-api.service.spec.ts 5 cases: request shape, parsing, errors ├── sse-parser.ts Pure ReadableStream<Uint8Array> → AsyncIterable<{event,data}> ├── sse-parser.spec.ts 8 cases: LF / CRLF / split chunks / trailing / JSON / passthrough └── chatbot.types.ts UI types decoupled from the proto-derived BFF types ``` Plus the shell-side glue: - `apps/portal-shell/src/app/app.html` — `<app-chatbot-host />` mounted as a sibling of `<app-footer />`, wrapped in `@defer (on idle)` so the widget bundle (~27 KB lazy chunk) does not weigh on the initial paint. - `apps/portal-shell/src/app/app.ts` — `ChatbotHost` added to `imports`. - `apps/portal-shell/src/app/app.spec.ts` — `AUTH_CSRF_COOKIE_NAME` provider added to keep the shell smoke test compiling now that the SPA renders the chatbot. - `apps/portal-shell/src/locale/messages.fr.xlf` — 19 new `@@chatbot.*` trans-units covering the trigger / title / fullscreen toggle / suggestions / input / streaming / errors / citation panel. - `apps/portal-shell/project.json` — `anyComponentStyle` budget raised from `5/6 KB` to `6/8 KB` to match `portal-admin`'s posture (the audit page hit the same wall). - `libs/shared/ui/src/lib/icon/icon.ts` — 6 new icons in the registry: `maximize-2`, `message-circle`, `minimize-2`, `send`, `square`, `x`. ### Accessibility decisions (per ADR-0016) | Decision | Why | |---|---| | **No drag**. Fixed bottom-right launcher + fullscreen toggle. | Stargate's mouse-only drag had no keyboard equivalent. Keyboard parity is the project's bar; drag is the wrong primitive to inherit. | | **`role="dialog"` + `aria-modal="false"`** (non-modal). | The page chrome stays operable behind the panel; no focus trap, no `inert` toggle on `<main>`. Closing returns focus to the launcher via a `viewChild` + `effect`. | | **`role="log"` + `aria-live="polite"` + `aria-relevant="additions"`** on the message container. | Screen readers announce the assistant's incoming tokens without re-reading the whole history. Used a `<div>` + `<article>` children — `role="log"` is not allowed on `<ol>` / `<ul>` per ARIA. | | **Typing dots `aria-hidden="true"`**, paired with a `<span class="sr-only">{{ streamingLabel }}</span>`. | The visual signal is decorative; the SR signal is textual. Animation gated by `@media (prefers-reduced-motion: reduce)`. | | **Stop button visible during streaming**. | Wired to `AbortController` → `ChatClient` → upstream LLM cancel (the cancel chain shipped in #195). | | **Inline `role="alert"`** on per-message error banners. | Errors are part of the conversation log, not page-level interruptions; assertive announcement keeps them perceivable without yanking focus. | | **44 × 44 px touch targets everywhere**. | Launcher (3.5 rem), header chrome buttons, send / stop, citation chips, suggestion buttons. ADR-0016 baseline. | | **Citations as inline footnotes** (`[1]`, `[2]`, …) + side panel with `source` / `score` / `snippet`. | Validated in the design check before implementation. Side panel extracted into its own component so the host stays under the SCSS budget. | | **`prefers-reduced-motion` gating** on launcher transitions, suggestion hover, action button transitions, typing-dots animation. | Standard motion-preferences contract. | | **i18n via `$localize`** with the `@@key` catalogue convention per [ADR-0019](docs/decisions/0019-internationalisation-angular-localize.md). | FR strings tagged at source; translations added to `messages.fr.xlf` (build fails otherwise — `i18nMissingTranslation: error`). | ### Streaming + cancellation The browser-side flow: 1. SPA submits a prompt → `ChatbotService.send(prompt)`. 2. Service appends a user message + empty assistant placeholder, sets `isStreaming = true`. 3. `ChatbotApiService.openChatStream(...)` opens a `POST` with `Accept: text/event-stream`, `credentials: 'include'`, `X-CSRF-Token` from the `__Host-portal_csrf` cookie, and an `AbortSignal`. 4. The response body's `ReadableStream<Uint8Array>` is parsed frame-by-frame by `sse-parser.ts` and yielded as `AsyncIterable<{event, data}>`. 5. The state service routes each frame: `token` appends to the assistant message, `citation` accumulates with a 1-based index, `error` marks the message as failed, `done` terminates the stream. 6. Cancellation: clicking Stop, navigating away, or any unhandled error fires `AbortController.abort()` → fetch terminates → upstream gRPC call is cancelled → AI service stops the LLM. Persistence: **session-ephemeral** by design. A SPA reload starts a fresh conversation. Matches the AI service's v1 posture (no per-user conversation table) and avoids the GDPR question of storing free-form transcripts before a consent flow lands. ### Native `fetch` + manual CSRF `HttpClient` buffers responses; native `fetch().body` is the only way to consume the stream incrementally. As a consequence, the project's `bffCredentialsInterceptor` + `csrfInterceptor` do not run on this call. The service handles both concerns manually: - `credentials: 'include'` is set explicitly so `__Host-portal_session` travels. - The `__Host-portal_csrf` cookie is read via `document.cookie` (it is intentionally not `HttpOnly` per [ADR-0009](docs/decisions/0009-auth-flow-oidc-pkce-msal-node.md) §"CSRF defense") and echoed in the `X-CSRF-Token` header. The cookie name + BFF base URL come from the same `AUTH_*` injection tokens the interceptors use, so the wire contract stays single-sourced. ## Notes for the reviewer - **Why ChatbotCitationPanel is its own component.** Initial draft put the side panel inside `chatbot-host.scss`, which crossed the `anyComponentStyle` 6 KB error budget. Two paths to fix: raise the budget, or extract. Did both — the panel is genuinely its own dialog with its own ARIA contract, and the host stays under budget. The budget bump (5/6 → 6/8 KB) brings portal-shell in line with portal-admin where the same wall was hit on the audit page. - **Why `@defer (on idle)` rather than `@defer (on viewport)` or eager.** Eager pushed the initial main bundle from ~282 KB to ~315 KB — past the 300 KB budget. The widget is not critical path on first paint, so deferring is correct on its own terms. `on idle` ensures it loads as soon as the browser is idle, so the launcher is interactive within a second on a fresh load. `on viewport` would have required the widget to be in the initial viewport, which the floating launcher is not consistently. - **Why `<article>` children rather than `<li>` inside `role="log"`.** axe-linter (and the underlying ARIA spec) reject `role="log"` on `<ol>` / `<ul>`. Switched to `<div role="log">` with `<article>` children; semantically each chat turn is a discrete piece of content, which is exactly what `<article>` is for. - **Why ChatbotService doesn't extend / re-use the existing `AuthService` patterns more directly.** Conversation state is ephemeral and not security-sensitive; the auth service's discriminated-union state machine is overkill for the toggle + buffer pattern the chatbot needs. The two services share the same `signal` / `computed` / `effect` idiom; that's enough consistency. - **Why hard-coded suggestions instead of pulling from a server.** v1 ships with four French suggestions tagged for translation; server-driven suggestions are a v2 step that requires a new endpoint and a personalisation question that v1 doesn't need to answer. The current shape moves to server-driven by replacing the array literal with an effect that fetches — single point of change. - **Tool-call event handling.** The SSE writer in #196 emits `event: tool-call` frames; the SPA parses them but does not render anything. v1 ships with an empty tool registry on the BFF side, so the AI service never emits them. When the first tool lands, the rendering UI is a follow-up PR. - **stargate-a11y-uplift memory.** The memory note `feedback_stargate_a11y_uplift.md` codifies the rule used here for future migrations from stargate: adapt, don't transcribe. The PR text under "Accessibility decisions" is the case study. ## Test plan - [x] `pnpm nx test portal-shell` — **85 specs pass** (was 58, +27 new across SSE parser / API service / state service / host component). - [x] `pnpm nx test shared-ui` — 10 specs pass (icon registry exhaustive-key spec picks up the 6 new icons). - [x] `pnpm nx lint portal-shell` — 0 errors. 5 pre-existing non-null assertion warnings in the new spec files are documented limits of vitest's mock typing. - [x] `pnpm nx build portal-shell` — clean. Main bundle 297.49 KB raw (under 300 KB budget). Chatbot lazy chunk: 27.61 KB raw / 6.49 KB transfer. SCSS: host 7.6 KB / citation panel 2.6 KB, both under the new 8 KB error budget. - [x] `pnpm nx run-many -t lint test build -p portal-shell,portal-admin,portal-bff,shared-ui,shared-charts` — five projects all green. - [ ] **Manual smoke** (requires the BFF wired to `apf-ai-service` per #196's plan): 1. `cd ../apf-ai-service && docker compose -f infra/docker-compose.yml up` to bring up the AI service. 2. `AI_SERVICE_GRPC_ENDPOINT=localhost:8080` in `apps/portal-bff/.env`, then `pnpm nx serve portal-bff` + `pnpm nx serve portal-shell`. 3. Sign in to the SPA, click the floating launcher bottom-right → panel opens, focus lands on the close button. 4. Pick a suggestion → user message appears right-aligned, assistant message streams in left-aligned with the typing dots animating (unless `prefers-reduced-motion` is on). 5. Click Stop mid-stream → the AI service log shows the gRPC call cancelled. 6. Press Escape with focus inside the panel → panel closes, focus returns to the launcher. 7. Toggle fullscreen → panel expands to `inset: 1rem`, ARIA contract unchanged. 8. Toggle dark mode → all themed surfaces switch via the CSS-variable swap in `chatbot-host.scss`; AA contrast still holds against the brand tokens. 9. Hit `/fr` and `/en` builds independently; suggestion labels swap between locales. ## What's next The AI relay chantier closes here. Pending follow-ups stay as written in #196: 1. **PR (post-v1)** — proto-drift CI gate diffing the BFF's vendored `proto/apf-ai/` against an upstream tag of `apf-ai-service`. 2. **Coordinated amendment** — when the first production deployment is in scope, both repos record the same prod-hardening choice (signed `Principal` envelope or mTLS) on the same date. 3. **Tool-call rendering** — UI surface for the `tool-call` SSE frame, once the BFF gains its first tool descriptor. 4. **Server-driven suggestions** — replace the four hard-coded prompts with an effect that fetches per-user suggestions from a future endpoint. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #197
389 lines
17 KiB
XML
389 lines
17 KiB
XML
<?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 d’accessibilité</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 d’accessibilité · 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 d’affichage</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's user base (see ADR-0016). </source>
|
||
<target> Le portail APF s’engage à 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 l’association (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 d’usagers d’APF France handicap. La version définitive intégrera la grille d’audit 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 d’accessibilité </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>
|
||
<!-- chatbot (AI assistant widget — ADR-0024) -->
|
||
<trans-unit id="chatbot.trigger.open" datatype="html">
|
||
<source>Open the assistant</source>
|
||
<target>Ouvrir l’assistant</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.title" datatype="html">
|
||
<source>AI assistant</source>
|
||
<target>Assistant IA</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.close" datatype="html">
|
||
<source>Close the assistant</source>
|
||
<target>Fermer l’assistant</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.fullscreen.enter" datatype="html">
|
||
<source>Open in full screen</source>
|
||
<target>Ouvrir en plein écran</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.fullscreen.exit" datatype="html">
|
||
<source>Exit full screen</source>
|
||
<target>Quitter le plein écran</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.empty.greeting" datatype="html">
|
||
<source>Ask me anything about the portal.</source>
|
||
<target>Posez-moi vos questions sur le portail.</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.suggestion.1" datatype="html">
|
||
<source>Find a document</source>
|
||
<target>Trouver un document</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.suggestion.2" datatype="html">
|
||
<source>Request leave</source>
|
||
<target>Faire une demande de congé</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.suggestion.3" datatype="html">
|
||
<source>Contact IT support</source>
|
||
<target>Contacter le support informatique</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.suggestion.4" datatype="html">
|
||
<source>My open alerts</source>
|
||
<target>Mes alertes en cours</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.input.label" datatype="html">
|
||
<source>Your question for the assistant</source>
|
||
<target>Votre question pour l’assistant</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.input.placeholder" datatype="html">
|
||
<source>Write a message…</source>
|
||
<target>Écrivez un message…</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.input.send" datatype="html">
|
||
<source>Send the message</source>
|
||
<target>Envoyer le message</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.input.stop" datatype="html">
|
||
<source>Stop generating</source>
|
||
<target>Arrêter la génération</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.streaming" datatype="html">
|
||
<source>Assistant is replying.</source>
|
||
<target>L’assistant rédige une réponse.</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.citation.panel.title" datatype="html">
|
||
<source>Citation details</source>
|
||
<target>Détails de la citation</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.citation.panel.close" datatype="html">
|
||
<source>Close citation details</source>
|
||
<target>Fermer les détails de citation</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.citation.source" datatype="html">
|
||
<source>Source</source>
|
||
<target>Source</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.citation.score" datatype="html">
|
||
<source>Score</source>
|
||
<target>Score</target>
|
||
</trans-unit>
|
||
<trans-unit id="chatbot.citation.button" datatype="html">
|
||
<source>Show citation <x id="INDEX" equiv-text="${citation.index}"/></source>
|
||
<target>Afficher la citation <x id="INDEX" equiv-text="${citation.index}"/></target>
|
||
</trans-unit>
|
||
</body>
|
||
</file>
|
||
</xliff>
|