EmDash integriert sich mit Astro’s eingebautem i18n-Routing für mehrsprachiges Content-Management. Astro übernimmt URL-Routing und Locale-Erkennung; EmDash speichert und liefert übersetzte Inhalte.
Jede Übersetzung ist ein vollständiger, eigenständiger Inhaltseintrag mit eigenem Slug, Status und Revisionsverlauf. Die französische Version eines Beitrags kann im Entwurf sein, während die englische Version veröffentlicht ist.
Konfiguration
Aktivieren Sie i18n mit einem i18n-Block in Ihrer Astro-Konfiguration. EmDash liest diese Konfiguration automatisch – es gibt keine separate Locale-Einrichtung in 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",
}),
}),
],
});
Ohne i18n in der Astro-Konfiguration sind alle i18n-Funktionen deaktiviert; EmDash verhält sich wie ein einsprachiges CMS.
Wie Übersetzungen funktionieren
EmDash nutzt ein eine Zeile pro Locale-Modell. Jede Übersetzung ist eine eigene Datenbankzeile mit eigener ID, eigenem Slug und Status, verknüpft über eine gemeinsame translation_group.
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
Daraus folgt:
- Slugs pro Locale —
/blog/my-postund/fr/blog/mon-articlefunktionieren natürlich - Veröffentlichung pro Locale — Englisch live, Französisch im Entwurf
- Revisionen pro Locale — jede Übersetzung hat eigenen Verlauf
- Keine quer-locale Listen-Komplexität — Listen liefern nur Einträge für eine Locale
Übersetzte Inhalte abfragen
Einzelner Eintrag
Übergeben Sie locale an getEmDashEntry, um eine bestimmte Übersetzung zu laden. Wenn weggelassen, gilt die aktuelle Request-Locale (gesetzt durch Astro’s i18n-Middleware).
---
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>
Fallback-Kette
Wenn für die angeforderte Locale kein Inhalt existiert, folgt EmDash der in der Astro-Konfiguration definierten Fallback-Kette. Bei fallback: { fr: "en" }:
- Angeforderte Locale (
fr) - Fallback-Locale (
en) - Standard-Locale
Fallback gilt nur für Einzelabfragen. Listen liefern nur Einträge für die angeforderte Locale — kein Mischen über Locales.
Collection-Liste
Collection nach Locale filtern:
---
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>
Sprachumschalter
Mit getTranslations bauen Sie einen Sprachumschalter mit Links zu vorhandenen Übersetzungen des aktuellen Eintrags:
---
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 liefert alle Locale-Varianten derselben Übersetzungsgruppe:
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" },
// ]
Übersetzungen im Admin verwalten
Inhaltsliste
Mit aktiviertem i18n zeigt die Liste:
- eine Locale-Spalte pro Eintrag
- einen Locale-Filter in der Toolbar
Übersetzungen anlegen
Öffnen Sie einen beliebigen Eintrag im Editor. In der Seitenleiste erscheint das Panel Translations. Pro konfigurierter Locale:
- „Translate“ wenn noch keine Übersetzung existiert — Klick legt eine an
- „Edit“ wenn eine Übersetzung existiert — Klick springt dorthin
- Die aktuelle Locale ist mit einem Häkchen markiert
Beim Anlegen wird der neue Eintrag aus der Quell-Locale vorbefüllt; Standard-Slug {source-slug}-{locale}. Slug und Inhalt anpassen, dann speichern.
Veröffentlichung pro Locale
Jede Übersetzung hat einen eigenen Status. Veröffentlichen, zurückziehen oder planen Sie unabhängig. Französisch kann Entwurf sein, während Englisch live ist.
Content-API
Locale-Parameter
Alle Content-API-Routen akzeptieren optional locale als Query-Parameter:
GET /_emdash/api/content/posts?locale=fr
GET /_emdash/api/content/posts/my-post?locale=fr
Ohne Angabe gilt die konfigurierte Standard-Locale.
Übersetzungen per API anlegen
Übersetzung mit locale und translationOf am Create-Endpunkt:
POST /_emdash/api/content/posts
Content-Type: application/json
{
"locale": "fr",
"translationOf": "01ABC...",
"data": {
"title": "Mon Article",
"slug": "mon-article"
}
}
Der neue Eintrag teilt die translation_group der Quelle und startet als Entwurf.
Übersetzungen auflisten
Alle Übersetzungen zu einem Eintrag:
GET /_emdash/api/content/posts/01ABC.../translations
Antwort: Übersetzungsgruppen-ID und Array der Locale-Varianten mit ID, Slug und Status.
CLI
Die CLI unterstützt --locale bei Content-Befehlen:
# 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...
Mehrsprachige Inhalte seeden
Seed-Dateien nutzen locale und 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" }
}
]
}
}
Der Quell-Eintrag muss in der Seed-Datei vor seinen Übersetzungen stehen, damit translationOf aufgelöst wird.
Feld-Übersetzbarkeit
Jedes Feld hat translatable (Standard: true). Beim Anlegen einer Übersetzung:
- Übersetzbare Felder werden aus der Quell-Locale zum Bearbeiten vorbefüllt
- Nicht übersetzbare Felder werden kopiert und über alle Varianten der Gruppe synchron gehalten
Systemfelder wie status, published_at und author_id sind immer pro Locale und werden nicht synchronisiert.
URL-Strategie
EmDash verwaltet keine Locale-URLs — das übernimmt Astro. Übliche Muster:
# 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
Nutzen Sie getRelativeLocaleUrl aus astro:i18n für korrekte URLs unabhängig vom Routing-Modus.
Mehrsprachige Inhalte importieren
WordPress mit WPML oder Polylang
Die WordPress-Importquelle erkennt WPML und Polylang automatisch. Importierte Inhalte enthalten dann Locale und Übersetzungsgruppe.
WXR-Dateien
WXR-Exporte enthalten keine WPML/Polylang-Metadaten. Import als eine Locale und Übersetzungen manuell, oder --locale, um allen importierten Elementen eine Locale zuzuweisen:
# 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
Nächste Schritte
- Querying Content — Vollständige Query-API
- Working with Content — Admin-Content-Management
- Astro i18n routing — Astro-Routing-Konfiguration