Internationalisation (i18n)

Sur cette page

EmDash s’intègre avec le routage i18n intégré d’Astro pour fournir une gestion de contenu multilingue. Astro gère le routage des URL et la détection de locale ; EmDash gère le stockage et la récupération du contenu traduit.

Chaque traduction est une entrée de contenu complète et indépendante avec son propre slug, statut et historique de révisions. La version française d’un article peut être en brouillon pendant que la version anglaise est publiée.

Configuration

Activez i18n en ajoutant un bloc i18n à votre configuration Astro. EmDash lit cette même configuration pour sa liste de locales, locale par défaut et chaîne de fallback.

import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";

export default defineConfig({
	i18n: {
		defaultLocale: "en",
		locales: ["en", "fr", "es"],
		fallback: { fr: "en", es: "en" },
	},
	integrations: [
		emdash({
			database: sqlite({ url: "file:./data.db" }),
			storage: local({
				directory: "./uploads",
				baseUrl: "/_emdash/api/media/file",
			}),
		}),
	],
});

Lorsque i18n n’est pas présent dans la configuration Astro, toutes les fonctionnalités i18n sont désactivées et EmDash se comporte comme un CMS monolingue.

Comment fonctionnent les traductions

EmDash utilise un modèle ligne-par-locale. Chaque traduction est sa propre ligne dans la base de données avec son propre ID, slug et statut, liée à d’autres traductions via un identifiant translation_group partagé. Une table de posts avec trois traductions ressemble à ceci :

ec_posts:
id       | slug        | locale | translation_group | status
---------|-------------|--------|-------------------|----------
01ABC... | my-post     | en     | 01ABC...          | published
01DEF... | mon-article | fr     | 01ABC...          | draft
01GHI... | mi-entrada  | es     | 01ABC...          | published

Cette conception signifie :

  • Slugs par locale/blog/my-post et /fr/blog/mon-article fonctionnent naturellement
  • Publication par locale — publiez la version anglaise tout en gardant le français en brouillon
  • Révisions par locale — chaque traduction a son propre historique de révisions
  • Requêtes monolocales — les requêtes de liste retournent des entrées pour une seule locale

Interroger du contenu traduit

Entrée unique

Passez locale à getEmDashEntry pour récupérer une traduction spécifique. Lorsqu’omis, il utilise par défaut la locale actuelle de la requête (définie par le middleware i18n d’Astro).

---
import { getEmDashEntry } from "emdash";

const { slug } = Astro.params;
const { entry: post, error } = await getEmDashEntry("posts", slug, {
  locale: Astro.currentLocale,
});

if (!post) return Astro.redirect("/404");
---

<article>
  <h1>{post.data.title}</h1>
</article>

Chaîne de fallback

Lorsqu’aucun contenu n’existe pour la locale demandée, EmDash suit la chaîne de fallback définie dans votre configuration Astro. Avec fallback: { fr: "en" } :

  1. Essaie la locale demandée (fr)
  2. Essaie la locale de fallback (en)
  3. Essaie la locale par défaut

Le fallback s’applique uniquement aux requêtes d’entrée unique. Les requêtes de liste retournent uniquement les entrées pour la locale demandée.

Les menus sont par locale — le même name (ex. "primary") peut exister dans plusieurs locales, tous liés via un translation_group partagé. Les éléments de menu résolvent leurs références de contenu par rapport à la version de la locale active du contenu référencé.

Le composant suivant récupère le menu principal pour la locale active :

---
import { getMenu } from "emdash";

const menu = await getMenu("primary", { locale: Astro.currentLocale });
---

<nav aria-label="Primary">
  <ul>
    {menu?.items.map((item) => (
      <li><a href={item.url}>{item.label}</a></li>
    ))}
  </ul>
</nav>

Créez des traductions d’un menu existant depuis la liste Menus de l’admin — les éléments sont clonés avec reference_id intact (il stocke le translation_group du contenu référencé), de sorte que les liens du nouveau menu pointent automatiquement vers le bon contenu par locale.

Taxonomies (catégories, tags)

Les termes sont par locale. Les définitions (_emdash_taxonomy_defs) sont également par locale, donc label / labelSingular peuvent aussi être traduits. Le pivot content_taxonomies.taxonomy_id stocke le translation_group du terme, de sorte qu’une seule affectation couvre chaque locale du contenu.

L’exemple suivant récupère les catégories et les termes d’un article pour la locale active :

---
import { getTaxonomyTerms, getEntryTerms } from "emdash";

const categories = await getTaxonomyTerms("category", {
  locale: Astro.currentLocale,
});
const terms = await getEntryTerms("posts", post.id, undefined, {
  locale: Astro.currentLocale,
});
---

Traduire un contenu hérite automatiquement des affectations de termes de la source — vous n’avez besoin de traduire que les termes eux-mêmes une fois, et chaque article qui les utilise sera résolu à la bonne locale au moment de la lecture.

Listage de collection

Filtrez une collection par locale :

---
import { getEmDashCollection } from "emdash";

const { entries: posts } = await getEmDashCollection("posts", {
  locale: Astro.currentLocale,
  status: "published",
});
---

<ul>
  {posts.map((post) => (
    <li><a href={`/${post.data.slug}`}>{post.data.title}</a></li>
  ))}
</ul>

Sélecteur de langue

Utilisez getTranslations pour construire un sélecteur de langue qui lie aux traductions existantes de l’entrée actuelle :

---
import { getTranslations } from "emdash";
import { getRelativeLocaleUrl } from "astro:i18n";

interface Props {
  collection: string;
  entryId: string;
}

const { collection, entryId } = Astro.props;
const { translations } = await getTranslations(collection, entryId);
---

<nav aria-label="Language">
  <ul>
    {translations.map((t) => (
      <li>
        <a
          href={getRelativeLocaleUrl(t.locale, `/blog/${t.slug}`)}
          aria-current={t.locale === Astro.currentLocale ? "page" : undefined}
        >
          {t.locale.toUpperCase()}
        </a>
      </li>
    ))}
  </ul>
</nav>

La fonction getTranslations retourne toutes les variantes de locale dans le même groupe de traduction :

const { translationGroup, translations } = await getTranslations("posts", post.entry.id);
// translations: [
//   { locale: "en", id: "01ABC...", slug: "my-post", status: "published" },
//   { locale: "fr", id: "01DEF...", slug: "mon-article", status: "draft" },
// ]

Gestion des traductions dans l’Admin

Liste de contenu

Lorsque i18n est activé, la liste de contenu affiche :

  • Une colonne de locale affichant la locale de chaque entrée
  • Un filtre de locale dans la barre d’outils pour basculer entre les locales

Création de traductions

Ouvrez n’importe quelle entrée de contenu dans l’éditeur. La barre latérale affiche un panneau Traductions listant toutes les locales configurées. Pour chaque locale :

  • “Traduire” apparaît pour les locales sans traduction — cliquez pour en créer une
  • “Modifier” apparaît pour les locales avec traduction existante — cliquez pour y naviguer
  • La locale actuelle est marquée d’une coche

Lors de la création d’une traduction, la nouvelle entrée est pré-remplie avec les données de la locale source et se voit attribuer un slug par défaut de {slug-source}-{locale}. Ajustez le slug et le contenu selon vos besoins, puis enregistrez.

Publication par locale

Chaque traduction a son propre statut. Publiez, dépubliez ou planifiez des traductions de manière indépendante. La version française peut être en brouillon pendant que la version anglaise est en ligne.

API de contenu

Paramètre de locale

Toutes les routes de l’API de contenu acceptent un paramètre de requête locale optionnel :

GET /_emdash/api/content/posts?locale=fr
GET /_emdash/api/content/posts/my-post?locale=fr

Lorsqu’omis, utilise par défaut la locale par défaut configurée.

Création de traductions via l’API

Créez une traduction en passant locale et translationOf au point de terminaison de création de contenu :

POST /_emdash/api/content/posts
Content-Type: application/json

{
  "locale": "fr",
  "translationOf": "01ABC...",
  "data": {
    "title": "Mon Article",
    "slug": "mon-article"
  }
}

La nouvelle entrée partage le translation_group de l’entrée source et commence en brouillon.

Listage des traductions

Récupérez toutes les traductions pour une entrée donnée :

GET /_emdash/api/content/posts/01ABC.../translations

Retourne l’ID du groupe de traduction et un tableau de variantes de locale avec leurs IDs, slugs et statuts.

CLI

La CLI prend en charge les flags --locale sur les commandes de contenu :

# Lister les articles en français
emdash content list posts --locale fr

# Obtenir une entrée spécifique en français
emdash content get posts my-post --locale fr

# Créer une traduction française d'une entrée existante
emdash content create posts --locale fr --translation-of 01ABC...

Initialisation de contenu multilingue

Les fichiers seed expriment les traductions en utilisant locale et translationOf :

{
  "content": {
    "posts": [
      {
        "id": "welcome",
        "slug": "welcome",
        "locale": "en",
        "status": "published",
        "data": { "title": "Welcome" }
      },
      {
        "id": "welcome-fr",
        "slug": "bienvenue",
        "locale": "fr",
        "translationOf": "welcome",
        "status": "draft",
        "data": { "title": "Bienvenue" }
      }
    ]
  }
}

L’entrée de locale source doit apparaître avant ses traductions dans le fichier seed afin que les références translationOf se résolvent correctement.

Traduisibilité des champs

Chaque champ a un paramètre translatable (par défaut : true). Lors de la création d’une traduction :

  • Les champs traduisibles sont pré-remplis depuis la locale source pour édition
  • Les champs non traduisibles sont copiés et maintenus synchronisés dans toutes les traductions du groupe

Les champs système comme status, published_at et author_id sont toujours par locale et jamais synchronisés.

Stratégie d’URL

EmDash ne gère pas les URL de locale — Astro gère le routage. Modèles courants :

# prefix-other-locales (par défaut Astro)
/blog/my-post          → en (locale par défaut, pas de préfixe)
/fr/blog/mon-article   → fr

# prefix-always
/en/blog/my-post       → en
/fr/blog/mon-article   → fr

Utilisez getRelativeLocaleUrl de astro:i18n pour construire des URL correctes quel que soit le mode de routage.

Importer du contenu multilingue

Importez du contenu WordPress via l’outil de migration admin — voir Import de contenu et Migrer depuis WordPress. Une exportation WXR ne contient pas la structure de locale et de groupe de traduction que WPML ou Polylang ajoutent, donc le contenu importé arrive dans votre locale par défaut.

Pour construire des traductions à partir du contenu importé, créez l’entrée traduite et liez-la à l’original :

emdash content create posts --locale fr --translation-of 01ABC...

C’est le même workflow --locale / --translation-of montré dans Initialisation de contenu multilingue ci-dessus, appliqué après la fin de l’importation.

Prochaines étapes