Theme
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-BRlabel— Anzeigename, z.B. "Deutsch (Österreich)"is_default— genau eine Locale pro Space ist Defaultfallback_code— Locale, auf die zurückgefallen wird, wenn dieser Content fehlt (z.B.en-GB→en)
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:
- Exact match —
de-ATselbst → Treffer? Ausliefern, kein Header. - Region-strip — Basis-Tag
deexistiert → ausliefern +X-Peacock-Locale-Fallback: de-AT -> de. - Sibling-bridge — anderer Regional-Sibling wie
de-CHexistiert (gleiche Basis) → ausliefern + Header. - Space-Default — keiner der oberen Pfade trifft →
space.default_locale_codeausliefern + Header. - 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.publishedautomatisch alle anderen Locales übersetzen. Phase 9. - Translation-Memory pro Space: gleiche Strings reuse-bar machen. Phase 9.