Internationalisierung (i18n)

Auf dieser Seite

EmDash integriert sich mit Astro’s eingebautem i18n-Routing für mehrsprachiges Content-Management. Astro übernimmt URL-Routing und Locale-Erkennung; EmDash speichert und liefert übersetzte Inhalte.

Jede Übersetzung ist ein vollständiger, eigenständiger 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 mit einem i18n-Block in Ihrer Astro-Konfiguration. EmDash liest diese Konfiguration automatisch – es gibt keine separate Locale-Einrichtung in EmDash.

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",
			}),
		}),
	],
});

Ohne i18n in der Astro-Konfiguration sind alle i18n-Funktionen deaktiviert; EmDash verhält sich wie ein einsprachiges CMS.

Wie Übersetzungen funktionieren

EmDash nutzt ein eine Zeile pro Locale-Modell. Jede Übersetzung ist eine eigene Datenbankzeile mit eigener ID, eigenem Slug und Status, verknüpft über eine gemeinsame translation_group.

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

Daraus folgt:

  • Slugs pro Locale/blog/my-post und /fr/blog/mon-article funktionieren natürlich
  • Veröffentlichung pro Locale — Englisch live, Französisch im Entwurf
  • Revisionen pro Locale — jede Übersetzung hat eigenen Verlauf
  • Keine quer-locale Listen-Komplexität — Listen liefern nur Einträge für eine Locale

Übersetzte Inhalte abfragen

Einzelner Eintrag

Übergeben Sie locale an getEmDashEntry, um eine bestimmte Übersetzung zu laden. Wenn weggelassen, gilt die aktuelle Request-Locale (gesetzt durch Astro’s 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 für die angeforderte Locale kein Inhalt existiert, folgt EmDash der in der Astro-Konfiguration definierten Fallback-Kette. Bei fallback: { fr: "en" }:

  1. Angeforderte Locale (fr)
  2. Fallback-Locale (en)
  3. Standard-Locale

Fallback gilt nur für Einzelabfragen. Listen liefern nur Einträge für die angeforderte Locale — kein Mischen über Locales.

Collection-Liste

Collection nach Locale filtern:

---
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

Mit getTranslations bauen Sie einen Sprachumschalter mit Links zu vorhandenen Übersetzungen des aktuellen Eintrags:

---
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>

getTranslations liefert alle Locale-Varianten derselben Übersetzungsgruppe:

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

Mit aktiviertem i18n zeigt die Liste:

  • eine Locale-Spalte pro Eintrag
  • einen Locale-Filter in der Toolbar

Übersetzungen anlegen

Öffnen Sie einen beliebigen Eintrag im Editor. In der Seitenleiste erscheint das Panel Translations. Pro konfigurierter Locale:

  • „Translate“ wenn noch keine Übersetzung existiert — Klick legt eine an
  • „Edit“ wenn eine Übersetzung existiert — Klick springt dorthin
  • Die aktuelle Locale ist mit einem Häkchen markiert

Beim Anlegen wird der neue Eintrag aus der Quell-Locale vorbefüllt; Standard-Slug {source-slug}-{locale}. Slug und Inhalt anpassen, dann speichern.

Veröffentlichung pro Locale

Jede Übersetzung hat einen eigenen Status. Veröffentlichen, zurückziehen oder planen Sie unabhängig. Französisch kann Entwurf sein, während Englisch live ist.

Content-API

Locale-Parameter

Alle Content-API-Routen akzeptieren optional locale als Query-Parameter:

GET /_emdash/api/content/posts?locale=fr
GET /_emdash/api/content/posts/my-post?locale=fr

Ohne Angabe gilt die konfigurierte Standard-Locale.

Übersetzungen per API anlegen

Übersetzung mit locale und translationOf am Create-Endpunkt:

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 der Quelle und startet als Entwurf.

Übersetzungen auflisten

Alle Übersetzungen zu einem Eintrag:

GET /_emdash/api/content/posts/01ABC.../translations

Antwort: Übersetzungsgruppen-ID und Array der Locale-Varianten mit ID, Slug und Status.

CLI

Die CLI unterstützt --locale bei Content-Befehlen:

# List French posts
emdash content list posts --locale fr

# Get a specific entry in French
emdash content get posts my-post --locale fr

# Create a French translation of an existing entry
emdash content create posts --locale fr --translation-of 01ABC...

Mehrsprachige Inhalte seeden

Seed-Dateien nutzen locale und translationOf:

{
  "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-Eintrag muss in der Seed-Datei vor seinen Übersetzungen stehen, damit translationOf aufgelöst wird.

Feld-Übersetzbarkeit

Jedes Feld hat translatable (Standard: true). Beim Anlegen einer Übersetzung:

  • Übersetzbare Felder werden aus der Quell-Locale zum Bearbeiten vorbefüllt
  • Nicht übersetzbare Felder werden kopiert und über alle Varianten der Gruppe synchron gehalten

Systemfelder wie status, published_at und author_id sind immer pro Locale und werden nicht synchronisiert.

URL-Strategie

EmDash verwaltet keine Locale-URLs — das übernimmt Astro. Übliche Muster:

# prefix-other-locales (Astro default)
/blog/my-post          → en (default locale, no prefix)
/fr/blog/mon-article   → fr

# prefix-always
/en/blog/my-post       → en
/fr/blog/mon-article   → fr

Nutzen Sie getRelativeLocaleUrl aus astro:i18n für korrekte URLs unabhängig vom Routing-Modus.

Mehrsprachige Inhalte importieren

WordPress mit WPML oder Polylang

Die WordPress-Importquelle erkennt WPML und Polylang automatisch. Importierte Inhalte enthalten dann Locale und Übersetzungsgruppe.

WXR-Dateien

WXR-Exporte enthalten keine WPML/Polylang-Metadaten. Import als eine Locale und Übersetzungen manuell, oder --locale, um allen importierten Elementen eine Locale zuzuweisen:

# Import a French WXR export
emdash import wordpress export-fr.xml --execute --locale fr

# Match against existing English content by slug
emdash import wordpress export-fr.xml --execute --locale fr --translation-of-locale en

Nächste Schritte