EmDash s’intègre au routage i18n intégré d’Astro pour la gestion multilingue. Astro gère les URL et la détection de langue ; EmDash stocke et récupère les traductions.
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 à la configuration Astro. EmDash lit cette configuration automatiquement — il n’y a pas de réglage de langue séparé dans EmDash.
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",
}),
}),
],
});
Sans bloc i18n dans la config Astro, toutes les fonctions i18n sont désactivées et EmDash se comporte comme un CMS monolingue.
Fonctionnement des traductions
EmDash utilise un modèle une ligne par langue. Chaque traduction est sa propre ligne en base avec son ID, slug et statut, liée aux autres via un translation_group partagé.
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
Cela implique :
- Slugs par langue —
/blog/my-postet/fr/blog/mon-articlefonctionnent naturellement - Publication par langue — publier l’anglais tout en gardant le français en brouillon
- Révisions par langue — chaque traduction a son propre historique
- Pas de mélange dans les listes — les requêtes de liste ne renvoient qu’une langue à la fois
Interroger le contenu traduit
Entrée unique
Passez locale à getEmDashEntry pour charger une traduction précise. S’il est omis, la langue courante de la requête s’applique (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
S’il n’y a pas de contenu pour la langue demandée, EmDash suit la chaîne définie dans la config Astro. Avec fallback: { fr: "en" } :
- Langue demandée (
fr) - Langue de repli (
en) - Langue par défaut
Le repli ne s’applique qu’aux requêtes d’entrée unique. Les listes ne renvoient que la langue demandée — pas de mélange.
Liste d’une collection
Filtrez une collection par langue :
---
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 lier vers les traductions existantes de l’entrée courante :
---
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>
getTranslations renvoie toutes les variantes du même groupe :
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
Avec i18n activé, la liste affiche :
- Une colonne langue pour chaque entrée
- Un filtre langue dans la barre d’outils
Créer des traductions
Ouvrez une entrée dans l’éditeur. Le panneau latéral Translations liste les langues configurées. Pour chacune :
- « Translate » s’il n’y a pas encore de traduction — cliquez pour en créer une
- « Edit » si une traduction existe — cliquez pour y aller
- La langue courante est indiquée par une coche
À la création, la nouvelle entrée est préremplie depuis la langue source avec un slug par défaut {slug-source}-{locale}. Ajustez slug et contenu, puis enregistrez.
Publication par langue
Chaque traduction a son propre statut. Publiez, dépubliez ou planifiez indépendamment. Le français peut rester en brouillon pendant que l’anglais est en ligne.
API de contenu
Paramètre locale
Toutes les routes de l’API de contenu acceptent un paramètre de requête optionnel locale :
GET /_emdash/api/content/posts?locale=fr
GET /_emdash/api/content/posts/my-post?locale=fr
S’il est omis, la langue par défaut configurée s’applique.
Créer des traductions via l’API
Créez une traduction en passant locale et translationOf au point de création :
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 la source et commence en brouillon.
Lister les traductions
Récupérez toutes les traductions d’une entrée :
GET /_emdash/api/content/posts/01ABC.../translations
Renvoie l’ID du groupe et un tableau de variantes avec ID, slug et statut.
CLI
La CLI accepte --locale sur les commandes de contenu :
# List French posts
emdash content list posts --locale fr
# Get a specific entry in French
emdash content get posts my-post --locale fr
# Create a French translation of an existing entry
emdash content create posts --locale fr --translation-of 01ABC...
Données de départ multilingues
Les fichiers seed expriment les traductions avec 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 dans la langue source doit apparaître avant ses traductions pour que translationOf se résolve correctement.
Champs traduisibles
Chaque champ a l’option translatable (défaut : true). Lors de la création d’une traduction :
- Les champs traduisibles sont préremplis depuis la langue source pour édition
- Les non traduisibles sont copiés et restent synchronisés dans tout le groupe
Les champs système comme status, published_at et author_id sont toujours par langue et ne sont pas synchronisés.
Stratégie d’URL
EmDash ne gère pas les URL par langue — Astro gère le routage. Exemples :
# prefix-other-locales (Astro default)
/blog/my-post → en (default locale, no prefix)
/fr/blog/mon-article → fr
# prefix-always
/en/blog/my-post → en
/fr/blog/mon-article → fr
Utilisez getRelativeLocaleUrl depuis astro:i18n pour des URL correctes quel que soit le mode.
Importer du contenu multilingue
WordPress avec WPML ou Polylang
La source d’import WordPress détecte WPML et Polylang. Le contenu importé inclut alors les métadonnées de langue et de groupe de traduction.
Fichiers WXR
Les exports WXR n’incluent pas ces métadonnées. Importez en une seule langue et créez les traductions manuellement, ou utilisez --locale pour assigner une langue à tous les éléments :
# Import a French WXR export
emdash import wordpress export-fr.xml --execute --locale fr
# Match against existing English content by slug
emdash import wordpress export-fr.xml --execute --locale fr --translation-of-locale en
Prochaines étapes
- Querying Content — Référence complète des requêtes
- Working with Content — Gestion du contenu dans l’admin
- Astro i18n routing — Configuration du routage Astro