feat(portal-shell): i18n string sweep — mark UI strings + FR translations
CI / commits (pull_request) Successful in 2m12s
CI / scan (pull_request) Successful in 2m13s
CI / check (pull_request) Successful in 2m22s
CI / a11y (pull_request) Successful in 2m13s
CI / perf (pull_request) Successful in 5m27s

Continue ADR-0019 implementation. Mark every UI string surfaced by
the shell with `i18n="@@id"` (templates) or `$localize` (TypeScript)
and populate `messages.fr.xlf` with French translations. 43 trans-
units total: skip-link, header, sidebar, theme switcher, footer,
home page, route titles.

What's marked:

- `app.html` skip-link.
- `header.html` brand wordmark, search label + placeholder, action
  buttons (notifications / help / settings), user-menu placeholder.
- `sidebar.ts` MENU data (group + item labels) and a computed aria-
  label for the toggle button (Expand / Collapse).
- `sidebar.html` aside / sections / role / collapse strings.
- `theme-switcher.ts` MODE_OPTIONS labels + computed trigger aria
  ("Theme: <mode> (open menu)" with a named placeholder).
- `theme-switcher.html` menu aria-label.
- `footer.html` aria-labels + copyright. The two accessibility-page
  link labels stay bilingual on purpose (one per language, each
  carrying its `lang` attribute) — the dual-link pattern goes away
  in the upcoming route-fusion PR.
- `home.html` welcome heading + intro + status widget labels.
- `app.routes.ts` browser tab titles. The `/accessibility` and
  `/accessibilite` routes share one title key; the route-fusion PR
  will collapse them to a single localised route.

Tooling:

- Add `"@angular/localize"` to the `types` array in both
  `tsconfig.app.json` and `tsconfig.spec.json` so TypeScript
  resolves the `$localize` global.

Translation choices worth flagging:

- Wordmark: "APF Portal" → "Portail APF" (consistent with the route
  title key).
- "System" theme mode → "Système".
- "Anonymous" role → "Anonyme"; "Role:" → "Rôle :" (French uses a
  non-breaking-space before the colon — typographic convention,
  preserved in the XLIFF target).

Verification:

- Production build emits both locales (`dist/.../{en,fr}/`).
  Spot-checked the FR bundle: no leftover English source text on
  any of the swept strings, route titles, or the home page intro.
- Initial bundle: 129 kB gzip per locale (vs 122 kB before).
  +7 kB gzip absorbs the i18n marker metadata + the FR translation
  data embedded in the FR bundle. Well under the 300 KB budget.
- 36 / 36 specs unchanged (they run in the source locale, so the
  English assertions still match).

Out of scope (each its own follow-up PR):

- Locale switcher in the footer + `__Host-portal_locale` cookie +
  smart `/` redirect.
- Collapse `/accessibility` + `/accessibilite` into one localised
  route.
- CI gate that fails the build on a missing translation.
This commit is contained in:
Julien Gautier
2026-05-11 19:46:13 +02:00
parent 04675b1b59
commit 47277114da
12 changed files with 312 additions and 56 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
<a class="skip-link" href="#main-content">Skip to main content</a>
<a class="skip-link" href="#main-content" i18n="@@app.skipLink">Skip to main content</a>
<app-header />
<div class="shell-body">
<app-sidebar />
+10 -3
View File
@@ -1,11 +1,18 @@
import { Route } from '@angular/router';
// Tab titles per route — marked with `$localize` so each locale's
// bundle ships its own. The `/accessibility` and `/accessibilite`
// pair shares a title key; the upcoming route fusion (per ADR-0019)
// collapses them to a single localised route.
const homeTitle = $localize`:@@route.home.title:APF Portal`;
const accessibilityTitle = $localize`:@@route.accessibility.title:Accessibility statement · APF Portal`;
export const appRoutes: Route[] = [
{
path: '',
pathMatch: 'full',
loadComponent: () => import('./pages/home/home').then((m) => m.Home),
title: 'APF Portal',
title: homeTitle,
},
// RGAA + ADR-0016: the accessibility statement must be reachable
// from both locales' canonical paths. Same component, content
@@ -15,13 +22,13 @@ export const appRoutes: Route[] = [
loadComponent: () =>
import('./pages/accessibility/accessibility').then((m) => m.AccessibilityStatement),
data: { lang: 'en' },
title: 'Accessibility statement · APF Portal',
title: accessibilityTitle,
},
{
path: 'accessibilite',
loadComponent: () =>
import('./pages/accessibility/accessibility').then((m) => m.AccessibilityStatement),
data: { lang: 'fr' },
title: 'Déclaration daccessibilité · APF Portal',
title: accessibilityTitle,
},
];
@@ -1,14 +1,16 @@
<footer
i18n-aria-label="@@footer.aria.label"
aria-label="Page footer"
class="flex h-10 shrink-0 items-center justify-between border-t border-gray-200 bg-white px-4 text-xs text-gray-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 sm:px-6"
>
<p>&copy; {{ year }} APF France handicap</p>
<p i18n="@@footer.copyright">&copy; {{ year }} APF France handicap</p>
<nav aria-label="Legal">
<nav i18n-aria-label="@@footer.aria.legal" aria-label="Legal">
<ul class="flex items-center gap-4">
<li>
<a
routerLink="/accessibility"
lang="en"
class="rounded hover:text-brand-primary-500 hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-brand-primary-500 dark:hover:text-brand-primary-300"
>Accessibility</a
>
@@ -17,6 +19,7 @@
<li>
<a
routerLink="/accessibilite"
lang="fr"
class="rounded hover:text-brand-primary-500 hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-brand-primary-500 dark:hover:text-brand-primary-300"
>Accessibilité</a
>
@@ -16,14 +16,16 @@
class="h-9 w-9 shrink-0"
/>
@if (!collapsed()) {
<span class="text-base font-semibold sm:text-lg">APF Portal</span>
<span class="text-base font-semibold sm:text-lg" i18n="@@header.wordmark">APF Portal</span>
}
</a>
</div>
<div class="flex flex-1 items-center gap-4 px-4 sm:px-6">
<form role="search" class="mx-2 hidden flex-1 md:flex" (submit)="$event.preventDefault()">
<label class="sr-only" for="global-search">Search the portal</label>
<label class="sr-only" for="global-search" i18n="@@header.search.label"
>Search the portal</label
>
<div
class="flex w-full max-w-xl items-center gap-2 rounded-full border border-gray-200 bg-gray-50 px-4 py-2 focus-within:border-brand-primary-500 focus-within:bg-white focus-within:ring-2 focus-within:ring-brand-primary-200 dark:border-gray-700 dark:bg-gray-800 dark:focus-within:border-brand-primary-300 dark:focus-within:bg-gray-900 dark:focus-within:ring-brand-primary-700"
>
@@ -31,6 +33,7 @@
<input
id="global-search"
type="search"
i18n-placeholder="@@header.search.placeholder"
placeholder="Search…"
class="w-full bg-transparent text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none dark:text-gray-100 dark:placeholder:text-gray-500"
/>
@@ -41,6 +44,7 @@
<button
type="button"
class="relative inline-flex h-11 w-11 items-center justify-center rounded-full text-gray-600 transition-colors hover:bg-gray-100 hover:text-brand-primary-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary-500 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-brand-primary-300"
i18n-aria-label="@@header.action.notifications"
aria-label="Notifications"
>
<app-icon name="bell" />
@@ -52,6 +56,7 @@
<button
type="button"
class="inline-flex h-11 w-11 items-center justify-center rounded-full text-gray-600 transition-colors hover:bg-gray-100 hover:text-brand-primary-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary-500 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-brand-primary-300"
i18n-aria-label="@@header.action.help"
aria-label="Help"
>
<app-icon name="circle-help" />
@@ -60,11 +65,13 @@
<button
type="button"
class="inline-flex h-11 w-11 items-center justify-center rounded-full text-gray-600 transition-colors hover:bg-gray-100 hover:text-brand-primary-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary-500 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-brand-primary-300"
i18n-aria-label="@@header.action.settings"
aria-label="Settings"
>
<app-icon name="settings" />
</button>
<span
i18n-aria-label="@@header.action.userMenu"
aria-label="User menu (coming soon)"
class="ml-1 inline-flex h-9 w-9 items-center justify-center rounded-full bg-brand-primary-500 text-sm font-semibold text-white"
>
@@ -1,9 +1,14 @@
<aside
class="sidebar flex h-full flex-col border-r border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900"
[class.sidebar--collapsed]="collapsed()"
[attr.aria-label]="'Sidebar navigation'"
i18n-aria-label="@@sidebar.aria.label"
aria-label="Sidebar navigation"
>
<nav aria-label="Sections" class="flex-1 overflow-y-auto px-2 py-4">
<nav
i18n-aria-label="@@sidebar.aria.sections"
aria-label="Sections"
class="flex-1 overflow-y-auto px-2 py-4"
>
@for (group of menu; track group.label) {
<div class="mb-4">
@if (!collapsed()) {
@@ -55,10 +60,13 @@
@if (!collapsed()) {
<p
class="mb-2 px-3 text-xs text-gray-500 dark:text-gray-400"
i18n-aria-label="@@sidebar.role.aria"
aria-label="Current role: anonymous (signed-out)"
>
Role:
<span class="font-medium text-gray-700 dark:text-gray-200">Anonymous</span>
<ng-container i18n="@@sidebar.role.label">Role:</ng-container>
<span class="font-medium text-gray-700 dark:text-gray-200" i18n="@@sidebar.role.anonymous"
>Anonymous</span
>
</p>
}
@@ -67,13 +75,11 @@
(click)="toggle()"
class="sidebar__toggle"
[attr.aria-expanded]="!collapsed()"
[attr.aria-label]="
collapsed() ? 'Expand sidebar' : 'Collapse sidebar'
"
[attr.aria-label]="toggleAriaLabel()"
>
<app-icon [name]="collapsed() ? 'chevron-right' : 'chevron-left'" />
@if (!collapsed()) {
<span class="sidebar__label">Collapse</span>
<span class="sidebar__label" i18n="@@sidebar.toggle.collapse">Collapse</span>
}
</button>
</div>
@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { Icon, type IconName } from '../icon/icon';
import { LayoutStateService } from '../../state/layout-state.service';
@@ -21,6 +21,11 @@ import { LayoutStateService } from '../../state/layout-state.service';
* plug in a filter without restructuring the data. The Dashboard
* entry uses `routerLink="/"` so the active-state styling is visible
* on the home page; the rest are anchors until real routes exist.
*
* String localisation: the menu data uses `$localize` tagged template
* literals (per ADR-0019). The literals evaluate at module load — the
* `@angular/localize/init` polyfill is loaded before the bundle so
* the active-locale value is resolved by the time MENU is built.
*/
export interface MenuItem {
@@ -40,24 +45,28 @@ export interface MenuGroup {
const MENU: readonly MenuGroup[] = [
{
label: 'General',
label: $localize`:@@sidebar.group.general:General`,
items: [
{ label: 'Dashboard', icon: 'layout-dashboard', routerLink: '/' },
{ label: 'Calendar', icon: 'calendar', href: '#' },
{
label: $localize`:@@sidebar.item.dashboard:Dashboard`,
icon: 'layout-dashboard',
routerLink: '/',
},
{ label: $localize`:@@sidebar.item.calendar:Calendar`, icon: 'calendar', href: '#' },
],
},
{
label: 'Establishments',
label: $localize`:@@sidebar.group.establishments:Establishments`,
items: [
{ label: 'Directory', icon: 'building-2', href: '#' },
{ label: 'Activities', icon: 'folder', href: '#' },
{ label: $localize`:@@sidebar.item.directory:Directory`, icon: 'building-2', href: '#' },
{ label: $localize`:@@sidebar.item.activities:Activities`, icon: 'folder', href: '#' },
],
},
{
label: 'Communication',
label: $localize`:@@sidebar.group.communication:Communication`,
items: [
{ label: 'Messages', icon: 'mail', href: '#' },
{ label: 'Documents', icon: 'file-text', href: '#' },
{ label: $localize`:@@sidebar.item.messages:Messages`, icon: 'mail', href: '#' },
{ label: $localize`:@@sidebar.item.documents:Documents`, icon: 'file-text', href: '#' },
],
},
];
@@ -75,6 +84,12 @@ export class Sidebar {
protected readonly menu = MENU;
protected readonly collapsed = this.layout.sidebarCollapsed;
protected readonly toggleAriaLabel = computed(() =>
this.collapsed()
? $localize`:@@sidebar.toggle.aria.expand:Expand sidebar`
: $localize`:@@sidebar.toggle.aria.collapse:Collapse sidebar`,
);
protected toggle(): void {
this.layout.toggleSidebar();
}
@@ -2,13 +2,19 @@
type="button"
class="inline-flex h-11 w-11 items-center justify-center rounded-full text-gray-600 transition-colors hover:bg-gray-100 hover:text-brand-primary-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary-500 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-brand-primary-300"
[cdkMenuTriggerFor]="themeMenu"
[attr.aria-label]="'Theme: ' + currentLabel() + ' (open menu)'"
[attr.aria-label]="triggerAriaLabel()"
>
<app-icon [name]="currentIcon()" />
</button>
<ng-template #themeMenu>
<div cdkMenu class="theme-switcher__menu" role="menu" aria-label="Theme">
<div
cdkMenu
class="theme-switcher__menu"
role="menu"
i18n-aria-label="@@theme.menu.aria"
aria-label="Theme"
>
@for (option of options; track option.mode) {
<button
type="button"
@@ -31,9 +31,9 @@ interface ModeOption {
}
const MODE_OPTIONS: readonly ModeOption[] = [
{ mode: 'light', label: 'Light', icon: 'sun' },
{ mode: 'dark', label: 'Dark', icon: 'moon' },
{ mode: 'auto', label: 'System', icon: 'monitor' },
{ mode: 'light', label: $localize`:@@theme.mode.light:Light`, icon: 'sun' },
{ mode: 'dark', label: $localize`:@@theme.mode.dark:Dark`, icon: 'moon' },
{ mode: 'auto', label: $localize`:@@theme.mode.system:System`, icon: 'monitor' },
];
@Component({
@@ -60,7 +60,12 @@ export class ThemeSwitcher {
() => MODE_OPTIONS.find((o) => o.mode === this.mode())?.icon ?? 'monitor',
);
protected readonly currentLabel = computed(
() => MODE_OPTIONS.find((o) => o.mode === this.mode())?.label ?? 'System',
() =>
MODE_OPTIONS.find((o) => o.mode === this.mode())?.label ??
$localize`:@@theme.mode.system:System`,
);
protected readonly triggerAriaLabel = computed(
() => $localize`:@@theme.trigger.aria:Theme: ${this.currentLabel()}:mode: (open menu)`,
);
protected select(mode: ThemeMode): void {
+30 -10
View File
@@ -1,8 +1,14 @@
<section class="mx-auto max-w-3xl px-6 py-12">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-gray-100">
<h1
class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-gray-100"
i18n="@@page.home.title"
>
Welcome to APF Portal
</h1>
<p class="mt-4 text-base leading-relaxed text-gray-700 dark:text-gray-300">
<p
class="mt-4 text-base leading-relaxed text-gray-700 dark:text-gray-300"
i18n="@@page.home.intro"
>
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.
</p>
@@ -11,26 +17,40 @@
aria-labelledby="system-status-heading"
class="mt-10 rounded-md border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-800 dark:bg-gray-900"
>
<h2 id="system-status-heading" class="text-base font-medium text-gray-900 dark:text-gray-100">
<h2
id="system-status-heading"
class="text-base font-medium text-gray-900 dark:text-gray-100"
i18n="@@page.home.status.title"
>
System status
</h2>
@if (isLoading()) {
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400" aria-live="polite">
<p
class="mt-2 text-sm text-gray-600 dark:text-gray-400"
aria-live="polite"
i18n="@@page.home.status.loading"
>
Checking the backend…
</p>
} @else if (health(); as h) {
<dl class="mt-3 grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
<dt class="text-gray-500 dark:text-gray-400">Service</dt>
<dt class="text-gray-500 dark:text-gray-400" i18n="@@page.home.status.service">Service</dt>
<dd class="font-medium text-gray-900 dark:text-gray-100">{{ h.service }}</dd>
<dt class="text-gray-500 dark:text-gray-400">Version</dt>
<dt class="text-gray-500 dark:text-gray-400" i18n="@@page.home.status.version">Version</dt>
<dd class="font-medium text-gray-900 dark:text-gray-100">{{ h.version }}</dd>
<dt class="text-gray-500 dark:text-gray-400">Uptime</dt>
<dd class="font-medium text-gray-900 dark:text-gray-100">{{ h.uptimeSeconds }} s</dd>
<dt class="text-gray-500 dark:text-gray-400">Status</dt>
<dt class="text-gray-500 dark:text-gray-400" i18n="@@page.home.status.uptime">Uptime</dt>
<dd class="font-medium text-gray-900 dark:text-gray-100">
<ng-container i18n="@@page.home.status.uptimeValue">{{ h.uptimeSeconds }} s</ng-container>
</dd>
<dt class="text-gray-500 dark:text-gray-400" i18n="@@page.home.status.status">Status</dt>
<dd class="font-medium text-green-700 dark:text-green-400">{{ h.status }}</dd>
</dl>
} @else if (isError()) {
<p class="mt-2 text-sm text-amber-700 dark:text-amber-300" role="status">
<p
class="mt-2 text-sm text-amber-700 dark:text-amber-300"
role="status"
i18n="@@page.home.status.error"
>
Backend unreachable. The BFF dev server may not be running, or CORS is misconfigured.
</p>
}
+199 -12
View File
@@ -3,20 +3,207 @@
<file source-language="en" target-language="fr" datatype="plaintext" original="ng2.template">
<body>
<!--
French translation file for portal-shell.
French translations for portal-shell, per ADR-0019.
Empty in this initial plumbing PR per ADR-0019. The first string
sweep PR will mark every UI string in the templates with
`i18n="@@<id>"` and add a `<trans-unit id="<id>">` entry below
with the French translation. Until then, the production build's
`--localize` emits both `dist/apps/portal-shell/browser/en/...`
and `dist/apps/portal-shell/browser/fr/...`, but the FR bundle
falls back to source text for every untranslated key.
Editor convention: keep the entries grouped by feature
(header.*, sidebar.*, footer.*, page.<route>.*) and ordered
alphabetically within each group, so diffs stay readable.
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.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.action.settings" datatype="html">
<source>Settings</source>
<target>Paramètres</target>
</trans-unit>
<trans-unit id="header.action.userMenu" datatype="html">
<source>User menu (coming soon)</source>
<target>Menu utilisateur (bientôt disponible)</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="sidebar.role.anonymous" datatype="html">
<source>Anonymous</source>
<target>Anonyme</target>
</trans-unit>
<trans-unit id="sidebar.role.aria" datatype="html">
<source>Current role: anonymous (signed-out)</source>
<target>Rôle actuel : anonyme (non connecté)</target>
</trans-unit>
<trans-unit id="sidebar.role.label" datatype="html">
<source>Role:</source>
<target>Rôle :</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>
<!-- 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>
+1 -1
View File
@@ -2,7 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": []
"types": ["@angular/localize"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
+1 -1
View File
@@ -2,7 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["vitest/globals"]
"types": ["vitest/globals", "@angular/localize"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts"]
}