EmDash integriert sich mit Astros eingebautem i18n-Routing, um mehrsprachige Inhaltsverwaltung zu bieten. Astro übernimmt URL-Routing und Lokale-Erkennung; EmDash übernimmt die Speicherung und den Abruf übersetzter Inhalte.
Jede Übersetzung ist ein vollständiger, unabhängiger Inhaltseintrag mit eigenem Slug, Status und Revisionshistorie. Die französische Version eines Beitrags kann als Entwurf vorliegen, 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 Lokale-Liste, Standard-Lokale 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 als einsprachiges CMS.
Wie Übersetzungen funktionieren
EmDash verwendet ein Zeile-pro-Lokale-Modell. Jede Übersetzung ist eine eigene Zeile in der Datenbank mit eigener ID, eigenem Slug und Status, verknüpft mit anderen Übersetzungen über einen gemeinsamen translation_group-Identifier. 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-Lokale Slugs —
/blog/my-postund/fr/blog/mon-articlefunktionieren natürlich - Pro-Lokale Veröffentlichung — veröffentlichen Sie die englische Version, während Französisch als Entwurf bleibt
- Pro-Lokale Revisionen — jede Übersetzung hat ihre eigene Revisionshistorie
- Einzelne-Lokale Abfragen — Listenabfragen geben Einträge nur für eine Lokale zurück
Übersetzte Inhalte abfragen
Einzelner Eintrag
Übergeben Sie locale an getEmDashEntry, um eine bestimmte Übersetzung abzurufen. Wenn weggelassen, wird standardmäßig die aktuelle Lokale 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 Lokale existiert, folgt EmDash der in Ihrer Astro-Konfiguration definierten Fallback-Kette. Bei fallback: { fr: "en" }:
- Versuche die angeforderte Lokale (
fr) - Versuche die Fallback-Lokale (
en) - Versuche die Standard-Lokale
Fallback gilt nur für Einzeleintrag-Abfragen. Listenabfragen geben Einträge nur für die angeforderte Lokale zurück.
Menüs
Menüs sind pro Lokale — derselbe name (z.B. "primary") kann in mehreren Lokalen existieren, alle über eine gemeinsame translation_group verknüpft. Menüeinträge lösen ihre Inhaltsreferenzen gegen die Version des aktiven Lokales des referenzierten Inhalts auf.
Die folgende Komponente ruft das primäre Menü für die aktive Lokale 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 über die Menüs-Liste des Admins — 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 pro-Lokale Inhalt zeigen.
Taxonomien (Kategorien, Tags)
Terme sind pro Lokale. Definitionen (_emdash_taxonomy_defs) sind ebenfalls pro Lokale, sodass label / labelSingular auch übersetzt werden können. Das Pivot content_taxonomies.taxonomy_id speichert die translation_group des Terms, sodass eine einzelne Zuweisung alle Lokalen des Inhalts umfasst.
Das folgende Beispiel ruft Kategorien und die Terme eines Beitrags für die aktive Lokale 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 Term-Zuweisungen der Quelle — Sie müssen die Terme selbst nur einmal übersetzen, und jeder Beitrag, der sie verwendet, löst zur Lesezeit die richtige Lokale auf.
Taxonomie-Lokale-Abweichungen reparieren
Wenn der Admin sein Site-Manifest lädt, warnt EmDash in den Server-Logs, wenn Taxonomie-Definitionen oder Terme eine Lokale verwenden, die nicht in den konfigurierten i18n.locales der Website ist. Ohne eine i18n-Konfiguration ist en die effektive Lokale. Diese Zeilen bleiben unverändert, weil EmDash nicht ableiten kann, welche konfigurierte Lokale der vorhandene Inhalt verwenden sollte.
Sichern Sie die Datenbank, dann untersuchen Sie die in der Warnung genannten betroffenen Zeilen:
SELECT id, name, locale FROM _emdash_taxonomy_defs ORDER BY name, locale;
SELECT id, name, slug, locale FROM taxonomies ORDER BY name, slug, locale;
Nachdem Sie die beabsichtigte Lokale für jede Zeile bestätigt haben, aktualisieren Sie sie nach id:
UPDATE _emdash_taxonomy_defs SET locale = 'ja' WHERE id = '<definition-id>';
UPDATE taxonomies SET locale = 'ja' WHERE id = '<term-id>';
Verwenden Sie die genaue Schreibweise aus i18n.locales. Überprüfen Sie vor der Aktualisierung, ob eine Zeile mit demselben Taxonomienamen und der Ziel-Lokale oder demselben Term-Namen, Slug und der Ziel-Lokale existiert. Diese Kombinationen sind einzigartig; wenn eine Zielzeile bereits existiert, gleichen Sie die Übersetzungen ab, anstatt ein Massen-Lokale-Update durchzuführen. Starten Sie EmDash neu und bestätigen Sie, dass die Warnung nicht mehr erscheint.
Collection-Auflistung
Filtern Sie eine Collection nach Lokale:
---
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 Lokale-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 Lokale-Spalte, die die Lokale jedes Eintrags anzeigt
- Einen Lokale-Filter in der Toolbar zum Wechseln zwischen Lokalen
Übersetzungen erstellen
Öffnen Sie einen beliebigen Inhaltseintrag im Editor. Die Sidebar zeigt ein Übersetzungen-Panel, das alle konfigurierten Lokalen auflistet. Für jede Lokale:
- „Übersetzen” erscheint für Lokalen ohne Übersetzung — klicken Sie, um eine zu erstellen
- „Bearbeiten” erscheint für Lokalen mit vorhandener Übersetzung — klicken Sie, um dorthin zu navigieren
- Die aktuelle Lokale ist mit einem Häkchen markiert
Beim Erstellen einer Übersetzung wird der neue Eintrag mit Daten der Quell-Lokale vorausgefüllt und erhält einen Standard-Slug von {source-slug}-{locale}. Passen Sie Slug und Inhalt nach Bedarf an und speichern Sie.
Pro-Lokale Veröffentlichung
Jede Übersetzung hat ihren eigenen Status. Veröffentlichen, deaktivieren oder planen Sie Übersetzungen unabhängig voneinander. Die französische Version kann als Entwurf vorliegen, während die englische Version live ist.
Content API
Lokale-Parameter
Alle Content-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 die konfigurierte Standard-Lokale verwendet.
Übersetzungen via API erstellen
Erstellen Sie eine Übersetzung, indem Sie locale und translationOf an den Content-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
Alle Übersetzungen für einen gegebenen Eintrag abrufen:
GET /_emdash/api/content/posts/01ABC.../translations
Gibt die Übersetzungsgruppen-ID und ein Array von Lokale-Varianten mit ihren IDs, Slugs und Status zurück.
CLI
Die CLI unterstützt --locale-Flags bei Content-Befehlen:
# Französische Posts 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-Lokale-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 von der Quell-Lokale 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 Lokale und werden nie synchronisiert.
URL-Strategie
EmDash verwaltet keine Lokale-URLs — Astro übernimmt das Routing. Gängige Muster:
# prefix-other-locales (Astro-Standard)
/blog/my-post → en (Standard-Lokale, kein Präfix)
/fr/blog/mon-article → fr
# prefix-always
/en/blog/my-post → en
/fr/blog/mon-article → fr
Verwenden Sie getRelativeLocaleUrl von astro:i18n, um korrekte URLs unabhängig vom Routing-Modus zu erstellen.
Sitemaps
Die Pro-Collection-Sitemap unter /sitemap-{collection}.xml ist lokale-bewusst. Wenn i18n aktiviert ist, wird jede Übersetzung als eigener <url>-Eintrag ausgegeben, wobei das Lokale-Präfix durch Astros getRelativeLocaleUrl aufgelöst wird. Ihre prefixDefaultLocale-Einstellung und benutzerdefinierte Lokale-path-Zuordnungen werden automatisch berücksichtigt.
Übersetzungs-Geschwister werden mit xhtml:link-Alternativen kreuzverlinkt, 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 Lokale-Variante eines bestehenden Beitrags) automatisch als Alternative bei jeder anderen Variante erscheint. Websites mit einer einzelnen Lokale erzeugen eine einfache Sitemap ohne xhtml-Namespace.
hreflang-Alternativen im Seiten-Head
Dieselben Alternativen 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 Übersetzungs-Geschwister 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 handgeschriebene Heads lösen Sie die Alternativen 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-Lokale-Variante. Wenn die Standard-Lokale keine veröffentlichte Übersetzung hat, fällt sie auf die erste routbare Variante zurück, sodass der Satz nie einx-defaultvermissen lässt.- Unveröffentlichte Geschwister werden ausgeschlossen — Entwurfs-Übersetzungen gelangen nie in die Alternativen.
- Nicht routbare Lokalen werden entfernt. Eine Zeile, deren Lokale nicht in Ihren konfigurierten
i18n.localesist, kann nicht bereitgestellt werden, und Suchmaschinen auf eine 404 zu verlinken ist schlimmer als kein Link. - Unübersetzte Einträge erhalten dennoch eine selbstreferenzierende Alternative und
x-default, wenn i18n aktiviert ist, und spiegeln die Sitemap wider. - Bei deaktiviertem i18n ist das Ergebnis leer und es werden keine Abfragen ausgeführt.
URLs werden aus dem urlPattern der Collection erstellt und über Ihre Astro i18n-Routing-Konfiguration (prefixDefaultLocale, benutzerdefinierte Lokale-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 enthält nicht die Lokale- und Übersetzungsgruppen-Struktur, die WPML oder Polylang hinzufügen, sodass importierte Inhalte in Ihrer Standard-Lokale 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 unter 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