EmDash s’intègre au 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 tandis 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, la locale par défaut et la chaîne de repli.
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 une ligne par locale. Chaque traduction est sa propre ligne dans la base de données avec son propre ID, slug et statut, liée aux 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-postet/fr/blog/mon-articlefonctionnent 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 mono-locale — les requêtes de liste renvoient des entrées pour une seule locale
Interroger le contenu traduit
Entrée unique
Passez locale à getEmDashEntry pour récupérer une traduction spécifique. Lorsqu’il est omis, la locale actuelle de la requête est utilisée par défaut (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 repli
Lorsqu’aucun contenu n’existe pour la locale demandée, EmDash suit la chaîne de repli définie dans votre configuration Astro. Avec fallback: { fr: "en" } :
- Essayez la locale demandée (
fr) - Essayez la locale de repli (
en) - Essayez la locale par défaut
Le repli ne s’applique qu’aux requêtes d’entrée unique. Les requêtes de liste renvoient des entrées pour la locale demandée uniquement.
Menus
Les menus sont par locale — le même name (par 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 du contenu référencé dans la locale active.
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é), donc 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. La colonne pivot content_taxonomies.taxonomy_id stocke le translation_group du terme, donc une seule attribution 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,
});
---
La traduction d’un contenu hérite automatiquement des attributions de termes de la source — vous n’avez besoin de traduire les termes eux-mêmes qu’une seule fois, et chaque article qui les utilise les résout à la bonne locale à la lecture.
Liste 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 renvoie 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" },
// ]
Gérer les traductions dans l’Admin
Liste de contenu
Lorsque i18n est activé, la liste de contenu affiche :
- Une colonne locale affichant la locale de chaque entrée
- Un filtre de locale dans la barre d’outils pour basculer entre les locales
Créer des traductions
Ouvrez n’importe quelle entrée de contenu dans l’éditeur. La barre latérale affiche un panneau Translations listant toutes les locales configurées. Pour chaque locale :
- “Translate” apparaît pour les locales sans traduction — cliquez pour en créer une
- “Edit” apparaît pour les locales avec une 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 {source-slug}-{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 les traductions indépendamment. La version française peut être en brouillon tandis que la version anglaise est en ligne.
API de contenu
Paramètre 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’il est omis, la locale par défaut configurée est utilisée.
Créer des 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.
Lister les traductions
Récupérez toutes les traductions pour une entrée donnée :
GET /_emdash/api/content/posts/01ABC.../translations
Renvoie 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...
Semer du 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 la locale source doit apparaître avant ses traductions dans le fichier seed pour 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 ne sont 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, sans 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.
Sitemaps
Le sitemap par collection à /sitemap-{collection}.xml est conscient de la locale. Lorsque i18n est activé, chaque traduction est émise comme sa propre entrée <url>, avec le préfixe de locale résolu via getRelativeLocaleUrl d’Astro. Votre paramètre prefixDefaultLocale et tout mappage personnalisé de path de locale sont respectés automatiquement.
Les traductions sœurs sont liées en croix avec des alternatives xhtml:link pour que les moteurs de recherche servent la bonne langue à chaque utilisateur :
<url>
<loc>https://example.com/blog/hello</loc>
<lastmod>2026-05-28T16:33:15.461Z</lastmod>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/blog/hello" />
<xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/blog/bonjour" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/blog/hello" />
</url>
Les sœurs sont regroupées par translation_group, donc une ligne ajoutée ultérieurement (une nouvelle variante de locale d’un article existant) apparaît automatiquement comme alternative sur chaque autre variante. Les sites avec une seule locale produisent un sitemap simple sans namespace xhtml.
Importer du contenu multilingue
Importez du contenu WordPress via l’outil de migration de l’admin — voir Content Import et Migrate from 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 flux de travail --locale / --translation-of montré dans Semer du contenu multilingue ci-dessus, appliqué après la fin de l’importation.
Prochaines étapes
- Querying Content — Référence complète de l’API de requête
- Working with Content — Gestion de contenu dans l’admin
- Astro i18n routing — Configuration du routage Astro