Aller au contenu
Cragdoo Blog

Contenu bilingue avec une i18n de premier ordre

Comment fonctionne le routage anglais-à-la-racine + français-sous-/fr, comment apparier les articles traduits avec translationKey, et comment ajouter une troisième locale.

Tutoriels 4 min de lecture

Ce site est bilingue. L’anglais est servi à la racine de l’URL, et le français vit sous /fr. Le sélecteur de langue dans la barre de navigation supérieure est contextuel : sur une page d’article, il vous emmène sur l’article traduit équivalent ; sur une page de listing, il échange le préfixe de locale en préservant le reste du chemin.

Vous lisez cet article en français. Cliquez sur le sélecteur de langue en haut de la page pour le lire en anglais — ce trajet fonctionne parce que les deux fichiers partagent la même translationKey.

Si vous ne publiez qu’en une seule langue, définissez multilingual: false dans src/config.ts. Le sélecteur de langue disparaît de la barre supérieure et les balises <link rel="alternate" hreflang> ne sont plus émises. Pour retirer également les routes de l’autre locale du build, supprimez ses dossiers de contenu (src/content/posts/<lang>/, src/content/pages/<lang>/), son miroir src/pages/<lang>/, et retirez-la de SITE.locales.

Vous pouvez laisser multilingual: true et ne traduire que les articles de votre choix. Le thème inspecte chaque page d’article au build et détecte les locales qui possèdent réellement une traduction :

  • Le sélecteur de langue ne propose que les locales dont le pendant existe. Si aucune autre locale n’a de traduction de l’article courant, le sélecteur est entièrement masqué pour cette page — les visiteurs ne tombent jamais sur une 404.
  • <link rel="alternate" hreflang="..."> (et le x-default correspondant) n’est émis que pour les locales qui possèdent bien l’article, afin que les moteurs de recherche ne voient pas d’alternates cassées.
  • Les pages de listing, de tags, de catégories, les archives, la recherche et la page « À propos » existent dans toutes les locales configurées ; le sélecteur reste donc visible sur ces routes et se contente de changer le préfixe d’URL.

Appariez les articles traduits en leur donnant la même translationKey dans le frontmatter — voir Apparier les traductions avec translationKey plus bas.

LocaleRacineArticlesTags
en//posts/<slug>/tags/...
fr/fr//fr/posts/<slug>/fr/tags/...

La locale par défaut (anglais) n’a jamais de préfixe. C’est appliqué à trois endroits qui s’accordent :

  1. astro.config.mjsi18n.routing.prefixDefaultLocale: false
  2. src/config.tsdefaultLocale: 'en'
  3. src/i18n/utils.tslocalePrefix() retourne '' pour la locale par défaut.

Les articles vivent dans des dossiers par locale :

src/content/posts/
├── en/
│ ├── welcome.md
│ ├── i18n-bilingual-content.md
│ └── ...
└── fr/
├── welcome.md
├── i18n-bilingual-content.md
└── ...

Le champ lang est inféré depuis le chemin du fichier — vous n’avez pas à le définir dans le frontmatter. Override uniquement dans des cas inhabituels.

Deux articles qui partagent la même translationKey sont considérés comme traductions l’un de l’autre. Le sélecteur de langue s’en sert pour atterrir sur l’article équivalent au lieu de rebondir vers la page d’accueil de la locale.

src/content/posts/en/welcome.md
translationKey: welcome
src/content/posts/fr/welcome.md
translationKey: welcome

Si la traduction correspondante n’existe pas pour la page active (article anglais sans version française), le sélecteur retombe sur la page d’accueil de la locale (/ ou /fr/) au lieu de servir une 404 au lecteur.

Le fallback de slug fonctionne aussi : si vous omettez translationKey, le thème utilise le slug du fichier. Donc des slugs identiques entre en/ et fr/ sont auto-appariés sans métadonnée supplémentaire.

Chaque libellé d’interface — navigation, boutons, messages d’erreur, labels d’accessibilité — vit dans src/i18n/ui.ts, indexé par locale. TypeScript garantit que toutes les clés sont présentes dans toutes les locales : en oublier une fait échouer le build.

src/i18n/utils.ts
const t = useTranslations('fr');
t('nav.home'); // 'Accueil'
formatDate(d, 'fr'); // '24 avril 2026'

Le helper formatDate utilise Intl.DateTimeFormat pour que dates, nombres et chaînes de temps de lecture s’affichent dans la locale active.

  • Un flux RSS distinct est généré par locale : /rss.xml (EN) et /fr/rss.xml (FR). Le lien est dans la rangée des liens sociaux de la barre latérale.
  • Le sitemap (intégration @astrojs/sitemap) émet des alternates hreflang pour chaque page traduite. Les moteurs de recherche s’en servent pour servir la bonne langue au bon lecteur.
  • Les balises <link rel="alternate" hreflang="..."> sont aussi injectées dans le <head> par SEO.astro.

Le code-base est conçu pour que ce soit une tâche en cinq étapes. Pour ajouter de :

  1. Ajouter 'de' à SITE.locales dans src/config.ts.
  2. Ajouter un bloc de dans src/i18n/ui.ts — TypeScript se plaindra tant que toutes les clés ne sont pas présentes, c’est voulu.
  3. Mettre à jour les switches conscients de la locale dans src/i18n/utils.tshtmlLang, localeLabel, formatDate. Chacun a un fallback, donc le site continue de compiler avant que vous n’ayez tout rempli.
  4. Refléter les dossiers de routes sous src/pages/de/... (copier depuis src/pages/fr/... — chaque fichier appelle essentiellement getPosts('de')).
  5. Ajouter des articles sous src/content/posts/de/... et des pages sous src/content/pages/de/.... Définissez translationKey pour les apparier avec leurs frères EN/FR.

C’est tout le cérémonial. Le sitemap, le RSS, la recherche, les widgets de la barre latérale et le sélecteur de langue captent la nouvelle locale automatiquement.

Pagefind respecte l’attribut lang sur <html>, que le thème définit par page depuis la locale inférée. La modale de recherche filtre les résultats à la locale courante, donc les lecteurs français n’ont pas accidentellement de résultats anglais.

SymptômeSolution
Le sélecteur tombe toujours sur /La traduction de la page active n’est pas encore écrite.
Format de date erronéMettez à jour le switch formatDate dans src/i18n/utils.ts.
Le build se plaint d’une clé manquanteAjoutez la clé à chaque bloc de locale dans ui.ts.
Article FR avec UI anglaiseLe chemin doit être posts/fr/... (pas posts/fr-FR/...).