# 09 — Langues et JSON : un récit, plusieurs représentations, plusieurs langues

**Statut : fait · Phase : P3 · Mis à jour : 24/10/2026.**

Deux idées, liées :

1. **Tout `.md` a un équivalent JSON typé.** Un récit peut s'écrire en Markdown (pour les humains),
   en JSON (pour les machines, les CMS, les agents, les données), ou dans les deux : le JSON
   complète alors le Markdown du même nom.
2. **Une même page existe en plusieurs langues** : `example.en.md` et `example.fr.md`, ou un seul
   JSON qui contient toutes les langues. Les deux mécanismes se combinent.

Ce document s'applique aux **neuf formats** (deck, motion, docs, report, toon, book, roadmap,
stories, calendar), à ceux des plans 10 à 13 ajoutés dans leur phase (glossary, directory, atlas,
règle B1), et aux **sites** `*.recit.dev`.

## 1. Les fichiers d'un récit

Pour un récit de nom de base `example` :

| Fichier                           | Rôle                                                                                                                                              | Obligatoire                            |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| `example.md` / `example.mdx`      | source Markdown, dans la langue par défaut                                                                                                        | un des deux : `.md` ou `.json` complet |
| `example.json`                    | **jumeau** du `.md` (`kind: "story"`, jamais fusionné), ou **compagnon** qui le complète par `id` (`kind: "sidecar"`). Règle unique : `AGENTS.md` | non                                    |
| `example.fr.md`, `example.de.md`… | la même page dans une autre langue (même structure, mêmes `id` de scène)                                                                          | non                                    |
| `example.fr.json`                 | compagnon propre à une langue : textes traduits, données localisées                                                                               | non                                    |

Règles de nommage :

- Le **suffixe de langue** est le dernier segment avant l'extension, au format BCP 47 : `fr`, `en`,
  `pt-BR`, `zh-Hans`. Il n'est reconnu **qu'en mode multilingue** (section `i18n` présente), et
  seulement s'il figure dans `i18n.locales`. Ainsi `hello.story.md` reste un nom de base
  `hello.story`, sans ambiguïté.
- Un fichier sans suffixe est dans la **langue par défaut** (`i18n.defaultLocale`, sinon `lang` du
  frontmatter). `example.md` et `example.en.md` ne peuvent pas coexister si `en` est la langue par
  défaut (erreur).
- Les **assets** (images, données CSV, cases de webtoon) sont partagés entre les langues. Une
  variante localisée suit la même règle : `cover.fr.png` remplace `cover.png` en français.
- **Un seul nom de base = un seul slug** : `example.fr.md` est publié en `/fr/<route>/example`, et
  non en `/example.fr`.

Exemples :

```
talks/
  launch.md            # deck, anglais (langue par défaut)
  launch.fr.md         # même deck, français
  launch.json          # compagnon partagé : données, props, liens
  launch.fr.json       # textes localisés restants (alt d'images, légendes de graphiques)
events/
  meetups.json         # calendrier complet en JSON, toutes langues dans le même fichier
roadmap/
  product.md           # roadmap, anglais
  product.json         # statuts, dates, propriétaires (sans traduction)
```

## 2. Le JSON typé

### 2.1 Un schéma par format, une seule source

- Les schémas sont écrits en **zod 4**, dans `@recit/core/schema` : `StorySourceSchema`, union
  discriminée sur `format`, avec `DeckSource`, `MotionSource`, `ReportSource`, `ToonSource`,
  `BookSource`, `RoadmapSource`, `StoriesSource`, `CalendarSource` et `DocsSource`.
- Les types TypeScript sont **inférés** (`z.infer`), pour ne jamais écrire deux fois la même
  structure.
- Les **JSON Schema** sont générés au build (`z.toJSONSchema`) et publiés par `sites/www` :
  - `https://recit.dev/schema/v1/story.json` (union) ;
  - `https://recit.dev/schema/v1/<format>.json` ;
  - `https://recit.dev/schema/v1/sidecar.json`.
- Chaque fichier JSON déclare son schéma, ce qui donne l'autocomplétion et la validation dans VS
  Code et les éditeurs :

```json
{
  "$schema": "https://recit.dev/schema/v1/deck.json",
  "kind": "story",
  "version": 1,
  "format": "deck"
}
```

- `version` est obligatoire. Toute rupture du schéma donne une `v2`, avec une migration
  `migrateStorySource(v1) → v2` et `recit migrate`.
- **Versionnement (règle B1, `06-roadmap.md`)** : un nouveau format ou un nouveau `kind` est un
  ajout compatible en `v1`. L'union reste ouverte en lecture : un lecteur `v1` qui rencontre un
  format ou un `kind` inconnu renvoie une erreur claire (« installez @recit/… »), pas une erreur de
  schéma. Seule une rupture donne une `v2`.
- **Formats ajoutés en `v1`, chacun dans sa phase** : `glossary` et `directory` (plan 12, P14, lot
  en P9), `atlas` (plan 13, P15), et `form` seulement si FO5 a lieu (plan 10). Chacun ajoute sa
  variante à `StorySourceSchema` et à `StoryFormat`.
- **`settings.pdf`** est typé par format dans sa phase (deck PDF0, report PDF1, toon PDF2, book
  PDF3, `15-export-pdf.md` §6). Les réglages sont **à plat**, sans clé `book:` ni `report:` dans le
  frontmatter (`08-formats.md`).

### 2.2 Forme commune

```ts
type Locale = string; // BCP 47, limité à i18n.locales
type Localized<T = string> = T | Partial<Record<Locale, T>>; // valeur simple, ou une valeur par langue

type StorySource = {
  $schema?: string;
  kind: "story";
  version: 1;
  format: StoryFormat; // deck | motion | docs | report | toon | book | roadmap | stories | calendar, puis glossary, directory, atlas (B1)
  meta: { title: Localized; description?: Localized; author?: string; lang?: Locale };
  settings?: FormatSettings; // section du format (deck, motion, report…) de recit.config.ts
  data?: Record<string, string | DataInline>; // sources de données (report, roadmap, calendar)
  scenes: SceneSource[];
};

type SceneSource = {
  id: string; // obligatoire en JSON (stable entre les langues)
  attrs?: Record<string, string | number | boolean>; // = attributs du séparateur `--- …`
  body?: Localized; // Markdown de la scène (prose, directives)
  notes?: Localized;
} & FormatSceneFields; // champs typés propres au format (voir 2.3)

type Sidecar = {
  $schema?: string;
  kind: "sidecar";
  version: 1;
  meta?: Partial<StorySource["meta"]>;
  data?: StorySource["data"];
  scenes?: Record<string /* id */, Partial<SceneSource>>; // fusion par id de scène
  strings?: Record<string, Localized>; // textes réutilisables : :t[key] dans le Markdown
};
```

- **`kind`** est le discriminant des documents JSON, et seulement lui : `"story"`, `"sidecar"`, puis
  `"bible"` et `"asset"` (P16, `14-ia-scenario.md` §8), ajoutés sous la règle B1. Aucun réglage ni
  attribut ne s'appelle `kind` : l'annuaire utilise `entries` (`12-glossaire-annuaire.md`), la carte
  `mode` (`13-cartes.md`).
- **`settings`** = la section du format dans `recit.config.ts`, pour tous les formats. La section du
  site qui publie les récits s'appelle donc `publish: { dir, routePrefix, decks }` (ex-`stories:`,
  tranché le 24/10, `02-architecture.md` §5) : `stories:` porte les réglages du format stories.
- **Blocs transverses** (tranché le 24/10, règle B2) : en `v1`, `:::form`, ` ```pixel `,
  `:::textcard`, `::map`, `::chart`, `::table` et `:::kpi` restent des **directives dans le `body`**
  de la scène. `SceneSource` ne gagne pas de champ `blocks` ; les schémas des blocs valident les
  directives.

### 2.3 Champs typés par format

| Format     | Une scène JSON contient                                                                                      | Exemple de champ localisable           |
| ---------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------- |
| `deck`     | `layout`, `cols`, `image`, `steps`                                                                           | `body`, `notes`, `image.alt`           |
| `motion`   | `type` (bloc), `props` du bloc emaki, `hold`, `dur`, `voiceover`                                             | `props.text`, `voiceover`              |
| `docs`     | `title`, `sidebar`                                                                                           | `body`, `title`                        |
| `report`   | aucun champ propre : `::chart`, `::table` et `:::kpi` sont des directives du `body` (B2)                     | `body`                                 |
| `toon`     | `src`, `alt`, `height`, `gutter`, `background`, `bubbles[] {x, y, tail, shape, text}`, `captions[]`, `sfx[]` | `bubbles[].text`, `captions[]`, `alt`  |
| `book`     | section assemblée d'un chapitre de `settings.chapters` : `body`, `epigraph`, `figures[]`                     | `body`, `figures[].caption`            |
| `roadmap`  | `lane`, `items[] {id, title, status, start, end, quarter, owner, link, tags}`                                | `items[].title`, `items[].description` |
| `stories`  | `background`, `duration`, `cta {label, href}`, `overlay`                                                     | `body`, `cta.label`                    |
| `calendar` | `events[] {id, title, start, end, allDay, rrule, timezone, location, url, tags}`                             | `events[].title`, `events[].location`  |

- **book** (P8) : les chapitres sont déclarés dans `settings.chapters` (avec `front` et `back`,
  `15-export-pdf.md` §3.3, `08-formats.md` §3) ; les scènes sont les sections assemblées des
  chapitres. En cas de doute, `settings.chapters` fait foi.
- **report** (P6) : pas de champs typés `charts[]`, `tables[]` ni `kpis[]` ; les blocs restent des
  directives du `body` (règle B2).
- **toon** (P7) : `alt` est localisable et obligatoire pour le PDF balisé (`15-export-pdf.md`
  PDF2.1) ; le fond s'appelle `background`, jamais `bg` (règle B4).
- **À ajouter dans la phase de chaque plan** : lignes `glossary` et `directory` (P14, plan 12 §8,
  avec l'exception de repli des termes), `atlas` (P15, plan 13), `Place` partagé (P10, règle B3), et
  la bible avec la règle `cast.fr.md` (P16, plan 14 §8).

Les champs **non textuels** (dates, nombres, statuts, coordonnées, URLs) ne sont **jamais**
localisables : un statut ou une date est le même dans toutes les langues. Le formatage se fait à
l'affichage avec `Intl`.

### 2.4 Équivalence Markdown ⇄ JSON

- `compileStory` accepte les deux entrées. Le Markdown est d'abord converti en `StorySource`
  (`markdownToSource`), puis tout suit le même chemin : `StorySource` → `Story` (compilé) → format.
- `recit convert example.md --to json` produit le JSON canonique (`kind: "story"`). `--to md` fait
  l'inverse. Le Markdown émis utilise la syntaxe canonique (`02-architecture.md` §3).
- **Propriété testée** pour chaque exemple et chaque fixture : `compile(md) == compile(toJson(md))`
  et `compile(json) == compile(toMd(json))`.
- **motion** : l'ancien `*.deck.json` d'emaki reste accepté en entrée (format machine, passe par le
  handover et `parseDeck`). `MotionSource` est son équivalent au niveau source, et
  `recit convert hello.deck.json --to md` permet la migration (`05-video.md` V5).

## 3. Fusion et résolution

Pour afficher `example` dans la langue `L` :

```
1. Source principale (la première trouvée) :
   example.L.md  →  example.<repli de L>.md  →  example.md  →  example.json (kind: story)
2. Compagnons, fusionnés dans cet ordre (le dernier gagne) :
   example.json (kind: sidecar)  →  example.L.json
3. Valeurs Localized : on prend L, puis la chaîne de repli, puis defaultLocale
4. Assets : cover.L.png  →  cover.png
```

- **Chaîne de repli** : `fr-CA` → `fr` → `defaultLocale`. Elle est configurable
  (`i18n.fallback: { "pt-BR": ["pt", "es"] }`).
- **Fusion par `id` de scène.** Le compagnon peut compléter (`attrs`, champs typés, `data`) ou
  remplacer (`body`). Un `id` du compagnon absent de la source produit une **erreur**
  (`example.fr.json: scene "pricing" does not exist in example.fr.md`).
- **Parité entre langues** : `recit i18n check` compare les variantes d'une page.
  - **Erreur** : scènes manquantes ou en trop, ordre différent, attributs structurels différents
    (`layout`, `type`, dates).
  - **Avertissement** : texte non traduit, identique à la langue par défaut.
- `:t[key]` dans le Markdown insère `strings.key` du compagnon, dans la langue courante. C'est utile
  pour des libellés répétés (CTA, mentions légales) sans dupliquer le fichier.

### Mode strict

`i18n.strict: true` (recommandé en CI) transforme les avertissements de traduction en erreurs. En
dev, une bannière « non traduit » s'affiche sur les scènes concernées (HUD du deck, overlay sur les
autres formats).

## 4. Configuration : une fonctionnalité optionnelle

**Le multilingue est optionnel.** Il s'active uniquement en ajoutant une section `i18n` au
`recit.config.ts`, le fichier de configuration unique qui remplace `recit.config.ts` (voir
`02-architecture.md` §5).

### Sans `i18n` (par défaut) : mode monolingue

- Aucun suffixe de langue n'est interprété : `example.fr.md` est un récit comme un autre, de nom de
  base `example.fr`.
- Aucune route préfixée, aucun sélecteur de langue, aucun `hreflang`, aucune vérification de parité.
- La langue de chaque page vient du frontmatter `lang:`, et à défaut c'est `en`. Elle sert seulement
  à `<html lang>`, à l'interface (packs `UI_STRINGS`), au sens d'écriture, aux formats de date et de
  nombre et à la césure.
- Les valeurs `Localized` d'un JSON restent permises : on prend celle de `lang`, sinon la première.

Un projet qui n'a qu'une langue n'a donc **rien à configurer ni à apprendre**.

### Avec `i18n` : mode multilingue

```ts
// recit.config.ts
export default defineConfig({
  i18n: {
    defaultLocale: "en",
    locales: ["en", "fr", "de", "ja", "ar", "pt-BR", "zh-Hans", "sw", "hi"], // n'importe quelle langue
    fallback: { "pt-BR": ["pt", "es"] }, // facultatif
    strict: false, // facultatif, true conseillé en CI
    routing: "prefix-except-default", // facultatif : /fr/…, la langue par défaut sans préfixe | "prefix-always"
  },
});
```

- **Toutes les langues du monde** sont acceptées : n'importe quelle étiquette BCP 47 valide (langue,
  écriture, région), vérifiée et normalisée avec `Intl.getCanonicalLocales`. Il n'y a pas de liste
  fermée dans Récit. Ajouter une langue, c'est ajouter une entrée à `locales`.
- Seul `locales` est obligatoire dans la section. `defaultLocale` vaut par défaut la première langue
  de la liste.
- **Interface (boutons, lecteurs, console présentateur)** :
  - Récit livre les packs `en` et `fr`. Ce sont les deux langues de `UI_STRINGS` aujourd'hui ;
  - pour une autre langue, l'interface se replie sur `en`, avec un avertissement en dev ;
  - on peut fournir un pack dans la config (`i18n.ui: { ja: "./i18n/ja.json" }`). Les packs de la
    communauté sont publiés dans `@recit/core/i18n/packs` au fil des contributions ;
  - les dates, nombres, pluriels, noms de langues et de mois viennent d'`Intl`, et fonctionnent donc
    pour toutes les langues sans pack.
- Le frontmatter `lang:` d'un fichier sans suffixe reste accepté. S'il contredit le suffixe, c'est
  une erreur.
- Passer d'un projet monolingue à un projet multilingue ne casse rien. Les fichiers existants
  deviennent ceux de la langue par défaut, et on ajoute des `*.fr.md` au fil de l'eau.

## 5. Ce que la langue change dans chaque format

| Domaine                                                  | Règle                                                                                                                                                                                                                                                                              |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Interface (boutons, console présentateur, lecteurs, HUD) | `UI_STRINGS` existe déjà en fr/en dans `@presentation/components/i18n.ts`. Il devient `@recit/core/i18n` avec des packs par langue (`en`, `fr` d'abord), et chaque format y ajoute ses clés. Si une langue manque, on se replie sur `en`                                           |
| Direction d'écriture                                     | `dir` est déduit de la langue (`ar`, `he`, `fa` → `rtl`) et posé sur la racine de chaque scène. Les layouts utilisent des propriétés logiques (`margin-inline-start`…). `split`, `image-left` et `image-right` s'inversent en RTL, sauf `mirror=false`                             |
| Dates et heures                                          | `Intl.DateTimeFormat` (calendar, roadmap, report), fuseau `timezone` explicite pour les événements                                                                                                                                                                                 |
| Nombres, devises, pourcentages                           | `Intl.NumberFormat` (`:value`, axes des graphiques)                                                                                                                                                                                                                                |
| Typographie                                              | césure (`hyphens: auto` + `lang`), espaces insécables françaises, guillemets selon la langue (`« »`, `“ ”`, `„ “`), veuves et orphelines (book, report)                                                                                                                            |
| Polices                                                  | lint de couverture des glyphes (il existe déjà dans emaki) étendu à tous les formats : une police du thème qui ne couvre pas le japonais ou l'arabe produit une erreur, ou un repli déclaré dans le thème                                                                          |
| motion                                                   | durées dépendantes de la langue : `wordsPerSecond` par langue (`motion.wordsPerSecond: { en: 2.6, fr: 2.4, ja: 7 }`), voix off (`voiceover`) et sous-titres par langue, un MP4 par langue (`recit render --locale fr`, ou `--all-locales`)                                         |
| toon                                                     | lettrage généré, donc traduisible sans retoucher les images. Les bulles s'agrandissent automatiquement, avec un avertissement si le texte déborde. Sens de lecture inversé possible pour les mangas (`toon.direction: rtl`)                                                        |
| book                                                     | un EPUB par langue ; un PDF par langue pour tous les formats de la liste fermée de `15-export-pdf.md` §1. `dc:language` dans l'OPF, table des matières et index traduits                                                                                                           |
| report                                                   | libellés des graphiques et des tableaux localisés. Les données restent les mêmes                                                                                                                                                                                                   |
| roadmap, calendar                                        | titres et descriptions localisés. Statuts, dates et récurrences communs                                                                                                                                                                                                            |
| stories                                                  | une story par langue, CTA localisés                                                                                                                                                                                                                                                |
| PDF                                                      | `--locale` et `--all-locales` pilotent aussi `recit export --to pdf` : `--all-locales` produit un PDF par locale (`{slug}.{locale}.pdf` par défaut), chacun une entrée de `exports.json` (`15-export-pdf.md` §3.8). Le lint de couverture des glyphes devient bloquant pour un PDF |
| Accessibilité                                            | `lang` sur `<html>` et sur chaque passage d'une autre langue (`:lang[mot]{l=de}`), textes alternatifs localisés obligatoires en mode strict                                                                                                                                        |

## 6. Sites et intégration du framework de sites

**le framework de sites n'a aucun support multilingue** (aucune notion de `locale`, `i18n` ou
`hreflang` dans le code du cœur 0.93). C'est donc à Récit de le fournir, sans patcher le cœur si
possible :

- `@recit/site-stories` génère les routes par langue (`/fr/present/<slug>`…) à partir des variantes
  de fichiers, et passe la langue au viewer.
- `@recit/site-kit` fournit :
  - le sélecteur de langue dans l'en-tête, qui garde la page courante si elle existe dans l'autre
    langue, et renvoie sinon vers l'accueil de cette langue ;
  - les balises `<link rel="alternate" hreflang>` et `x-default` ;
  - la balise `<html lang dir>` ;
  - un `sitemap.xml` par langue.
- **Recherche** : Pagefind indexe séparément chaque langue d'après `<html lang>`. On le vérifie dans
  les e2e.
- **Pages de documentation (MDX du framework de sites)** : `docs.recit.dev` reste **en anglais**
  (règle du projet). Si l'on veut un jour une doc traduite, il faudra un plugin de pages `*.fr.mdx`,
  ou un changement du framework de sites (`packages/site`).
- Les **exemples** des sites de format sont publiés en **anglais et en français** dès leur sortie :
  c'est la démonstration de la fonctionnalité.

## 7. Outillage

| Commande ou outil                          | Rôle                                                                                                                                 |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| `recit convert x.md --to json` / `--to md` | conversion canonique                                                                                                                 |
| `recit validate x.json`                    | validation zod, avec erreurs localisées (chemin JSON, et ligne quand on part du Markdown)                                            |
| `recit i18n status`                        | tableau de couverture par page et par langue (scènes traduites, chaînes manquantes)                                                  |
| `recit i18n extract x.md --locale de`      | crée `x.de.json` (compagnon) avec toutes les chaînes à traduire, en reprenant le texte source                                        |
| `recit i18n check`                         | parité entre langues (§3), à lancer en CI                                                                                            |
| `recit migrate`                            | migration des JSON vers la dernière version du schéma                                                                                |
| MCP `describe_format(format)`              | renvoie le JSON Schema du format et la spec Markdown                                                                                 |
| MCP `translate_story(path, locale)`        | propose la variante dans la langue cible (`.L.md` ou `.L.json`). L'auteur valide, et aucune traduction automatique n'a lieu au build |
| Éditeur                                    | `$schema` donne l'autocomplétion. Plus tard, une extension VS Code (aperçu, sélecteur de langue)                                     |

## 8. Packages concernés

| Package               | Ajout                                                                                                                                                                                                                                                                                                                                                      |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@recit/core`         | `./schema` (schémas zod, types, JSON Schema), `./i18n` (résolution de langue, repli, `Localized`, packs d'interface), `markdownToSource`, `sourceToMarkdown`, fusion des compagnons, `recit i18n check` (logique) ; `ExportManifestSchema` (`exports.json`) et `ExportOriginSchema`, publiés en JSON Schema, sous-chemin `./print` (P6) ; `./jsonld` (P10) |
| `@recit/cli`          | `convert`, `validate`, `migrate`, `i18n status \| extract \| check`, `--locale` et `--all-locales` sur `build`, `export` et `render` (liste complète : `03-packaging.md` §6)                                                                                                                                                                               |
| `@recit/components`   | `./terms` : plugin `:term` et liens automatiques (P14, plan 12 ; le cœur ne garde que le nom, règle B4)                                                                                                                                                                                                                                                    |
| `@recit/site-stories` | routes par langue, `StoryEmbed locale`, `hreflang`                                                                                                                                                                                                                                                                                                         |
| `@recit/site-kit`     | sélecteur de langue, sitemaps, alternates                                                                                                                                                                                                                                                                                                                  |
| `@recit/mcp`          | `describe_format`, `translate_story`                                                                                                                                                                                                                                                                                                                       |
| chaque format         | ses champs typés (§2.3) et ses règles de langue (§5)                                                                                                                                                                                                                                                                                                       |
| `sites/www`           | publie `recit.dev/schema/v1/*.json`                                                                                                                                                                                                                                                                                                                        |

## 9. Phases

Le socle (fichiers, JSON, résolution) arrive **avec le modèle `Story`, en P3**, pour que chaque
format soit multilingue dès sa sortie.

- [x] L1 (P3) — `@recit/core/schema` : `StorySource`, `Sidecar`, `Localized`, schémas deck et docs.
      JSON Schema générés. `markdownToSource` et `sourceToMarkdown`, avec le test d'équivalence sur
      tous les exemples.
- [x] L2 (P3) — Résolution des fichiers (`example.L.md`, compagnons, assets localisés), chaîne de
      repli, `:t[key]`, `i18n.*` dans `recit.config.ts`.
- [x] L3 (P3) — `@recit/core/i18n` : migration de `UI_STRINGS` (fr/en), `dir`, propriétés CSS
      logiques dans `@recit/theme`. Restent : packs d'interface par langue, RTL complet
      (`journal.md` §2).
- [x] L4 (P3) — CLI : `convert`, `validate`, `i18n status | extract | check`, `--locale`.
- [x] L5 (P3) — `@recit/site-stories` et `@recit/site-kit` : routes par langue, sélecteur,
      `hreflang`, sitemaps, Pagefind par langue. `examples/deck-showcase` en `en` et `fr`. Restent :
      sélecteur de langue plein écran, bandeau « non traduit » (`journal.md` §2).
- [ ] L6 (P4 et suivantes) — Chaque nouveau format livre son schéma JSON, ses champs localisables et
      ses règles de langue (§5), avec un exemple bilingue.
- [ ] L7 (P4) — motion : `wordsPerSecond` par langue, voix off et sous-titres,
      `render --all-locales`.
- [ ] L8 (P5) — Publication des JSON Schema sur recit.dev, MCP `describe_format` et
      `translate_story`.

**Critère de sortie de L1 à L5 :**

- `examples/deck-showcase` existe en `.md`, en `.json` et en `.fr.md`, avec une compilation
  identique en `md` et en `json` ;
- `recit i18n check` passe ;
- le site sert `/present/showcase` et `/fr/present/showcase` avec `hreflang`, le sélecteur de langue
  et une recherche par langue.

## 10. Risques

| Risque                                                                       | Parade                                                                                                                                                                                                                          |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Deux sources de vérité (`.md` et `.json`) qui divergent                      | un `kind: "sidecar"` complète par `id` et ne redéfinit jamais la liste des scènes (un `id` inconnu est une erreur) ; un `kind: "story"` posé à côté d'un `.md` est un **jumeau**, jamais fusionné, et l'aller-retour le vérifie |
| Des variantes de langue qui dérivent (scènes ajoutées dans une seule langue) | `recit i18n check` en CI (parité stricte des `id` et des attributs structurels)                                                                                                                                                 |
| Une ambiguïté de nom (`hello.story.md`)                                      | seules les langues déclarées dans `i18n.locales` sont reconnues comme suffixe                                                                                                                                                   |
| Des schémas JSON qui cassent les utilisateurs                                | `version` obligatoire, schémas publiés par version (`/schema/v1/`), `recit migrate`                                                                                                                                             |
| Le framework de sites sans i18n                                              | tout passe par `@recit/site-stories` et `@recit/site-kit`. La doc du framework de sites reste en anglais                                                                                                                        |
| Des polices qui ne couvrent pas une langue                                   | lint de couverture des glyphes, avec erreur en mode strict                                                                                                                                                                      |
