Chapitre 11Construire
Multilingue
Une URL par langue, construite au build. Un repli qui rend la page utilisable pendant qu'on la traduit, et un contrôle qui refuse de laisser passer le travail restant.
Une URL par langue
/ → redirection émise au build vers /fr/
/fr/ page d'accueil française
/fr/services/
/en/ English home page
/en/services/
Le préfixe est présent même pour la langue de référence
(prefixDefaultLocale: true). Sans lui, la même page serait servie sous deux
adresses — la racine et le préfixe — et il faudrait arbitrer une canonique entre deux URL
identiques.
Ni sur le site, ni dans l'overlay, où le choix de langue est fait de vrais liens. Une bascule par script produirait une seule URL pour deux contenus : invisible pour les moteurs, impossible à partager.
Les fichiers
src/content/pages/
├── fr/ la langue de référence — c'est elle qui fait la liste des pages
│ ├── home.json
│ └── services.json
└── en/ la traduction
├── home.json
└── services.json
La structure de clés doit être identique d'une langue à l'autre. Ce n'est pas
une convention : c'est vérifié, dans les deux sens, par check-locales.mjs.
Quand une traduction manque
Deux mécanismes, qui semblent se contredire et se complètent :
À l'affichage
La page reste consultable
Un champ absent de la traduction est repris de la langue de référence plutôt que de
laisser un trou. Le champ est marqué (data-cms-untranslated), et la barre de
l'overlay affiche « 2 textes restent à traduire sur cette page ».
Au contrôle
Rien ne part en silence
npm run check échoue sur toute clé présente dans une langue et absente d'une
autre — dans les deux sens, y compris une clé traduite qui n'existe pas
dans la référence et ne serait donc jamais affichée.
Le repli n'est donc pas une tolérance : c'est ce qui rend une page utilisable pendant qu'on la traduit, sans masquer le travail restant.
export { localePath, mergeWithDefault, type MergeResult } from 'inline-core/translate';
export const LOCALES = ['fr', 'en'] as const;
export const DEFAULT_LOCALE = 'fr';
/** Nom de la langue dans la langue elle-même — jamais un code à l'écran. */
export const LOCALE_LABELS = {
fr: 'Français',
en: 'English',
};La mécanique du repli vit dans le paquet ; la liste des langues vit dans le site. Un paquet partagé ne peut pas savoir combien de langues a un site donné, ni comment elles s'appellent.
Les listes et les traductions
Les items se réconcilient par identifiant, jamais par position. Une traduction peut donc ranger ses témoignages dans un autre ordre sans que les textes ne glissent d'un item à l'autre.
Un item ajouté dans une langue doit recevoir le même identifiant dans les autres. C'est ce que fait l'overlay quand le client ajoute un témoignage puis passe à l'autre langue ; c'est à faire à la main quand on écrit le JSON directement.
Ajouter une langue
Quatre endroits, et il faut les quatre :
-
L'intégration et le routage i18n
astro.config.mjs js integrations: [ inline({ locales: ['fr', 'en', 'de'] }), // la référence en premier ], i18n: { locales: ['fr', 'en', 'de'], defaultLocale: 'fr', routing: { prefixDefaultLocale: true }, },Le code doit faire deux lettres minuscules : l'intégration refuse le reste.
-
Les langues du site
src/lib/locales.ts ts export const LOCALES = ['fr', 'en', 'de'] as const; export const LOCALE_LABELS = { fr: 'Français', en: 'English', de: 'Deutsch', };Le libellé est le nom de la langue dans cette langue : le client voit « Deutsch », jamais « de ».
-
Le contrôle de parité
scripts/check-locales.mjs js const LOCALES = ['fr', 'en', 'de']; -
Le contrôle du HTML
scripts/check-html.mjsliste explicitement les pages construites, une entrée par page et par langue. C'est l'oubli le plus fréquent : le contrôle passe, mais il ne vérifie rien pour la nouvelle langue.scripts/check-html.mjs js const PAGES = [ { content: 'src/content/pages/fr/home.json', html: 'dist/fr/index.html' }, { content: 'src/content/pages/en/home.json', html: 'dist/en/index.html' }, { content: 'src/content/pages/de/home.json', html: 'dist/de/index.html' }, ];
Puis créer src/content/pages/de/. Tant que les fichiers manquent, les pages
s'affichent dans la langue de référence et npm run check le signale.
Les routes reçoivent les langues du site (createSaveRoute({ locales: LOCALES }))
et n'acceptent d'écrire que dans un dossier de langue déclaré. Sans cela,
src/content/pages/zz/home.json créerait un dossier que rien ne construit, et le
dépôt se remplirait de contenus invisibles.
Les liens réciproques
Chaque page se déclare elle-même en plus de ses traductions : c'est la réciprocité qu'attendent les moteurs, une page qui ne se cite pas est ignorée du groupe.
<link rel="canonical" href="https://le-site.fr/fr/services/" />
<link rel="alternate" hreflang="fr" href="https://le-site.fr/fr/services/" />
<link rel="alternate" hreflang="en" href="https://le-site.fr/en/services/" />
<link rel="alternate" hreflang="x-default" href="https://le-site.fr/fr/services/" />
x-default pointe vers la langue de référence. Tout cela est produit par le layout
à partir de la liste des langues — il n'y a rien à tenir à jour à la main.
Éditer dans une autre langue
Dans l'overlay, le sélecteur de langue est un jeu de vrais liens vers la même
page dans les autres langues, lus sur data-cms-locales, posé au build. Le client
change de langue comme un visiteur, et son brouillon local reste attaché à la page qu'il
éditait.
La barre affiche le nombre de champs encore repris de la langue de référence sur la page courante. C'est le seul indicateur de traduction de l'interface — il n'y a pas de tableau d'avancement, pas de vue d'ensemble, pas d'arborescence.
Un site monolingue
Déclarez une seule langue partout. Rien d'autre ne change : le repli n'a pas d'objet, le contrôle de parité passe trivialement, le sélecteur de langue ne s'affiche pas. Le passage à deux langues plus tard ne demande que les quatre modifications ci-dessus.
Vérifications
npm run checkpasse : les clés sont identiques dans toutes les langues.- Chaque page existe dans la langue de référence — sinon elle n'est pas construite du tout.
- Les
hreflangsont réciproques et incluentx-default. - Les identifiants d'items de liste sont les mêmes d'une langue à l'autre.
check-html.mjsa une entrée par page et par langue.- Les libellés de langue sont écrits dans leur propre langue.