EmDash integriert sich mit Astros eingebautem i18n-Routing, um mehrsprachiges Content-Management zu bieten. Astro übernimmt URL-Routing und Locale-Erkennung; EmDash übernimmt die Speicherung und den Abruf übersetzter Inhalte.
Jede Übersetzung ist ein vollständiger, unabhängiger Content-Eintrag mit eigenem Slug, Status und Revisionshistorie. Die französische Version eines Beitrags kann im Entwurf sein, während die englische Version veröffentlicht ist.
Locales konfigurieren
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, werden alle i18n-Funktionen deaktiviert und EmDash verhält sich als 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, verbunden 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-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 ihre eigene Revisionshistorie
- Einzelne-Locale-Abfragen — Listenabfragen geben Einträge nur für eine Locale zurück
Slugs, Eintrags-IDs und Datenbank-IDs
Ein Eintrag hat zwei Identifier mit unterschiedlichen Zwecken:
entry.idist der Slug des Eintrags. Verwenden Sie ihn beim Erstellen der öffentlichen URL.entry.data.idist die Datenbank-ID. Verwenden Sie sie für API-Operationen und Helfer, die auf eine gespeicherte Inhaltszeile verweisen, einschließlichgetTranslations()undgetEntryTerms().
Übersetzungen haben unterschiedliche Datenbank-IDs, da jede Locale eine separate Zeile ist. Ihre gemeinsame translation_group zeichnet auf, dass die Zeilen Übersetzungen desselben Inhalts sind. EmDash verwaltet diese Gruppe, wenn Sie eine Übersetzung erstellen; Templates benötigen normalerweise nur die Datenbank-ID einer beliebigen Zeile in der Gruppe.
Übersetzte Inhalte abfragen
Einzelner Eintrag
Übergeben Sie Astro.currentLocale an getEmDashEntry auf einer mehrsprachigen Route. Astro kennt die vom Router ausgewählte Locale, während EmDash den expliziten Wert benötigt, um Slugs zu disambiguieren, die in mehr als einer Locale existieren können. Dasselbe gilt für Collection-Abfragen.
---
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 ein passender veröffentlichter Eintrag in der angeforderten Locale nicht existiert, folgt getEmDashEntry der Fallback-Kette aus der Astro-Konfiguration. Im Vorschau- oder visuellen Bearbeitungsmodus kann dieselbe Suche einen Entwurf zurückgeben. Bei fallback: { fr: "en" }:
- Versuche die angeforderte Locale (
fr) - Versuche die Fallback-Locale (
en) - Versuche die Standard-Locale, wenn sie nicht bereits in der Kette ist
Fallback gilt nur für Einzeleintrags-Abfragen. Listenabfragen geben Einträge nur für die angeforderte Locale zurück.
Jede Fallback-Suche verwendet dasselbe id-Argument. Zum Beispiel kann eine Anfrage für den Slug about von Französisch auf einen englischen Eintrag zurückfallen, dessen Slug ebenfalls about ist. Eine Anfrage für a-propos kann keinen englischen Eintrag entdecken, dessen Slug about ist; die beiden Zeilen verwenden unterschiedliche öffentliche Identifier. Verwenden Sie getTranslations(), um Locale-Varianten mit unterschiedlichen Slugs zu finden und zu verlinken.
Menüs
Menüs sind pro Locale — derselbe name (z.B. "primary") kann in mehreren Locales existieren, alle über eine gemeinsame translation_group verlinkt. Menüelemente lösen ihre Inhaltsreferenzen gegen die Version des referenzierten Inhalts in 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="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 Menüs-Liste im Admin — die Elemente 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 auch übersetzt werden können. Der Pivot content_taxonomies.taxonomy_id speichert die translation_group des Begriffs, sodass eine einzelne Zuweisung jede Locale 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.data.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, löst sich zur Lesezeit in die richtige Locale auf.
Reparatur von Taxonomie-Locale-Diskrepanzen
Wenn der Admin sein Site-Manifest lädt, warnt EmDash in den Serverlogs, wenn Taxonomie-Definitionen oder -Begriffe eine Locale verwenden, die nicht in den konfigurierten i18n.locales der Site enthalten ist. Ohne eine i18n-Konfiguration ist en die effektive Locale. Diese Zeilen werden unverändert gelassen, da EmDash nicht ableiten kann, welche konfigurierte Locale der bestehende Inhalt verwenden sollte.
Sichern Sie die Datenbank und inspizieren Sie dann 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 Locale 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 exakte Schreibweise aus i18n.locales. Prüfen Sie vor dem Update, ob eine Zeile mit demselben Taxonomienamen und der Ziel-Locale oder demselben Begriffsnamen, Slug und der Ziel-Locale existiert. Diese Kombinationen sind eindeutig; wenn eine Zielzeile bereits existiert, gleichen Sie die Übersetzungen ab, anstatt ein Massen-Locale-Update anzuwenden. Starten Sie EmDash neu und bestätigen Sie, dass die Warnung nicht mehr erscheint.
Collection-Auflistung
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.id}`}>{post.data.title}</a></li>
))}
</ul>
Einen Sprachumschalter bauen
Verwenden Sie getTranslations, um einen Sprachumschalter zu bauen, 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);
const publishedTranslations = translations.filter(
(translation): translation is typeof translation & { slug: string } =>
translation.status === "published" && translation.slug !== null
);
---
<nav aria-label="Language">
<ul>
{publishedTranslations.map((translation) => (
<li>
<a
href={getRelativeLocaleUrl(translation.locale, `/blog/${translation.slug}`)}
aria-current={translation.locale === Astro.currentLocale ? "page" : undefined}
>
{translation.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.data.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 Symbolleiste 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:
- “Translate” erscheint für Locales ohne Übersetzung — klicken Sie, um eine zu erstellen
- “Edit” 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 den 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.
Die Content-API verwenden
Locale-Parameter
Content-API-Routen erfordern eine authentifizierte Sitzung oder ein Bearer-Token. Listenrouten akzeptieren einen optionalen locale-Abfrageparameter. Eine Einzeleintrags-Route akzeptiert ihn ebenfalls, wenn der Pfad einen Slug verwendet; Datenbank-IDs sind global eindeutig und benötigen keine Locale-Disambiguierung.
GET /_emdash/api/content/posts?locale=fr
GET /_emdash/api/content/posts/my-post?locale=fr
Wenn eine Listenanfrage locale weglässt, verwendet sie die konfigurierte Standard-Locale.
Übersetzungen per 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
X-EmDash-Request: 1
{
"locale": "fr",
"translationOf": "01ABC...",
"slug": "mon-article",
"data": {
"title": "Mon Article"
}
}
translationOf ist die Datenbank-ID der Quellzeile, wie entry.data.id. Der neue Eintrag teilt die translation_group des Quelleintrags und beginnt als Entwurf.
Übersetzungen auflisten
Rufen Sie alle Übersetzungen für einen gegebenen 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.
Die CLI verwenden
Nach der Authentifizierung der CLI verwenden Sie die --locale-Flags bei Content-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 als Entwurf erstellen
emdash content create posts \
--locale fr \
--translation-of 01ABC... \
--slug mon-article \
--data '{"title":"Mon article"}' \
--draft
content create erfordert Eingabe von --data, --file oder --stdin. Es veröffentlicht nach der Erstellung, es sei denn, Sie übergeben --draft.
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.
Wählen, welche Felder übersetzbar sind
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
Bei einer Collection mit Revisionen kopiert das Veröffentlichen eines Eintrags die geänderten nicht übersetzbaren Werte in die anderen Übersetzungen, und das Speichern eines Entwurfs ändert nur diesen Eintrag. Wenn eine andere Übersetzung einen ausstehenden Entwurf hat, der einen dieser Werte geändert hat, behält der Entwurf seinen eigenen Wert, und das Veröffentlichen dieser Übersetzung kopiert ihn in den Rest der Gruppe.
Systemfelder wie status, published_at und author_id sind immer pro Locale und werden nie synchronisiert.
Locale-URLs erstellen
EmDash speichert die Locale; Astro übernimmt das öffentliche Routing. Die unterstützte EmDash-Konfiguration lässt die Standard-Locale ohne Präfix:
# prefix-other-locales (Astro-Standard)
/blog/my-post → en (Standard-Locale, kein Präfix)
/fr/blog/mon-article → fr
Verwenden Sie getRelativeLocaleUrl aus astro:i18n, um das korrekte Präfix und jedes benutzerdefinierte Locale-Pfad-Mapping hinzuzufügen. Aktivieren Sie kein Standard-Locale-Präfix; wie in Locales konfigurieren beschrieben, verhindert diese Routing-Strategie das Laden der injizierten Admin-Seiten.
Sitemaps
Die pro-Collection-Sitemap unter /sitemap-{collection}.xml ist locale-bewusst. Sie enthält veröffentlichte Einträge aus routbaren, SEO-aktivierten Collections. Gelöschte Einträge, Einträge ohne Slug und als noindex markierte Einträge werden ausgeschlossen. Jede enthaltene Übersetzung wird zu einem eigenen <url>-Eintrag. EmDash erstellt seinen Pfad aus dem urlPattern der Collection und wendet dann Astros Locale-Präfix und jedes benutzerdefinierte Locale-path-Mapping an.
Übersetzungs-Geschwister sind mit xhtml:link-Alternaten verlinkt, sodass Suchmaschinen dem jeweiligen Benutzer die richtige Sprache anbieten 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 veröffentlichte Locale-Variante als Alternate bei jeder anderen veröffentlichten, indexierbaren Variante erscheint. Locales, die nicht in i18n.locales enthalten sind, werden weggelassen, da Astro keine Route für sie hat. Websites mit einer einzelnen Locale erzeugen eine einfache Sitemap ohne xhtml-Namespace.
hreflang-Links zum Seitenheader hinzufügen
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, gibt es ein <link rel="alternate"> pro veröffentlichtem Übersetzungs-Geschwister aus — 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 selbst erstellte Headers lösen Sie die Alternate mit getHreflangAlternates auf:
---
import { getEmDashEntry, getHreflangAlternates } from "emdash";
const { entry, error } = await getEmDashEntry("posts", Astro.params.slug, {
locale: Astro.currentLocale,
});
if (error) return new Response("Server error", { status: 500 });
if (!entry) return Astro.redirect("/404");
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 stimmt mit der Sitemap überein:
x-defaultzeigt auf die Standard-Locale-Variante. Wenn die Standard-Locale keine veröffentlichte Übersetzung hat, fällt es auf die erste routbare Variante zurück, sodass dem Set nie einx-defaultfehlt.- Unveröffentlichte Geschwister werden ausgeschlossen — Entwurfs-Übersetzungen gelangen nie in die Alternate.
noindex-Geschwister werden ausgeschlossen. Wenn der aktuelle Eintragnoindexist, werden keine Alternate zurückgegeben.- Nicht routbare Locales werden entfernt. Eine Zeile, deren Locale nicht in Ihren konfigurierten
i18n.localesist, kann nicht bereitgestellt werden, und das Verlinken von Suchmaschinen auf eine 404 ist schlimmer als kein Link. - Nicht übersetzte Einträge erhalten trotzdem ein selbstreferenzierendes Alternate und
x-default, wenn i18n aktiviert ist, was die Sitemap widerspiegelt. - Bei deaktiviertem i18n ist das Ergebnis leer und es werden keine Abfragen ausgeführt.
URLs werden aus dem urlPattern der Collection erstellt und durch die Astro-i18n-Konfiguration lokalisiert. getHreflangAlternates() benötigt eine absolute Site-URL. Es verwendet siteUrl aus dem Aufruf oder die Site-Einstellungs-URL; ohne beides gibt es ein leeres Array zurück, da hreflang-Links absolut sein müssen.
Mehrsprachige Inhalte importieren
Importieren Sie WordPress-Inhalte über das Admin-Migrationstool — siehe Content Import und Von WordPress migrieren. Ein WXR-Export enthält 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 als Entwurf und verlinken ihn mit der ursprünglichen Datenbank-ID:
emdash content create posts \
--locale fr \
--translation-of 01ABC... \
--slug mon-article \
--data '{"title":"Mon article"}' \
--draft
Dies ist dieselbe --locale- und --translation-of-Beziehung, die von Seed-Dateien verwendet wird, angewendet nach Abschluss des Imports.
Nächste Schritte
- Inhalte abfragen — Vollständige Abfrage-API-Referenz
- Mit Inhalten arbeiten — Admin-Content-Management
- Astro i18n-Routing — Astros Routing-Konfiguration