Diil Docs
  1. Doku
  2. Anleitungen

Sprachen, der lang-Parameter und Fallbacks

Aktualisiert:

Jeder Text in Diil kann in mehreren Sprachen existieren, und die API liefert sie dir als simple Maps: { "en": "Hello", "de": "Hallo" }. Diese Anleitung zeigt, wie du genau die Sprachen anfragst, die du brauchst, was passiert, wenn eine Übersetzung fehlt, und wie du eine mehrsprachige Website baust, die Besuchern nie eine leere Überschrift zeigt.

Der Parameter lang: eine, mehrere oder alle Sprachen

Jeder Content-Endpoint — Seiten, Sections, Blöcke und der Blog — akzeptiert denselben Query-Parameter lang. Er kürzt nur die Übersetzungen in der Response; die Struktur der Seite bleibt gleich.

langquerystringoptionalStandard: alle aktiven Sprachen
Ein Code (?lang=en), eine kommagetrennte Liste (?lang=en,de) oder ein wiederholter Parameter (?lang=en&lang=de). Bis zu 50 Codes. Jeder muss eine aktive Sprache der Website sein.
Du schickstDu bekommst
nichtsAlle aktiven Sprachen der Website
?lang=deNur Deutsch
?lang=de,enDeutsch und Englisch
?lang=de&lang=enDasselbe, für Libraries, die wiederholte Parameter mögen
curl "https://back.sitecog.com/content/v1/pages/home?lang=de,en" \
  -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"

Welche Sprachen sind aktiv? Frag GET /v1/langs. Der Endpoint liefert sie in der im CRM festgelegten Reihenfolge, und diese Liste ist das Rückgrat von allem, was jetzt kommt.

GET /v1/langsjson
[
  { "key": "en", "title": "English", "index": 0 },
  { "key": "de", "title": "German", "index": 1 },
  { "key": "uk", "title": "Українська", "index": 2 }
]

So sehen Sprach-Maps aus

Überall, wo Text übersetzt werden kann, bekommst du ein Objekt mit Sprachcodes als Keys. Text- und html-Blöcke, Link-Titel, Seitentitel und -beschreibungen, Blog-Titel — alles dieselbe Form.

Ein Text-Block, ?lang=en,de,ukjson
"promo_title": {
  "type": "text",
  "content": { "en": "Free delivery this week", "de": "" }
}

Schau genau hin, in diesem winzigen Objekt stecken drei verschiedene Situationen:

  • en — übersetzt, alles gut.
  • de — jemand hat den deutschen Text geleert, also ist es ein leerer String.
  • uk — wurde nie ausgefüllt, also fehlt der Key einfach.

Blog-Maps sind etwas aufgeräumter: Leere Übersetzungen werden dort komplett weggelassen. So oder so — behandle "" und einen fehlenden Key gleich: „keine Übersetzung“. Alle Formen stehen auf der Seite Block-Typen.

Key-Reihenfolge und ETag-Stabilität

Sprach-Keys kommen immer in der im CRM festgelegten Reihenfolge zurück, egal wie du sie im Request aufzählst. ?lang=de,en und ?lang=en,de liefern byte-genau denselben Body mit demselben ETag — ein 304 Not Modified klappt also, egal wie du es schreibst.

Validierungsfehler

Die API ist bei lang absichtlich streng: Ein Tippfehler soll in der Entwicklung laut scheitern und nicht in Production still eine leere Seite liefern. Alle drei Fehler sind 400 mit einem JSON-Body.

invalid_lang — der Code ist falsch formatiert

Ein Code muss aussehen wie en oder pt-BR: zwei Kleinbuchstaben, optional ein Bindestrich und 2–4 weitere Buchstaben. Groß- und Kleinschreibung zählt, also fällt EN durch.

GET /v1/pages/home?lang=EN → 400json
{ "message": "invalid_lang", "lang": "EN" }

unknown_lang — die Sprache ist auf der Website nicht aktiv

Der Code ist korrekt aufgebaut, aber die Website hat diese Sprache nicht, oder sie ist im CRM abgeschaltet. Der Body verrät dir netterweise, welche Codes unbekannt waren und welche du nutzen kannst.

GET /v1/pages/home?lang=fr → 400json
{
  "message": "unknown_lang",
  "lang": "fr",
  "unknown": ["fr"],
  "available": ["en", "de"]
}

Auch hier zählt die Schreibweise: Ist die Sprache der Website pt-BR, dann besteht pt-br zwar die Formatprüfung, ist aber ein anderer Code — und du bekommst unknown_lang.

too_many_langs — mehr als 50 Codes

400json
{ "message": "too_many_langs", "max": 50 }

Falls du je hier landest, wolltest du lang wahrscheinlich ganz weglassen: kein Parameter heißt alle Sprachen.

Einen serverseitigen Fallback gibt es nicht

Fehlt die deutsche Übersetzung, schiebt die API nicht heimlich die englische an ihre Stelle. Du bekommst genau das, was gespeichert wurde: einen leeren String oder gar keinen Key. Das ist Absicht — nur du weißt, ob deine Website dann Englisch zeigen, den Block ausblenden oder einen Hinweis „noch nicht übersetzt“ anzeigen soll.

Die Kehrseite: Der Fallback ist dein Job. Zum Glück sind das etwa zehn Zeilen.

Empfohlener clientseitiger Fallback

Wir empfehlen diese Reihenfolge, und genau die nutzt jedes Beispiel in dieser Doku:

  1. Die gewünschte Sprache

    Der Besucher ist auf /de/, also zuerst de probieren.
  2. Die Standardsprache der Website

    Die erste Sprache in /v1/langs — die, die der Inhaber im CRM nach oben gesetzt hat.
  3. Irgendeine nicht leere Übersetzung

    Besser irgendwas als nichts. Klappt selbst das nicht: ein leerer String.
lib/langs.tsts
const API = 'https://back.sitecog.com/content';

export type SiteLang = { key: string; title: string; index: number };
export type LangMap = Record<string, string>;

/** Aktive Sprachen in CRM-Reihenfolge; die erste ist die Standardsprache der Website */
export async function getLangs(): Promise<SiteLang[]> {
  const res = await fetch(API + '/v1/langs', {
    headers: { 'x-crm-key': process.env.CRM_KEY! },
    next: { revalidate: 60 },
  });
  if (!res.ok) throw new Error('Content API: ' + res.status);
  return res.json();
}

/** Gewünschte Sprache → Fallback-Sprache → erste nicht leere Übersetzung → '' */
export function t(map: LangMap | null | undefined, lang: string, fallback?: string): string {
  if (!map) return '';
  const own = map[lang];
  if (own) return own;
  const backup = fallback ? map[fallback] : '';
  if (backup) return backup;
  return Object.values(map).find((value) => value !== '') ?? '';
}

/** Wert für ?lang=: Sprache des Besuchers plus Standardsprache, immer in CRM-Reihenfolge */
export function langParam(lang: string, langs: SiteLang[]): string {
  const fallback = langs[0]?.key;
  return langs
    .filter((item) => item.key === lang || item.key === fallback)
    .map((item) => item.key)
    .join(',');
}

Dasselbe t() steckt im universellen Block-Renderer, und pickMedia() wendet dort genau dieselbe Reihenfolge auf sprachspezifische Bilder und Videos an.

Die Sprache des Besuchers erkennen

Der API ist egal, wie du eine Sprache auswählst — sie braucht nur einen gültigen Code. Hier ein Setup, das leicht nachzuvollziehen, suchmaschinenfreundlich und nett zu Besuchern ist.

Die URL hat das letzte Wort

Pack die Sprache in den Pfad: /en/about, /de/about, /uk/about. Jede Sprache bekommt ihre eigene Adresse, Links lassen sich teilen, und Suchmaschinen indexieren jede Version separat. Die Liste gültiger Präfixe ist einfach /v1/langs.

app/[lang]/page.tsxtsx
import { notFound } from 'next/navigation';
import { getLangs, langParam, t } from '@/lib/langs';

export default async function Home({ params }: { params: Promise<{ lang: string }> }) {
  const { lang } = await params;
  const langs = await getLangs();

  // Nie ein rohes URL-Segment an die API geben: /fr/ würde zum 400
  if (!langs.some((item) => item.key === lang)) notFound();
  const fallback = langs[0].key;

  const res = await fetch(
    'https://back.sitecog.com/content/v1/pages/home?lang=' + langParam(lang, langs),
    { headers: { 'x-crm-key': process.env.CRM_KEY! }, next: { revalidate: 60 } },
  );
  const page = await res.json();
  const hero = page.content.hero.content;

  return <h1>{t(hero.hero_title.content, lang, fallback)}</h1>;
}

Die nackte Domain wählt eine Sprache

Nur die Root / muss raten. Prüf in dieser Reihenfolge: die Sprache, die der Besucher schon mal gewählt hat (ein Cookie), das Accept-Language des Browsers und zuletzt die Standardsprache der Website.

lib/langs.ts (Fortsetzung)ts
/** Bester Treffer für den Accept-Language-Header: 'de-AT,de;q=0.9' → 'de' */
export function langFromHeader(header: string, langs: SiteLang[]): string | null {
  const keys = langs.map((item) => item.key);
  for (const part of header.split(',')) {
    const code = part.split(';')[0].trim();
    const base = code.split('-')[0];
    if (keys.includes(code)) return code;
    if (keys.includes(base)) return base;
  }
  return null;
}
app/page.tsxtsx
import { cookies, headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { getLangs, langFromHeader } from '@/lib/langs';

export default async function Root() {
  const langs = await getLangs();
  const saved = (await cookies()).get('lang')?.value;
  const accept = (await headers()).get('accept-language') ?? '';

  const lang =
    langs.find((item) => item.key === saved)?.key ?? // schon mal vom Besucher gewählt
    langFromHeader(accept, langs) ??                  // Vorliebe des Browsers
    langs[0].key;                                     // Standardsprache der Website

  redirect('/' + lang);
}

Der Umschalter schreibt dieses Cookie, sobald jemand eine Sprache von Hand wählt. Die Titel kommen direkt aus dem CRM:

components/LangSwitcher.tsxtsx
'use client';
import { usePathname } from 'next/navigation';
import type { SiteLang } from '@/lib/langs';

export function LangSwitcher({ langs, current }: { langs: SiteLang[]; current: string }) {
  // '/de/about/team' → 'about/team'
  const rest = usePathname().split('/').slice(2).join('/');

  const remember = (lang: string) => {
    document.cookie = 'lang=' + lang + '; path=/; max-age=31536000; samesite=lax';
  };

  return (
    <nav aria-label="Language">
      {langs.map((item) => (
        <a
          key={item.key}
          href={'/' + item.key + (rest ? '/' + rest : '')}
          aria-current={item.key === current ? 'page' : undefined}
          onClick={() => remember(item.key)}
        >
          {item.title}
        </a>
      ))}
    </nav>
  );
}

SEO: hreflang für jede Sprache

Sag Suchmaschinen, dass /en/about und /de/about dieselbe Seite in verschiedenen Sprachen sind. Dann landet jemand, der auf Deutsch sucht, auf der deutschen Version, und die beiden Seiten gelten nicht als Duplikate. Bau die Tags aus /v1/langs, dann taucht eine im CRM hinzugefügte Sprache hier automatisch auf.

Im <head> von /de/abouthtml
<html lang="de">
<head>
  <link rel="alternate" hreflang="en" href="https://example.com/en/about" />
  <link rel="alternate" hreflang="de" href="https://example.com/de/about" />
  <link rel="alternate" hreflang="uk" href="https://example.com/uk/about" />
  <link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
</head>
lib/langs.ts (Fortsetzung)ts
// Codes, die der Inhaber evtl. anders als nach ISO 639-1 eingetragen hat
const HREFLANG_FIX: Record<string, string> = { ua: 'uk' };

export function alternates(path: string, langs: SiteLang[], origin: string) {
  const links = langs.map((item) => ({
    hreflang: HREFLANG_FIX[item.key] ?? item.key,
    href: origin + '/' + item.key + path,
  }));
  if (langs[0]) links.push({ hreflang: 'x-default', href: origin + '/' + langs[0].key + path });
  return links;
}
  • Jede Sprachversion listet alle Versionen auf, auch sich selbst.
  • x-default ist die Version für alle anderen; die Standardsprache der Website ist eine vernünftige Wahl.
  • In Next.js kommt dieselbe Liste in alternates.languages von generateMetadata.
  • Setz auch <html lang> auf die aktuelle Sprache — Screenreader und Übersetzungstools verlassen sich darauf.

Sprachcodes und ein Wort zum Ukrainischen

Die API nutzt genau die Codes, die der Inhaber der Website im CRM eingestellt hat. Zwei Kleinbuchstaben, optional eine Region: en, de, pt-BR. Codier die Liste bei dir nicht hart — lies sie aus /v1/langs, dann ist eine neue Sprache eine CRM-Einstellung statt eines Deploys.