EmDash integriert sich mit Astros eingebautem i18n-Routing, um mehrsprachige Inhaltsverwaltung bereitzustellen. Astro übernimmt URL-Routing und Locale-Erkennung; EmDash übernimmt die Speicherung und den Abruf übersetzter Inhalte.
Jede Übersetzung ist ein vollständiger, unabhängiger 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, indem Sie einen i18n-Block zu Ihrer Astro-Konfiguration hinzufügen. EmDash liest dieselbe Konfiguration für seine Locale-Liste, Standard-Locale und 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 Zeile-pro-Locale-Modell. Jede Übersetzung ist ihre eigene Zeile in der Datenbank mit eigener ID, eigenem Slug und 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:
- Pro-Locale-Slugs —
/blog/my-postund/fr/blog/mon-articlefunktionieren natürlich - Pro-Locale-Veröffentlichung — veröffentlichen Sie die englische Version, während die französische im Entwurf bleibt
- Pro-Locale-Revisionen — 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 Astros 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 kein Inhalt für die angeforderte Locale existiert, folgt EmDash der Fallback-Kette, die in Ihrer Astro-Konfiguration definiert ist. Bei fallback: { fr: "en" }:
- Versuche die angeforderte Locale (
fr) - Versuche die Fallback-Locale (
en) - Versuche die Standard-Locale
Fallback gilt nur für Einzeleintrags-Abfragen. Listenabfragen geben nur Einträge 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 über eine gemeinsame translation_group verknüpft. Menüeinträge lösen ihre Inhaltsreferenzen gegen die Version des referenzierten Inhalts der aktiven Locale auf.
Die folgende Komponente ruft das Hauptmenü für die aktive Locale ab:
---
import { getMenu } from "emdash";
const menu = await getMenu("primary", { locale: Astro.currentLocale });
---
<nav aria-label="Primär">
<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 Menü-Liste des Admins — die Einträge werden mit intakter reference_id geklont (sie speichert die translation_group des referenzierten Inhalts), sodass die Links des neuen Menüs automatisch auf den richtigen pro-Locale-Inhalt zeigen.
Taxonomien (Kategorien, Tags)
Begriffe sind pro-Locale. Definitionen (_emdash_taxonomy_defs) sind ebenfalls pro-Locale, sodass label / labelSingular ebenfalls übersetzt werden können. Der Pivot content_taxonomies.taxonomy_id speichert die translation_group des Begriffs, sodass eine einzelne Zuweisung alle Locales des Inhalts umfasst.
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,
});
---
Das Übersetzen eines Inhalts erbt automatisch die Begriffszuweisungen der Quelle — Sie müssen die Begriffe selbst nur einmal übersetzen, und jeder Beitrag, der sie verwendet, wird zur Lesezeit auf die richtige Locale aufgelöst.
Sammlungsliste
Filtern Sie eine Sammlung 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 auf vorhandene Ü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="Sprache">
<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
Inhaltsliste
Wenn i18n aktiviert ist, zeigt die Inhaltsliste:
- Eine Locale-Spalte, die die Locale jedes Eintrags anzeigt
- Einen Locale-Filter in der Toolbar zum Wechseln zwischen Locales
Übersetzungen erstellen
Öffnen Sie einen beliebigen Inhaltseintrag im Editor. Die Seitenleiste zeigt ein Übersetzungen-Panel, das alle konfigurierten Locales auflistet. Für jede Locale:
- “Übersetzen” erscheint für Locales ohne Übersetzung — klicken Sie, um eine zu erstellen
- “Bearbeiten” erscheint für Locales mit einer bestehenden Ü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 {quell-slug}-{locale}. Passen Sie Slug und Inhalt nach Bedarf an und speichern Sie dann.
Pro-Locale-Veröffentlichung
Jede Übersetzung hat ihren eigenen Status. Veröffentlichen, zurückziehen oder planen Sie Übersetzungen unabhängig. Die französische Version kann im Entwurf sein, während die englische Version live ist.
Inhalts-API
Locale-Parameter
Alle Inhalts-API-Routen akzeptieren einen optionalen locale-Abfrageparameter:
GET /_emdash/api/content/posts?locale=fr
GET /_emdash/api/content/posts/my-post?locale=fr
Wenn weggelassen, wird standardmäßig die konfigurierte Standard-Locale verwendet.
Übersetzungen über API erstellen
Erstellen Sie eine Übersetzung, indem Sie locale und translationOf an den Inhalts-Erstellungs-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 beginnt 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 Inhalts-Befehlen:
# Französische Beiträge auflisten
emdash content list posts --locale fr
# Einen bestimmten Eintrag auf Französisch abrufen
emdash content get posts my-post --locale fr
# Eine 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 zur Bearbeitung vorausgefüllt
- Nicht-übersetzbare Felder werden kopiert und über alle Übersetzungen in 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-Sammlung-Sitemap unter /sitemap-{collection}.xml ist locale-bewusst. Wenn i18n aktiviert ist, wird jede Übersetzung als eigener <url>-Eintrag ausgegeben, wobei das Locale-Präfix über Astros getRelativeLocaleUrl aufgelöst wird. Ihre prefixDefaultLocale-Einstellung und alle benutzerdefinierten Locale-path-Zuordnungen werden automatisch berücksichtigt.
Übersetzungsgeschwister werden mit xhtml:link-Alternaten kreuzverknüpft, sodass Suchmaschinen jedem Benutzer die richtige Sprache bereitstellen 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.
hreflang-Alternate im Seiten-Head
Dieselben Alternate gehören in den <head> jeder Inhaltsseite. Wenn Ihr Layout <EmDashHead> verwendet, geschieht dies automatisch: Wenn i18n aktiviert ist und der Seitenkontext content enthält, wird ein <link rel="alternate"> pro veröffentlichtem Übersetzungsgeschwister ausgegeben — einschließlich eines selbstreferenzierenden Links, wie Google empfiehlt — plus x-default:
<link rel="alternate" hreflang="en" href="https://example.com/blog/hello" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/blog/bonjour" />
<link rel="alternate" hreflang="x-default" href="https://example.com/blog/hello" />
Für manuell erstellte Heads lösen Sie die Alternate mit getHreflangAlternates auf:
---
import { getEmDashEntry, getHreflangAlternates } from "emdash";
const { entry } = await getEmDashEntry("posts", Astro.params.slug);
const alternates = await getHreflangAlternates("posts", entry.data.id, {
siteUrl: Astro.url.origin,
});
---
<head>
{alternates.map((a) => <link rel="alternate" hreflang={a.hreflang} href={a.href} />)}
</head>
Das Verhalten entspricht genau der Sitemap:
x-defaultzeigt auf die Standard-Locale-Variante. Wenn die Standard-Locale keine veröffentlichte Übersetzung hat, fällt sie auf die erste routbare Variante zurück, sodass der Satz nie einx-defaultvermisst.- Unveröffentlichte Geschwister werden ausgeschlossen — Entwurfsübersetzungen gelangen nie in die Alternate.
- Nicht-routbare Locales werden verworfen. Eine Zeile, deren Locale nicht in Ihren konfigurierten
i18n.localesist, kann nicht bereitgestellt werden, und Suchmaschinen auf eine 404 zu verlinken ist schlimmer als kein Link. - Nicht übersetzte Einträge erhalten trotzdem einen selbstreferenzierenden Alternate und
x-default, wenn i18n aktiviert ist, spiegelnd zur Sitemap. - Bei deaktiviertem i18n ist das Ergebnis leer und keine Abfragen werden ausgeführt.
URLs werden aus dem urlPattern der Sammlung erstellt und über Ihre Astro-i18n-Routing-Konfiguration (prefixDefaultLocale, benutzerdefinierte Locale-path-Zuordnungen) lokalisiert, sodass Head und Sitemap immer übereinstimmen.
Mehrsprachige Inhalte importieren
Importieren Sie WordPress-Inhalte über das Admin-Migrationstool — siehe Inhaltsimport und Von WordPress migrieren. Ein WXR-Export trägt nicht die Locale- und Übersetzungsgruppen-Struktur, die WPML oder Polylang hinzufügen, sodass importierte Inhalte in Ihrer Standard-Locale landen.
Um Übersetzungen aus importierten Inhalten zu erstellen, erstellen Sie den übersetzten Eintrag und verknüpfen ihn mit dem Original:
emdash content create posts --locale fr --translation-of 01ABC...
Dies ist derselbe --locale / --translation-of-Workflow, der oben in Mehrsprachige Inhalte seeden gezeigt wird, angewendet nach Abschluss des Imports.
Nächste Schritte
- Inhalte abfragen — Vollständige Abfrage-API-Referenz
- Mit Inhalten arbeiten — Admin-Inhaltsverwaltung
- Astro i18n-Routing — Astros Routing-Konfiguration