# Rationalisation i18n pré-launch — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Interface limitée à EN+FR, traductions FR auditées, drapeaux SVG unifiés (footprint JS ≈ 0) partout.

**Architecture:** 3 volets indépendants. (1) `src/i18n/config.ts` réduit à en/fr. (2) Composant `<Flag>` rendant des SVG servis en assets statiques `public/flags/`, remplaçant les 4 implémentations emoji. (3) Script d'audit FR + garde anti-régression.

**Tech Stack:** Next.js 16 + next-intl + Bun + Drizzle + Tailwind/shadcn. Lib `flag-icons` en devDependency (source de SVG uniquement).

**Spec:** `docs/superpowers/specs/2026-07-24-i18n-launch-design.md`

## Global Constraints

- Interface = **EN + FR uniquement**. Ne PAS toucher les langues de *site* (`GOOGLE_ADS_LANGUAGES`, 51 langues) ni la langue de *contenu* des keywords — axes distincts de la langue d'interface.
- Drapeaux : **zéro SVG dans le bundle JS**. SVG = assets statiques `public/flags/{code}.svg` (code alpha-2 minuscule), rendus via `<img loading="lazy">`. `flag-icons` est une **devDependency**, jamais importée dans le code runtime.
- Filtre pays des toolbars : reste **dérivé des données** (pays présents) — on ne change QUE le rendu (label + `<Flag>`), pas la source `facets.countries`.
- Le drapeau collé sur une langue (badge backlink) est **conservé** mais rendu via `<Flag>`.
- Installer toute dépendance via `sfw` (Socket Firewall) : `sfw bun add ...`.
- `public/flags/*` doit être servi sans auth : exclure `flags` du matcher `src/proxy.ts` (même correctif que `logo`/`favicon.ico`).
- Tests : `bun run test <path>` (PAS `bun test` — l'env de test vit dans le script npm) ; `bunx tsc --noEmit` ; biome sur les fichiers touchés.
- Conventional Commits ; chaque message finit par les deux trailers :
  `Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>` puis `Claude-Session: https://claude.ai/code/session_01TPVZEibkftCoz8dwbRSMkn`.
- Hook local : jamais heredoc + `git commit` dans la même commande ; si `-m` multiple rejeté, `git commit -F <fichier>`. Ne JAMAIS `git stash` (stash GitHub Desktop de l'utilisateur à préserver).
- Branche de travail : `feature/i18n-launch` (depuis `dev`).

---

### Task 1 : Interface EN+FR

**Files:**
- Modify: `src/i18n/config.ts`
- Modify: `src/i18n/catalogs.test.ts`
- Delete: `messages/de.json`, `messages/es.json`
- Verify (no change expected): `src/components/i18n/language-selector.tsx`, `src/i18n/locale.ts`

**Interfaces:**
- Produces : `locales = ["en","fr"]`, `localeLabels = { en, fr }` (consommés par le sélecteur et les tests).

- [ ] **Step 1 : réduire `src/i18n/config.ts`**

```ts
export const locales = ["en", "fr"] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "en";

export const localeLabels: Record<Locale, string> = {
  en: "English",
  fr: "Français",
};

export function isLocale(value: string | null | undefined): value is Locale {
  return value != null && (locales as readonly string[]).includes(value);
}
```

- [ ] **Step 2 : nettoyer `src/i18n/catalogs.test.ts`** — retirer les imports `de`/`es` et les sortir de l'objet `catalogs` (sinon l'import échoue après suppression des JSON) :

```ts
import { describe, expect, it } from "bun:test";
import en from "../../messages/en.json";
import fr from "../../messages/fr.json";
import { locales } from "./config";

const catalogs: Record<string, unknown> = { en, fr };
```

(le reste du fichier — `keyPaths`, la boucle `for (const loc of locales)` — est inchangé ; il n'itère plus que `en`/`fr`.)

- [ ] **Step 3 : supprimer les catalogues morts**

```bash
git rm messages/de.json messages/es.json
```

- [ ] **Step 4 : vérifier les consommateurs** — grep pour tout usage résiduel en dur de `"de"`/`"es"` comme locale d'INTERFACE (ne pas confondre avec les langues de contenu/site) :

```bash
grep -rn '"de"\|"es"' src/i18n/ && echo "à inspecter" || echo "clean"
```

Attendu : plus aucune référence `de`/`es` dans `src/i18n/`. Le `language-selector.tsx` itère `locales` → n'affiche plus que EN/FR sans modification. Un `user.locale`/cookie resté `de`/`es` retombe sur `defaultLocale` via `isLocale()` (rétrocompat, pas de migration DB).

- [ ] **Step 5 : vérifs**

Run: `bun run test src/i18n/` — attendu : parité OK sur en/fr, 0 fail. Puis `bunx tsc --noEmit` (exit 0) et `bunx biome check src/i18n/config.ts src/i18n/catalogs.test.ts`.

- [ ] **Step 6 : commit**

```bash
git add src/i18n/config.ts src/i18n/catalogs.test.ts
git commit -m "feat(i18n): interface limitee a EN+FR (retire DE/ES)"
```

---

### Task 2 : Composant `<Flag>` + assets SVG + script + exclusion proxy

**Files:**
- Create: `src/components/ui/flag.tsx`
- Create: `scripts/sync-flags.ts`
- Create: `public/flags/*.svg` (générés par le script)
- Modify: `src/proxy.ts` (matcher : ajouter `flags`)
- Modify: `package.json` (devDependency `flag-icons`)
- Test: `src/components/ui/flag.test.tsx`

**Interfaces:**
- Produces : `<Flag code={string} className?: string title?: string />` (rendu `<img src="/flags/{code}.svg">` + fallback). Consommé par Tasks 3 et 4.

- [ ] **Step 1 : mesurer avant de figer (data-driven)** — comparer honnêtement le poids : l'approche assets (`<img>`, ~0 JS) vs `country-flag-icons` en imports. Écrire une ligne dans le rapport avec les tailles. L'approche assets est le défaut fort (le combobox 250 pays casse le tree-shaking d'une lib React). Si la mesure contredit, escalader — sinon continuer.

- [ ] **Step 2 : installer la source de SVG (devDependency)**

```bash
sfw bun add -d flag-icons
```

Vérifier le chemin réel des SVG rectangulaires après install : `ls node_modules/flag-icons/flags/4x3/ | head` (fichiers `{alpha2-minuscule}.svg`, ex. `fr.svg`, `gb.svg`). Si le chemin diffère (`flags/4x3` absent), inspecter `node_modules/flag-icons/flags/` et adapter le script.

- [ ] **Step 3 : script de copie `scripts/sync-flags.ts`**

```ts
// scripts/sync-flags.ts — copie les SVG de drapeaux (flag-icons, devDep) vers
// public/flags/ pour un service statique. Idempotent. À relancer si flag-icons
// est mis à jour. flag-icons n'est JAMAIS importée dans le code runtime — seule
// sa collection de fichiers SVG sert (footprint JS = 0).
import { cp, mkdir, readdir } from "node:fs/promises";
import { join } from "node:path";

const SRC = "node_modules/flag-icons/flags/4x3";
const DEST = "public/flags";

async function main() {
  await mkdir(DEST, { recursive: true });
  const files = (await readdir(SRC)).filter((f) => f.endsWith(".svg"));
  for (const f of files) {
    await cp(join(SRC, f), join(DEST, f));
  }
  console.log(`Copié ${files.length} drapeaux vers ${DEST}/`);
}

main();
```

- [ ] **Step 4 : lancer le script et committer les assets**

```bash
bun run scripts/sync-flags.ts
ls public/flags/ | wc -l   # ~260 fichiers attendus
```

(Décision spec : on COMMITTE `public/flags/` — statiques, aucun impact bundle. Ajouter un script npm `"sync-flags": "bun run scripts/sync-flags.ts"` dans package.json pour la maintenance.)

- [ ] **Step 5 : test failing du composant `src/components/ui/flag.test.tsx`**

Suivre le pattern de test de composant du repo (regarde un `*.test.tsx` existant sous `src/components/` pour l'invocation `@testing-library/react` ou l'équivalent utilisé ; si aucun test de rendu React n'existe, faire un test pur sur la résolution du `src` en extrayant une petite fonction pure `flagSrc(code): string | null`). Cas :

```tsx
// resolution pure du chemin (extraite pour testabilité)
import { expect, test } from "bun:test";
import { flagSrc } from "./flag";

test("code alpha-2 valide -> chemin minuscule", () => {
  expect(flagSrc("FR")).toBe("/flags/fr.svg");
  expect(flagSrc("gb")).toBe("/flags/gb.svg");
});
test("code invalide/vide -> null (fallback)", () => {
  expect(flagSrc("")).toBeNull();
  expect(flagSrc("XXX")).toBeNull();
  expect(flagSrc(null)).toBeNull();
});
```

- [ ] **Step 6 : implémenter `src/components/ui/flag.tsx`**

```tsx
// src/components/ui/flag.tsx — drapeau pays unifié. Rend un SVG servi en
// statique (public/flags/{code}.svg), zéro SVG dans le bundle JS. Fallback
// neutre si le code est inconnu ou le fichier absent (onError).
"use client";

import { GlobeIcon } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";

/** `/flags/{alpha2}.svg` pour un code ISO alpha-2 valide, sinon null. */
export function flagSrc(code: string | null | undefined): string | null {
  if (!code || !/^[a-zA-Z]{2}$/.test(code)) return null;
  return `/flags/${code.toLowerCase()}.svg`;
}

export function Flag({
  code,
  className,
  title,
}: {
  code: string | null | undefined;
  className?: string;
  title?: string;
}) {
  const src = flagSrc(code);
  const [failed, setFailed] = useState(false);
  if (!src || failed) {
    return (
      <GlobeIcon
        aria-label={title ?? undefined}
        className={cn("inline-block h-4 w-4 text-muted-foreground", className)}
      />
    );
  }
  return (
    // biome-ignore lint/nursery/noImgElement: SVG statique servi à la demande, next/image inutile
    <img
      src={src}
      alt={title ?? ""}
      title={title}
      loading="lazy"
      onError={() => setFailed(true)}
      className={cn("inline-block h-4 w-auto rounded-[2px]", className)}
    />
  );
}
```

- [ ] **Step 7 : exclure `flags` du middleware `src/proxy.ts`** — dans le `matcher`, ajouter `flags` à la liste d'exclusion (à côté de `favicon.ico|logo`) :

```ts
"/((?!login|signup|two-factor|forgot-password|reset-password|accept-invitation|pricing|api/auth|api/worker|api/mcp|_next/static|_next/image|favicon.ico|logo|flags).*)",
```

Et compléter le commentaire au-dessus du matcher pour mentionner `flags` (assets drapeaux publics, servis sur des pages non authentifiées).

- [ ] **Step 8 : vérifs** — `bun run test src/components/ui/flag.test.tsx` (0 fail), `bunx tsc --noEmit`, biome. Vérifier via le navigateur d3k (si dispo) qu'un `/flags/fr.svg` se sert bien en 200 (le proxy ne le redirige plus).

- [ ] **Step 9 : commit**

```bash
git add src/components/ui/flag.tsx src/components/ui/flag.test.tsx scripts/sync-flags.ts public/flags package.json bun.lock src/proxy.ts
git commit -m "feat(ui): composant Flag (SVG statiques, footprint JS ~0) + exclusion proxy"
```

---

### Task 3 : Migrer `countryFlag()` → `<Flag>` (données & toolbars)

**Files:**
- Delete: `src/lib/keywords/country-flag.ts`
- Modify: les ~15 consommateurs de `countryFlag` (liste ci-dessous)

**Interfaces:**
- Consumes : `<Flag>` (Task 2).

- [ ] **Step 1 : recenser les usages**

```bash
grep -rln "countryFlag" src/ --include="*.tsx" --include="*.ts" | grep -v country-flag.ts
```

Consommateurs attendus (cf. cartographie) : `content/*-toolbar/columns/card`, `online-toolbar/table`, `coverage-matrix`, `research-columns/toolbar`, `keyword-modal-rank-card`, `columns`, `keyword-overview-modal`, `planning-table`, `keyword-picker-dialog`, `reservoir-toolbar`, `keyword-sheet`.

- [ ] **Step 2 : remplacer chaque usage.** Pattern : `countryFlag(x)` renvoyait un **string emoji** rendu inline (souvent `{countryFlag(x)}` ou dans une concat). Le remplacer par le composant `<Flag code={x} title={countryLabel(x)} />`.
  - Si l'emoji était concaténé dans une string (ex. `` `${countryFlag(c)} ${name}` ``), séparer en JSX : `<><Flag code={c} /> {name}</>`.
  - Si l'emoji était rendu seul dans une cellule/badge, remplacer directement par `<Flag code={c} />`.
  - Importer `Flag` depuis `@/components/ui/flag`. Retirer l'import `countryFlag`.
  - **Attention `"use client"`** : `<Flag>` est un composant client. Les fichiers qui l'utilisent sont déjà des composants client (toolbars/tables) ; vérifier qu'aucun n'est un Server Component pur (si oui, le drapeau doit être rendu dans un sous-composant client — mais les tables/toolbars keywords sont client).

- [ ] **Step 3 : filtre pays des toolbars.** Dans `reservoir-toolbar.tsx` et `research/research-toolbar.tsx`, les options de pays (dérivées de `facets.countries`) qui rendaient `countryFlag(c)` rendent désormais `<Flag code={c} />` + le nom via `countryLabel(c)` (nom seul après Task 4 ; si Task 4 pas encore faite, `countryLabel` renvoie encore `flag+name` — voir NB). Rendu cible : `<Flag code={c} /> {countryLabel(c)}`.

  **NB ordre** : cette task précède Task 4, donc `countryLabel()` retourne encore `"🇫🇷 France"`. Pour éviter un double drapeau transitoire, dans ces deux toolbars afficher pour l'instant le **nom brut** via `countryByCode`/le nom, PAS `countryLabel`. La bascule propre sur `countryLabel` (nom seul) se fait en Task 4. Documenter ce choix transitoire en commentaire.

- [ ] **Step 4 : supprimer la fonction morte**

```bash
git rm src/lib/keywords/country-flag.ts
grep -rn "countryFlag" src/ && echo "RESTE" || echo "OK aucun usage"
```

Attendu : `OK aucun usage`.

- [ ] **Step 5 : vérifs** — `bunx tsc --noEmit` (0), biome sur les fichiers touchés, et les suites de tests couvrant les composants modifiés si elles existent (`bun run test src/lib/keywords/ src/components/keywords/` — noter les fails préexistants, 0 nouvel échec).

- [ ] **Step 6 : commit**

```bash
git add -A
git commit -m "refactor(flags): migre countryFlag emoji vers <Flag> (donnees + toolbars)"
```

---

### Task 4 : `options.ts` structuré + combobox setup + badge langue

**Files:**
- Modify: `src/lib/sites/options.ts`
- Modify: `src/components/site/single-combobox.tsx`, `src/components/site/multi-combobox.tsx` (+ tout consommateur rendant `option.label` pour des PAYS)
- Modify: `src/components/backlink/source-badges.tsx`

**Interfaces:**
- Consumes : `<Flag>` (Task 2).
- Produces : `COUNTRY_OPTIONS` avec champ `code`, `countryLabel()` renvoyant le **nom seul**.

- [ ] **Step 1 : `options.ts` — dissocier drapeau et label.**
  - Supprimer `flagEmoji()`.
  - `COUNTRY_OPTIONS` : ajouter le code alpha-2 pour permettre le rendu `<Flag>`. Étendre l'interface : garder `ComboOption { value, label }` pour les langues, et pour les pays exposer `value = alpha-2`, `label = name` (nom seul), le drapeau étant dérivé de `value`. Concrètement :

```ts
export const COUNTRY_OPTIONS: ComboOption[] = [...GOOGLE_ADS_COUNTRIES]
  .sort(byName)
  .map((c) => ({ value: c.code, label: c.name })); // nom seul ; drapeau via <Flag code={value}/>

/** Nom d'affichage (SANS drapeau) pour un code alpha-2 stocké. */
export function countryLabel(code: string): string {
  const c = countryByCode.get(code.trim().toUpperCase());
  return c ? c.name : code;
}
```

  `LANGUAGE_OPTIONS` et `languageLabel()` inchangés (pas de drapeau sur les langues).

- [ ] **Step 2 : combobox setup — rendre `<Flag>` pour les options PAYS.** Dans `single-combobox.tsx`/`multi-combobox.tsx` (et les dialogs consommateurs de `COUNTRY_OPTIONS` : `add-keyword-dialog`, `import-keywords-dialog`, `import-brief-dialog`, `targeting-bar`, `coverage-matrix`, `projects/page`), là où l'option pays rendait `option.label` (qui contenait l'emoji), rendre `<Flag code={option.value} /> {option.label}`.
  - Les combobox sont génériques (pays ET langues). Ne PAS ajouter de drapeau aux options de LANGUE. Deux voies acceptables : (a) un flag optionnel passé par le call-site pays (`renderFlag`), ou (b) un prop booléen `withFlag` sur le combobox activé côté pays. Choisir la plus simple selon la structure réelle des combobox (les lire d'abord) et documenter.

- [ ] **Step 3 : basculer les toolbars sur `countryLabel` (nom seul).** Revenir sur le choix transitoire de Task 3 Step 3 : dans `reservoir-toolbar.tsx`/`research-toolbar.tsx`, utiliser `<Flag code={c} /> {countryLabel(c)}` (countryLabel renvoie désormais le nom seul). Plus de double drapeau.

- [ ] **Step 4 : badge langue backlink (`source-badges.tsx`).** Transformer `LANGUAGE_FLAG` (langue→emoji) en map langue→**alpha-2**, et `LanguageFlag` rend `<Flag>` :

```tsx
import { Flag } from "@/components/ui/flag";

/** Langue -> pays du drapeau (choix éditorial : en -> GB). */
const LANGUAGE_COUNTRY: Record<string, string> = {
  en: "GB", fr: "FR", de: "DE", es: "ES", it: "IT", pt: "PT",
  nl: "NL", pl: "PL", sv: "SE", da: "DK", no: "NO", fi: "FI",
  ru: "RU", ja: "JP", zh: "CN", ko: "KR",
};

export function LanguageFlag({ code }: { code: string }) {
  const country = LANGUAGE_COUNTRY[code.toLowerCase()];
  return <Flag code={country} title={code.toUpperCase()} className="h-3.5" />;
}
```

  (`<Flag>` gère déjà le fallback si `country` est `undefined`.)

- [ ] **Step 5 : chasse aux emojis résiduels**

```bash
grep -rn "flagEmoji" src/ && echo "RESTE" || echo "OK"
grep -rnP "\x{1F1E6}-\x{1F1FF}" src/ --include="*.ts" --include="*.tsx" | grep -v flag.tsx || echo "OK aucun emoji drapeau"
```

Attendu : plus de `flagEmoji`, plus d'emoji regional-indicator dans le code (hors éventuels tests/allowlist).

- [ ] **Step 6 : vérifs** — `bunx tsc --noEmit` (0), biome, `bun run test src/lib/sites/` (0 nouvel échec).

- [ ] **Step 7 : commit**

```bash
git add -A
git commit -m "refactor(flags): options pays structurees + combobox/badge langue via <Flag>, supprime flagEmoji"
```

---

### Task 5 : Audit traductions FR

**Files:**
- Create: `scripts/i18n-audit.ts`
- Create: `src/i18n/untranslated-allowlist.ts`
- Create: `src/i18n/untranslated.test.ts`
- Modify: `messages/fr.json` (corrections)

**Interfaces:**
- Consumes : `messages/{en,fr}.json`.

- [ ] **Step 1 : allowlist `src/i18n/untranslated-allowlist.ts`**

```ts
// src/i18n/untranslated-allowlist.ts — valeurs FR légitimement identiques à
// l'EN (sigles, marques, symboles). Deux niveaux : valeurs globalement
// tolérées, et dot-paths explicitement validés.
export const ALLOWED_IDENTICAL_VALUES = new Set<string>([
  "SEO", "GSC", "CPC", "KD", "URL", "API", "EN", "FR", "OK", "SERP",
  "Google", "GitHub", "Search Console", "SE Ranking", "Ahrefs", "Semrush",
]);

/** dot-paths (clé aplatie) dont la valeur FR PEUT rester identique à l'EN. */
export const ALLOWED_IDENTICAL_KEYS = new Set<string>([
  // à compléter au fil de l'audit avec les faux positifs vérifiés
]);
```

- [ ] **Step 2 : script d'audit `scripts/i18n-audit.ts`**

```ts
// scripts/i18n-audit.ts — repère les valeurs de fr.json identiques à en.json
// (candidat "anglais non traduit"), hors allowlist. Sortie : rapport + exit 1
// si des non-traduits hors allowlist subsistent (utilisable en CI plus tard).
import en from "../messages/en.json";
import fr from "../messages/fr.json";
import {
  ALLOWED_IDENTICAL_KEYS,
  ALLOWED_IDENTICAL_VALUES,
} from "../src/i18n/untranslated-allowlist";

type Flat = Record<string, string>;
function flatten(obj: unknown, prefix = "", out: Flat = {}): Flat {
  if (obj && typeof obj === "object") {
    for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
      flatten(v, prefix ? `${prefix}.${k}` : k, out);
    }
  } else if (typeof obj === "string") {
    out[prefix] = obj;
  }
  return out;
}

const enFlat = flatten(en);
const frFlat = flatten(fr);
const suspects: string[] = [];
for (const [key, enVal] of Object.entries(enFlat)) {
  const frVal = frFlat[key];
  if (frVal === undefined) continue; // parité gérée par catalogs.test.ts
  if (frVal !== enVal) continue; // traduit
  if (ALLOWED_IDENTICAL_VALUES.has(frVal.trim())) continue;
  if (ALLOWED_IDENTICAL_KEYS.has(key)) continue;
  suspects.push(`${key} = ${JSON.stringify(frVal)}`);
}
if (suspects.length) {
  console.log(`\n${suspects.length} valeur(s) FR non traduite(s) (hors allowlist) :`);
  for (const s of suspects) console.log("  " + s);
  process.exit(1);
}
console.log("Aucune valeur FR non traduite (hors allowlist).");
```

- [ ] **Step 3 : lancer l'audit, trier les résultats**

```bash
bun run scripts/i18n-audit.ts
```

Pour CHAQUE clé signalée : soit c'est un vrai non-traduit → traduire dans `messages/fr.json` ; soit c'est un faux positif légitime (sigle/marque manqué) → ajouter la valeur à `ALLOWED_IDENTICAL_VALUES` ou le dot-path à `ALLOWED_IDENTICAL_KEYS`. **Ne pas** faire taire un vrai non-traduit via l'allowlist. Relancer jusqu'à exit 0.

- [ ] **Step 4 : garde anti-régression `src/i18n/untranslated.test.ts`**

```ts
import { expect, test } from "bun:test";
import en from "../../messages/en.json";
import fr from "../../messages/fr.json";
import {
  ALLOWED_IDENTICAL_KEYS,
  ALLOWED_IDENTICAL_VALUES,
} from "./untranslated-allowlist";

function flatten(obj: unknown, prefix = "", out: Record<string, string> = {}) {
  if (obj && typeof obj === "object") {
    for (const [k, v] of Object.entries(obj as Record<string, unknown>))
      flatten(v, prefix ? `${prefix}.${k}` : k, out);
  } else if (typeof obj === "string") out[prefix] = obj;
  return out;
}

test("aucune valeur FR identique à EN hors allowlist", () => {
  const enFlat = flatten(en);
  const frFlat = flatten(fr);
  const suspects = Object.entries(enFlat)
    .filter(([k, enVal]) => {
      const frVal = frFlat[k];
      return (
        frVal !== undefined &&
        frVal === enVal &&
        !ALLOWED_IDENTICAL_VALUES.has(frVal.trim()) &&
        !ALLOWED_IDENTICAL_KEYS.has(k)
      );
    })
    .map(([k]) => k);
  expect(suspects).toEqual([]);
});
```

- [ ] **Step 5 : vérifs** — `bun run test src/i18n/` (parité + non-traduit, 0 fail), `bunx tsc --noEmit`, biome.

- [ ] **Step 6 : commit**

```bash
git add scripts/i18n-audit.ts src/i18n/untranslated-allowlist.ts src/i18n/untranslated.test.ts messages/fr.json package.json
git commit -m "test(i18n): audit + garde anti-regression des traductions FR non traduites"
```

---

## Fin de branche

Review finale de branche (diff `dev..HEAD`) par le reviewer le plus capable, puis merge ff-only dans `dev` (`superpowers:finishing-a-development-branch`). Vérification visuelle recommandée via d3k : login + un filtre pays (drapeaux SVG cohérents), sélecteur de langue (EN/FR only).
