Skip to content

Multi-Language (i18n)

Jeder Space hat eine Liste von Locales. Stories werden pro Locale gespeichert — eine de-AT-Version und eine en-GB-Version sind voneinander unabhängige Story-Reihen mit eigenen full_path und eigenem Content.

Locales pro Space

Eine Locale hat:

  • code — IETF-Tag, z.B. de-AT, en-GB, es-ES, pt-BR
  • label — Anzeigename, z.B. "Deutsch (Österreich)"
  • is_default — genau eine Locale pro Space ist Default
  • fallback_code — Locale, auf die zurückgefallen wird, wenn dieser Content fehlt (z.B. en-GBen)

Default-Setup für neue Spaces (via OnboardingController): eine einzige Locale (Default), die der User beim Signup als default_locale_code angibt (Fallback: en).

API: GET /v1/spaces/{slug}/locales → Liste.

Fallback-Resolution

Wenn das Frontend path=/about mit lang=de-AT anfragt, der Space aber de-AT nicht hat, läuft die Resolution:

  1. Exact matchde-AT selbst → Treffer? Ausliefern, kein Header.
  2. Region-strip — Basis-Tag de existiert → ausliefern + X-Peacock-Locale-Fallback: de-AT -> de.
  3. Sibling-bridge — anderer Regional-Sibling wie de-CH existiert (gleiche Basis) → ausliefern + Header.
  4. Space-Default — keiner der oberen Pfade trifft → space.default_locale_code ausliefern + Header.
  5. Hard 404 — der Space-Default selbst hat keinen Content unter /about → JSON-404 ({"error":"not_found"}).

POLICY (locked-in)

Eine fehlende Übersetzung produziert NIE einen 404. Solange der Space-Default unter dem Pfad publiziert ist, sieht jede Locale-Anfrage Content — die wirkliche Sprache steht in meta.served_lang und im Response-Header X-Peacock-Locale-Fallback. SDK / Embed kann daraus einen "Diese Seite ist nur auf X verfügbar"-Banner rendern, statt mit 404 umzugehen.

Hart 404 gibt es ausschließlich, wenn die URL selbst (/about) gar nicht existiert — nicht wegen einer fehlenden Übersetzung.

Diese Regel ist durch test_POLICY_missing_translation_never_returns_hard_404 gegen Regression abgesichert — bricht laut, wenn jemand am resolveLocaleChain() schraubt.

Dadurch geht eine teilweise übersetzte Site nicht kaputt: solange ein Default existiert, bleibt jede URL bedienbar.

Story pro Locale anlegen

Drei Wege:

1. Direkt via API

bash
curl -X POST "$API/v1/spaces/mysite/stories" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "about",
    "lang": "de-AT",
    "content": { "_component": "page", "headline": "Über uns" }
  }'

2. Via Admin

Dropdown im Topbar wechselt die Locale, dann hat jede Story dieselbe slug/full_path aber unterschiedlichen Content.

3. Via AI-Translation

Im Editor: Klick auf "Translate headline → en-GB" schickt das headline-Feld an die TranslateField-Action, die per Anthropic/OpenAI ein übersetztes Feld zurückgibt und in den Draft mergt. Save → neue Version für en-GB.

Frontend-Nutzung

Astro

astro
---
import { getStory } from '@peacock/sdk-astro';

const lang = Astro.params.lang ?? 'de-AT';
const story = await getStory(Astro.url.pathname, { lang });
---

Multi-Language-Routen via Astro's [lang]/[...slug].astro Dynamic-Segment-Pattern.

Next.js

tsx
const result = await api.cdnStory(SPACE, path, { lang: 'de-AT' });

next-intl (oder ähnliche i18n-Libs) lassen sich vorlagern — die lang-Param fließt unverändert in den Peacock-Call.

Laravel

php
$response = Http::withToken($token)->get(
  "$base/cdn/spaces/$space/stories/by-path",
  ['path' => "/$path", 'lang' => app()->getLocale()],
);

SEO

Die CDN-Antwort enthält pro Story die verfügbaren Übersetzungen:

json
{
  "data": {
    "uuid": "01HF...",
    "lang": "de-AT",
    "alternate_versions": [
      { "lang": "en-GB", "full_path": "/en/about" },
      { "lang": "es-ES", "full_path": "/es/about" }
    ]
  }
}

Das Frontend rendert daraus <link rel="alternate" hreflang="en-GB" href="/en/about" /> für saubere Google-Multi-Language-Indexierung.

RTL-Support

Locales mit rtl: true (z.B. ar, he) liefert die CDN-Antwort mit einem dir: "rtl"-Feld, das Frontends als <html dir="rtl"> rendern können. Peacock selber macht kein Auto-Layout-Mirroring — das liegt am CSS deiner Site.

Was noch fehlt

  • Auto-Translation Webhook: bei story.published automatisch alle anderen Locales übersetzen. Phase 9.
  • Translation-Memory pro Space: gleiche Strings reuse-bar machen. Phase 9.