Internationalisierung (i18n)

Auf dieser Seite

EmDash integriert sich mit Astros eingebautem i18n-Routing, um mehrsprachiges Content-Management zu ermöglichen. Astro übernimmt das URL-Routing und die 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 sich im Entwurf befinden, 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 diese Konfiguration für die 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 nicht in der Astro-Konfiguration vorhanden ist, sind 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 eine eigene Zeile in der Datenbank mit eigener ID, 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:

  • Locale-spezifische Slugs/blog/my-post und /fr/blog/mon-article funktionieren natürlich
  • Locale-spezifische Veröffentlichung — veröffentlichen Sie die englische Version, während die französische im Entwurf bleibt
  • Locale-spezifische Revisionen — jede Übersetzung hat ihren eigenen Revisionsverlauf
  • Einzelne-Locale-Abfragen — Listenabfragen geben nur Einträge für eine Locale zurück

Abfrage übersetzter Inhalte

Einzelner Eintrag

Übergeben Sie locale an getEmDashEntry, um eine bestimmte Übersetzung abzurufen. Wenn weggelassen, wird standardmäßig die aktuelle Locale der Anfrage verwendet (festgelegt 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 in Ihrer Astro-Konfiguration definierten Fallback-Kette. Bei fallback: { fr: "en" }:

  1. Versuche die angeforderte Locale (fr)
  2. Versuche die Fallback-Locale (en)
  3. 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 locale-spezifisch — derselbe name (z.B. "primary") kann in mehreren Locales existieren, alle verknüpft über eine gemeinsame translation_group. Menüeinträge lösen ihre Inhaltsreferenzen 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 Menüs-Liste im Admin-Bereich — 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 die richtigen locale-spezifischen Inhalte verweisen.

Taxonomien (Kategorien, Tags)

Begriffe sind locale-spezifisch. Definitionen (_emdash_taxonomy_defs) sind ebenfalls locale-spezifisch, sodass auch label / labelSingular ü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 übernimmt automatisch die Begriffszu­weisungen der Quelle — Sie müssen nur die Begriffe selbst einmal übersetzen, und jeder Beitrag, der sie verwendet, wird zur Lesezeit in die richtige Locale aufgelöst.

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.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="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" },
// ]

Verwaltung von Übersetzungen im Admin-Bereich

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

Erstellen von Übersetzungen

Öffnen Sie einen beliebigen Inhaltseintrag im Editor. Die Seitenleiste zeigt ein Übersetzungen-Panel mit allen konfigurierten Locales. Für jede Locale:

  • “Übersetzen” erscheint für Locales ohne Übersetzung — klicken Sie, um eine zu erstellen
  • “Bearbeiten” erscheint für Locales mit vorhandener Ü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 vorgefüllt und erhält einen Standard-Slug von {quell-slug}-{locale}. Passen Sie Slug und Inhalt nach Bedarf an und speichern Sie.

Locale-spezifische Veröffentlichung

Jede Übersetzung hat ihren eigenen Status. Veröffentlichen, unveröffentlichen oder planen Sie Übersetzungen unabhängig voneinander. Die französische Version kann sich im Entwurf befinden, während die englische Version live ist.

Content-API

Locale-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 standardmäßig die konfigurierte Standard-Locale verwendet.

Erstellen von Übersetzungen über die API

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 sich die translation_group des Quelleintrags und beginnt als Entwurf.

Auflisten von Übersetzungen

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

# Einen bestimmten Eintrag auf Französisch abrufen
emdash content get posts my-post --locale fr

# Eine französische Übersetzung eines vorhandenen Eintrags erstellen
emdash content create posts --locale fr --translation-of 01ABC...

Seeding mehrsprachiger Inhalte

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 vorgefü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 locale-spezifisch 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.

Importieren mehrsprachiger Inhalte

Importieren Sie WordPress-Inhalte über das Admin-Migrationstool — siehe Content-Import und Migration von WordPress. 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 und verknüpfen Sie ihn mit dem Original:

emdash content create posts --locale fr --translation-of 01ABC...

Dies ist derselbe --locale / --translation-of-Workflow, der oben unter Seeding mehrsprachiger Inhalte gezeigt wird, angewendet nach Abschluss des Imports.

Nächste Schritte