FR EN

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.

5 min de lecture9 sectionsChapitre 11 / 22

Une URL par langue

text text
/            →  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.

Aucune bascule par JavaScript

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

text text
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.

src/lib/locales.ts ts
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.

Le corollaire

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 :

  1. 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.

  2. 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 ».

  3. Le contrôle de parité

    scripts/check-locales.mjs js
    const LOCALES = ['fr', 'en', 'de'];
  4. Le contrôle du HTML

    scripts/check-html.mjs liste 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.

Côté serveur

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.

dans le <head> html
<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 check passe : 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 hreflang sont réciproques et incluent x-default.
  • Les identifiants d'items de liste sont les mêmes d'une langue à l'autre.
  • check-html.mjs a une entrée par page et par langue.
  • Les libellés de langue sont écrits dans leur propre langue.