<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Un blog en 7 langues avec des URL traduites : comment ce site est construit

Ce blog est né en italien. Quand j'ai ajouté les six autres langues — anglais, albanais, portugais, espagnol, français et allemand —, traduire les textes a été la partie facile. La partie difficile, c'était tout le reste : les URL, les liens déjà partagés, les moteurs de recherche et une traduction automatique qui semblait fonctionner mais abîmait le contenu en silence.

Le problème : une URL italienne pour sept langues

Au début, chaque article n'avait qu'un seul slug, l'italien. Résultat : des adresses comme /de/blog/perche-il-blog-non-viene-indicizzato, une page en allemand avec une URL en italien. Peu agréable à partager, peu claire pour le lecteur et un gâchis pour le SEO, car les mots de l'URL comptent.

Le modèle de données : un article, sept langues

J'ai choisi de garder un seul document par article, avec des champs par langue : title_en, content_de, slug_fr, etc. L'alternative était un document par traduction, mais il aurait fallu synchroniser tags, vues et statut de publication entre sept copies. Avec un seul document, l'article reste une seule entité.

Des slugs traduits qui ne changent jamais

Chaque slug par langue est généré une seule fois, à partir du titre traduit, puis n'est plus jamais modifié, même si je corrige le titre : une URL déjà partagée doit continuer à fonctionner. De plus, chaque slug doit être unique parmi tous les champs de tous les articles, pour qu'une adresse mène toujours à un seul article, quelle que soit sa langue :

// Fill slug_xx from the translated title, only where it's missing.
// Existing slugs are never touched: they're in URLs people have shared.
for (const lang of ['en', 'sq', 'pt', 'es', 'fr', 'de']) {
  if (post[`slug_${lang}`]) continue;
  const title = post[`title_${lang}`];
  if (title?.trim()) {
    post[`slug_${lang}`] = await ensureUniqueSlug(title); // unique across ALL slug fields
  }
}

Les anciens liens fonctionnent toujours

Avant les slugs traduits, beaucoup de liens avaient déjà été partagés sous la forme /sq/blog/slug-italien. Les casser aurait signifié des pages 404 et des signaux négatifs pour Google. C'est pourquoi le build génère aussi chaque article avec le slug italien sous chaque préfixe de langue, avec un canonical pointant vers la version traduite : qui arrive par l'ancien lien voit la bonne page, et Google sait quelle adresse est l'officielle.

Du SEO pour chaque langue

  • hreflang sur chaque page, pour relier entre elles les sept versions du même article.
  • og:locale dynamique, pour que les aperçus sur les réseaux sociaux affichent la bonne langue, le bon titre et la bonne description.
  • Un sitemap avec toutes les versions, mis à jour automatiquement à chaque publication.
  • Des pages pré-rendues par langue : sans elles, Google ne voyait que la coquille de l'application, comme je l'ai raconté dans cet article.

La traduction automatique qui cassait le code

Pour traduire sans toucher au HTML, je remplaçais balises et blocs de code par des marqueurs, j'envoyais le texte au traducteur puis je remettais tout en place. Cela fonctionnait presque toujours. Mais quand plusieurs marqueurs étaient proches — typique des articles avec beaucoup de blocs de code —, le traducteur en dupliquait parfois un et en perdait un autre. Ce n'était pas aléatoire : en relançant, l'erreur se reproduisait à l'identique, surtout en albanais.

La solution a été de ne plus envoyer du tout les marqueurs au traducteur : je découpe le texte sur les marqueurs et je ne traduis que les morceaux de prose. Plus d'appels, mais aucune chance qu'un marqueur soit altéré :

// Split on the placeholders: the translator never sees them
const parts = text.split(/(✦\d+✦)/);
const out = [];
for (const part of parts) {
  out.push(/^✦\d+✦$/.test(part) ? part : await translate(part, lang));
}
return out.join('');

Depuis, après chaque traduction, je compare la structure de chaque langue à celle de l'italien — même nombre de titres, de paragraphes, de tableaux et de blocs de code — sans me fier au « aucune erreur » renvoyé par le script.


En résumé

Un blog multilingue n'est pas un blog auquel on a greffé un traducteur : il faut un modèle de données clair, des URL stables pour chaque langue, des anciens liens qui ne cassent pas et des contrôles automatiques de la traduction. Ce sont les mêmes choix que je propose quand un client veut un site en plusieurs langues. Si c'est votre cas, écrivez-moi.

💬 Notes des lecteurs

0 notes

Écrire une note

Partagez votre avis, une suggestion ou un compliment

Dernières notes

Aucune note pour le moment. Soyez le premier à commenter !