EmDash integriert sich mit Astro’s eingebautem i18n-Routing, um mehrsprachiges Content-Management bereitzustellen. Astro übernimmt URL-Routing und Locale-Erkennung; EmDash verwaltet die Speicherung und den Abruf übersetzter Inhalte.
Jede Übersetzung ist ein vollständiger, unabhängiger Content-Eintrag 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, indem Sie einen i18n-Block zu Ihrer Astro-Konfiguration hinzufügen. EmDash liest dieselbe Konfiguration für die Locale-Liste, die Standard-Locale und die Fallback-Kette.
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",
}),
}),
],
});
Wenn i18n in der Astro-Konfiguration nicht vorhanden ist, sind alle i18n-Funktionen deaktiviert und EmDash verhält sich wie ein einsprachiges CMS.
Wie Übersetzungen funktionieren
EmDash verwendet ein Row-per-Locale-Modell. Jede Übersetzung ist eine eigene Zeile in der Datenbank mit eigener ID, eigenem Slug und eigenem Status, verknüpft mit anderen Übersetzungen über eine gemeinsame translation_group-Kennung. Eine Posts-Tabelle mit drei Übersetzungen sieht so aus:
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
Dieses Design bedeutet:
- Slugs pro Locale —
/blog/my-postund/fr/blog/mon-articlefunktionieren natürlich - Veröffentlichung pro Locale — veröffentlichen Sie die englische Version, während Französisch im Entwurf bleibt
- Revisionen pro Locale — jede Übersetzung hat ihren eigenen Revisionsverlauf
- Einzel-Locale-Abfragen — Listenabfragen geben Einträge nur für eine Locale zurück
Übersetzte Inhalte abfragen
Einzelner Eintrag
Übergeben Sie locale an getEmDashEntry, um eine bestimmte Übersetzung abzurufen. Wenn weggelassen, wird standardmäßig die aktuelle Locale der Anfrage verwendet (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 Ihrer Astro-Konfiguration definierten Fallback-Kette. Bei fallback: { fr: "en" }:
- Versuchen Sie die angeforderte Locale (
fr) - Versuchen Sie die Fallback-Locale (
en) - Versuchen Sie die Standard-Locale
Fallback gilt nur für Einzel-Eintrags-Abfragen. Listenabfragen geben Einträge nur für die angeforderte Locale zurück.
Menüs
Menüs sind pro Locale — derselbe name (z. B. "primary") kann in mehreren Locales existieren, alle verknüpft über eine gemeinsame translation_group. Menüeinträge lösen ihre Content-Referenzen gegen die Version des referenzierten Inhalts in der aktiven Locale auf.
Die folgende Komponente ruft das primäre Menü für die aktive Locale ab:
---
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>
Erstellen Sie Übersetzungen eines bestehenden Menüs aus der Menus-Liste im Admin — die Einträge werden mit intaktem reference_id geklont (es speichert die translation_group des referenzierten Inhalts), sodass die Links des neuen Menüs automatisch auf den richtigen Inhalt pro Locale verweisen.
Taxonomien (Kategorien, Tags)
Begriffe sind pro Locale. Definitionen (_emdash_taxonomy_defs) sind ebenfalls pro Locale, sodass label / labelSingular übersetzt werden können. Die Pivot-Spalte content_taxonomies.taxonomy_id speichert die translation_group des Begriffs, sodass eine einzelne Zuweisung jede Locale des Inhalts abdeckt.
Das folgende Beispiel ruft Kategorien und die Begriffe eines Beitrags für die aktive Locale ab:
---
import { getTaxonomyTerms, getEntryTerms } from "emdash";
const categories = await getTaxonomyTerms("category", {
locale: Astro.currentLocale,
});
const terms = await getEntryTerms("posts", post.id, undefined, {
locale: Astro.currentLocale,
});
---
Beim Übersetzen eines Inhalts werden die Begriffszuweisungen der Quelle automatisch übernommen — Sie müssen nur die Begriffe selbst einmal übersetzen, und jeder Beitrag, der sie verwendet, löst sie beim Lesen in der richtigen Locale auf.
Collection-Listing
Filtern Sie eine Collection nach 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>
Sprachumschalter
Verwenden Sie getTranslations, um einen Sprachumschalter zu erstellen, der zu bestehenden Übersetzungen des aktuellen Eintrags verlinkt:
---
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>
Die Funktion getTranslations gibt alle Locale-Varianten in derselben Übersetzungsgruppe zurück:
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
Content-Liste
Wenn i18n aktiviert ist, zeigt die Content-Liste:
- Eine Locale-Spalte, die die Locale jedes Eintrags anzeigt
- Einen Locale-Filter in der Symbolleiste zum Wechseln zwischen Locales
Übersetzungen erstellen
Öffnen Sie einen beliebigen Content-Eintrag im Editor. Die Seitenleiste zeigt ein Translations-Panel mit allen konfigurierten Locales. Für jede Locale:
- “Translate” erscheint für Locales ohne Übersetzung — klicken Sie, um eine zu erstellen
- “Edit” erscheint für Locales mit bestehender Übersetzung — klicken Sie, um dorthin zu navigieren
- Die aktuelle Locale ist mit einem Häkchen markiert
Beim Erstellen einer Übersetzung wird der neue Eintrag mit Daten aus der Quell-Locale vorausgefüllt und erhält einen Standard-Slug von {source-slug}-{locale}. Passen Sie Slug und Inhalt nach Bedarf an und speichern Sie.
Veröffentlichung pro Locale
Jede Übersetzung hat ihren eigenen Status. Veröffentlichen, zurückziehen oder planen Sie Übersetzungen unabhängig voneinander. Die französische Version kann im Entwurf sein, während die englische Version live ist.
Content-API
Locale-Parameter
Alle Content-API-Routen akzeptieren einen optionalen locale-Query-Parameter:
GET /_emdash/api/content/posts?locale=fr
GET /_emdash/api/content/posts/my-post?locale=fr
Wenn weggelassen, wird die konfigurierte Standard-Locale verwendet.
Übersetzungen über die API erstellen
Erstellen Sie eine Übersetzung, indem Sie locale und translationOf an den Content-Create-Endpunkt übergeben:
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 des Quelleintrags und startet als Entwurf.
Übersetzungen auflisten
Rufen Sie alle Übersetzungen für einen bestimmten Eintrag ab:
GET /_emdash/api/content/posts/01ABC.../translations
Gibt die Übersetzungsgruppen-ID und ein Array von Locale-Varianten mit ihren IDs, Slugs und Status zurück.
CLI
Die CLI unterstützt --locale-Flags bei Content-Befehlen:
# Französische Beiträge auflisten
emdash content list posts --locale fr
# Bestimmten Eintrag auf Französisch abrufen
emdash content get posts my-post --locale fr
# Französische Übersetzung eines bestehenden Eintrags erstellen
emdash content create posts --locale fr --translation-of 01ABC...
Mehrsprachige Inhalte seeden
Seed-Dateien drücken Übersetzungen mit locale und translationOf aus:
{
"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-Locale-Eintrag muss vor seinen Übersetzungen in der Seed-Datei erscheinen, damit translationOf-Referenzen korrekt aufgelöst werden.
Feldübersetzbarkeit
Jedes Feld hat eine translatable-Einstellung (Standard: true). Beim Erstellen einer Übersetzung:
- Übersetzbare Felder werden aus der Quell-Locale zum Bearbeiten vorausgefüllt
- Nicht übersetzbare Felder werden kopiert und in allen Übersetzungen der Gruppe synchron gehalten
Systemfelder wie status, published_at und author_id sind immer pro Locale und werden nie synchronisiert.
URL-Strategie
EmDash verwaltet keine Locale-URLs — Astro übernimmt das Routing. Gängige Muster:
# prefix-other-locales (Astro-Standard)
/blog/my-post → en (Standard-Locale, kein Präfix)
/fr/blog/mon-article → fr
# prefix-always
/en/blog/my-post → en
/fr/blog/mon-article → fr
Verwenden Sie getRelativeLocaleUrl aus astro:i18n, um korrekte URLs unabhängig vom Routing-Modus zu erstellen.
Sitemaps
Die pro-Collection-Sitemap unter /sitemap-{collection}.xml ist locale-aware. Wenn i18n aktiviert ist, wird jede Übersetzung als eigener <url>-Eintrag ausgegeben, wobei das Locale-Präfix über Astro’s getRelativeLocaleUrl aufgelöst wird. Ihre prefixDefaultLocale-Einstellung und alle benutzerdefinierten Locale-path-Zuordnungen werden automatisch berücksichtigt.
Übersetzungs-Geschwister werden mit xhtml:link-Alternates verknüpft, damit Suchmaschinen jedem Nutzer die richtige Sprache ausliefern können:
<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>
Geschwister werden nach translation_group gruppiert, sodass eine später hinzugefügte Zeile (eine neue Locale-Variante eines bestehenden Beitrags) automatisch als Alternate bei jeder anderen Variante erscheint. Websites mit einer einzelnen Locale erzeugen eine einfache Sitemap ohne xhtml-Namespace.
Mehrsprachige Inhalte importieren
Importieren Sie WordPress-Inhalte über das Admin-Migrationstool — siehe Content Import und Migrate from WordPress. Ein WXR-Export enthält nicht die Locale- und Übersetzungsgruppen-Struktur, die WPML oder Polylang hinzufügen, daher landen importierte Inhalte in Ihrer Standard-Locale.
Um Übersetzungen aus importierten Inhalten zu erstellen, erstellen Sie den übersetzten Eintrag und verknüpfen Sie ihn mit dem Original:
emdash content create posts --locale fr --translation-of 01ABC...
Dies ist derselbe --locale / --translation-of-Workflow wie in Mehrsprachige Inhalte seeden oben, angewendet nach Abschluss des Imports.
Nächste Schritte
- Querying Content — Vollständige Query-API-Referenz
- Working with Content — Admin-Content-Management
- Astro i18n routing — Astro’s Routing-Konfiguration