Internationalisierung (i18n)

Auf dieser Seite

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-post und /fr/blog/mon-article funktionieren 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" }:

  1. Versuche die angeforderte Lokale (fr)
  2. Versuche die Fallback-Lokale (en)
  3. 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-default zeigt 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 ein x-default vermissen 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.locales ist, 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