# Rationalisation i18n / langues / drapeaux (pré-launch) — Design

- **Date** : 2026-07-24
- **Objectif** : préparer le launch côté i18n — interface EN+FR uniquement, traductions FR auditées, drapeaux visuellement cohérents partout.
- **Décisions de cadrage (Bastien)** : interface **EN+FR seulement** ; filtre pays des toolbars = **pays présents** (dérivé) via source centralisée ; drapeaux = **SVG via lib** (rendu identique cross-OS) ; le drapeau collé sur une langue (badge backlink) est **conservé mais rendu via `<Flag/>`**.
- **Hors périmètre (différé)** : la grande refonte des 4 listes de pays statiques (`google-ads.ts` 250, `gsc/country-iso3.ts`, `gsc/language.ts`, `seranking/country-db.ts`) — elles ont des besoins spécialisés distincts. On ne centralise que le nécessaire pour l'affichage (label + drapeau).

Les trois volets sont **indépendants** et peuvent être exécutés séparément.

---

## Volet 1 — Interface EN+FR

**But** : ne plus proposer que l'anglais et le français comme langue d'**interface** (next-intl). N'affecte NI les langues de *site* (setup, 51 langues Google Ads) NI la langue de *contenu* des keywords — axes distincts.

**Changements**
- `src/i18n/config.ts` : `locales = ["en", "fr"] as const` ; `localeLabels` réduit à `{ en: "English", fr: "Français" }` ; `defaultLocale = "en"` inchangé ; `isLocale()` inchangé (rejette désormais `de`/`es`).
- Supprimer `messages/de.json` et `messages/es.json`.
- `src/i18n/messages.d.ts` : reste typé sur `en.json` (référence) — inchangé.
- `src/i18n/catalogs.test.ts` : itère `locales` de config → couvre automatiquement en/fr seulement. **Vérifier** qu'il ne référence pas `de`/`es` en dur (sinon retirer).
- `src/components/i18n/language-selector.tsx` : itère `locales` → n'affiche que EN/FR automatiquement. Aucun changement de code attendu — à vérifier.

**Rétrocompatibilité (utilisateurs déjà en de/es)**
- Cookie `NEXT_LOCALE=de|es` ou `user.locale=de|es` : `getUserLocale()` (`src/i18n/locale.ts`) passe par `isLocale()` qui renverra false → fallback `defaultLocale` (`en`). Pas de crash, dégradation propre.
- **Pas de migration DB requise.** Optionnel (non retenu) : un UPDATE mettant `user.locale` de/es → null. On s'appuie sur le fallback.

**Tests** : `catalogs.test.ts` passe avec 2 locales ; `bunx tsc --noEmit` ; vérifier que le sélecteur ne liste que EN/FR (rendu).

---

## Volet 2 — Audit traductions FR

**But** : garantir que le FR est complet ET réellement traduit (pas d'anglais copié).

**Constat** : `catalogs.test.ts` vérifie la **parité de clés** (toutes les clés de `en.json` présentes dans `fr.json`) mais PAS que les valeurs sont traduites — une valeur anglaise copiée dans `fr.json` passe le test.

**Approche**
1. **Script d'audit** `scripts/i18n-audit.ts` (Bun, one-shot + réutilisable) : compare `messages/fr.json` à `messages/en.json` clé par clé (aplaties en dot-paths), signale les entrées où `fr === en` (candidat non traduit). Applique une **allowlist** de valeurs légitimement identiques : sigles/marques (`SEO`, `GSC`, `CPC`, `KD`, `URL`, `API`, `EN`, `FR`…), symboles, et une liste de dot-paths explicitement validés. Sortie : liste des clés suspectes → rapport.
2. **Correction** : traduire en FR les entrées réellement non traduites détectées.
3. **Garde anti-régression** : étendre `catalogs.test.ts` (ou un nouveau `catalogs-untranslated.test.ts`) d'un test « aucune valeur FR identique à EN hors allowlist ». L'allowlist vit dans un fichier dédié (`src/i18n/untranslated-allowlist.ts`) pour rester maintenable.

**Limite assumée** : l'égalité exacte ne détecte pas une traduction *fausse* (FR grammaticalement correct mais mauvais sens) — hors périmètre, ça reste un audit humain.

**Tests** : le script tourne et produit un rapport ; après correction, le test de garde passe.

---

## Volet 3 — Drapeaux SVG unifiés

**But** : un seul visuel de drapeau partout, identique cross-OS.

**Contrainte forte (Bastien) : footprint JS minimal — « chasse aux octets ».**

Le piège : le setup site liste **250 pays** dans un combobox. Une lib de SVG-React importés statiquement (`country-flag-icons/react/*`, `react-country-flag`…) bundlerait potentiellement les 250 drapeaux dans le JS → inacceptable. On veut **zéro SVG dans le bundle JS** ; les drapeaux sont des **assets servis à la demande**, l'utilisateur ne télécharge que ceux visibles (lazy + cache HTTP).

**Approche retenue** : les SVG vivent en assets statiques `public/flags/{code}.svg` ; le composant les référence en `<img>`. Bundle JS runtime ≈ 0 (juste le composant).

- **Source des SVG** : `flag-icons` (MIT, jeu SVG standard et complet, ~alpha-2). Installée en **devDependency** via `sfw bun add -d flag-icons` — elle n'est PAS importée dans le code runtime ; elle sert uniquement de source de fichiers.
- **Script** `scripts/sync-flags.ts` (Bun) : copie `node_modules/flag-icons/flags/4x3/*.svg` → `public/flags/`, en minifiant si simple (optionnel). Idempotent, relançable. Documenté ; lancé une fois (et à la mise à jour de la lib). Décision à confirmer à l'implémentation : **committer** `public/flags/` (repo +~3 Mo de statiques, aucun impact bundle) OU générer au build — committer est plus simple et sans surprise CI ; retenu par défaut.

**Comparaison mesurée à faire par l'implémenteur (data-driven, pas au jugé)** : avant de figer, comparer le poids réel — (a) approche `<img>`/assets (attendue : ~0 JS) vs (b) `country-flag-icons` en imports sélectifs (léger SI peu de pays, mais le combobox 250 casse ça). Retenir la plus légère **mesurée** ; l'approche assets est le défaut fort.

**Composant unique** `src/components/ui/flag.tsx` :
- `<Flag code={string} className?: string title?: string />` — `code` = alpha-2 (insensible à la casse, normalisé en minuscule pour le nom de fichier). Rend `<img src={"/flags/" + code + ".svg"} loading="lazy" alt={title ?? ""} />`.
- **Fallback** (code inconnu / vide / 404) : `onError` → placeholder neutre (petit rectangle bordé ou icône globe `lucide`), jamais de crash ni d'image cassée. Aria : `alt`/`title` = nom du pays quand fourni.
- Taille pilotée par `className` (ex. `h-4 w-auto`), pas de largeur en dur.
- **Rappel infra** : `public/flags/*` doit être servi sans auth — le middleware `proxy.ts` doit exclure `flags` de son matcher (même correctif que `logo`/`favicon.ico`, sinon les drapeaux sur `/login` ou pages publiques seraient redirigés).

**Remplacements (les 4 implémentations emoji → `<Flag/>`)**
- `src/lib/keywords/country-flag.ts` (`countryFlag()`, ~15 consommateurs) : remplacer chaque usage par `<Flag code={alpha2} />`. Supprimer la fonction une fois tous les usages migrés (grep final vide).
  - Consommateurs (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.
- `src/lib/sites/options.ts` (`flagEmoji()` + `COUNTRY_OPTIONS.label = flag+name`) : **point délicat** — un SVG ne peut pas vivre dans une string `label`. `COUNTRY_OPTIONS` passe d'un `label: string` à une forme structurée exposant `code` + `name` séparément ; les combobox (`site/single-combobox.tsx`, `site/multi-combobox.tsx`, et les dialogs consommateurs) rendent `<Flag code={code}/> {name}`. `countryLabel()` retourne le **nom seul** (sans emoji) ; le drapeau est rendu par le composant. Supprimer `flagEmoji()`.
- `src/components/backlink/source-badges.tsx` (`LANGUAGE_FLAG` map langue→emoji, `<LanguageFlag>`) : conserver le **mapping langue→pays** (choix éditorial `en→GB`, etc.) mais rendre via `<Flag code={pays}/>` au lieu de l'emoji. La map devient `langue → alpha-2` (pas → emoji).
- Emojis littéraux en dur : supprimés au passage de la map ci-dessus.

**Filtre pays des toolbars (pays présents)** : `facets.countries` (dérivé, `list-query.ts`) inchangé côté données ; chaque option rendue avec `<Flag code={country}/>` + label centralisé via `countryLabel()` (nom seul). Ainsi le filtre et le setup partagent le même visuel et les mêmes noms.

**Filtre langue** : reste **sans drapeau** dans les toolbars keywords (principe « langue de contenu ≠ pays », déjà en place) — inchangé. Seul le badge backlink garde un drapeau sur la langue (décision Bastien), via `<Flag/>`.

**Tests** : test de rendu `flag.test.tsx` (code valide → SVG du bon pays ; code inconnu → fallback, pas de throw) ; `bunx tsc --noEmit` ; grep final : plus aucun appel à `countryFlag`/`flagEmoji`/emoji regional-indicator résiduel hors `<Flag/>`.

---

## Ordre d'exécution recommandé

1. **Volet 1** (rapide, réduit la surface) → 2. **Volet 3** (drapeaux, le plus de fichiers) → 3. **Volet 2** (audit FR, en dernier pour auditer le FR stabilisé).

Chaque volet est indépendant et mergeable seul.
