Next.js App Router-Lokalisierung mit Markdoc

Zuletzt aktualisiert: vor 6 Tagen · 6 Min. Lesezeit

Die Lingo.dev CLI übersetzt Markdoc-Dateien und JSON-Kataloge mit UI-Strings über eine konfigurierte Lokalisierungs-Engine. Markdoc ist ein Markdown-basiertes Authoring-Format mit typisierten, React-gestützten Custom Tags – ideal für Next.js App Router-Websites, die umfangreiche Inhalte mit interaktiven Komponenten verbinden.

Dieser Leitfaden zeigt dir Schritt für Schritt, wie du eine Next.js App Router-Website vollständig lokalisierst: von der CLI-Konfiguration über die Organisation sprachspezifischer Inhalte und das Rendern von Markdoc in dynamischen Routen bis hin zur Automatisierung der Übersetzungen mit der Lingo.dev GitHub App.

Demo-Repository

Klone oder forke lingodotdev/markdoc-nextjs-localization-example, um direkt mitzumachen. Das Repository enthält eine lauffähige Next.js App Router-App mit Markdoc-Inhalten, einer Lingo.dev CLI-Konfiguration und einem CI-Workflow.

So funktioniert die Lokalisierung mit Next.js + Markdoc#

Die meisten Next.js App Router-Websites unterteilen lokalisierte Inhalte in zwei Ebenen:

EbeneInhaltBeispieldatei
Umfangreiche InhalteMarketingseiten, Dokumentation, Blogbeiträgesrc/content/en/pages/home.md
UI-StringsNavbar-Labels, CTAs, Button-Statussrc/content/en/ui.json

Die Routen liegen unter src/app/[lang]/ und lesen zur Laufzeit die Dateien der passenden Sprache. Eine Middleware wählt anhand des Accept-Language-Headers des Browsers eine Standardsprache aus und leitet einfache Pfade wie / zu /en weiter – oder zur jeweils besten Übereinstimmung.

Die CLI verarbeitet Markdoc-Dateien, ohne Frontmatter oder benutzerdefinierte Tags anzutasten, und behandelt den UI-String-Katalog als JSON. In beiden Fällen wird das Delta über deine Lokalisierungs-Engine übersetzt und als sprachspezifische Dateien neben den Quelldateien abgelegt.

Voraussetzungen#

1

Eine Lokalisierungs-Engine erstellen

Bei jedem CLI-Durchlauf laufen Inhalte durch eine Lokalisierungs-Engine – die Konfiguration, die festlegt, welches LLM-Modell, Glossar, welche Markenstimme und welche Regeln verwendet werden. Erstelle sie im Lingo.dev dashboard und generiere einen API key für CI.

2

Node.js prüfen

Die CLI setzt Node.js 22 oder höher voraus:

bash
node -v
3

Ihr Next.js-Projekt einrichten

Dein Projekt braucht den App Router (src/app/) und ein Inhaltsverzeichnis pro Sprache. Das Demo-Repository verwendet unter src/content/ ein Verzeichnis je Sprache (zum Beispiel src/content/en/) mit zwei Unterordnern (pages/ und blog/) sowie einer ui.json-Datei. Die Grundlagen zum Routing findest du unter Next.js internationalization.

Inhalte organisieren#

Teilen Sie Inhalte nach ihrer Funktion auf. Umfangreiche Seiten und Beiträge werden in Markdoc verfasst, kurze UI-Strings liegen in JSON, damit Komponenten sie direkt laden können.

text
src/content/
  en/                  # Source locale
    pages/home.md      # Long-form Markdoc
    blog/hello.md
    ui.json            # UI strings (navbar, CTAs, button states)
  es/                  # Target locales – generated by Lingo.dev
  fr/
  de/

Markdoc-Dateien unterstützen Frontmatter für seitenbezogene Metadaten wie Titel, Beschreibung, Datum und Autor sowie Custom Tags, die als React-Komponenten gerendert werden. Eine minimale Seite sieht so aus:

markdown
---
title: Author once in Markdoc, ship in every language.
description: An example Next.js App Router app that localizes Markdoc with Lingo.
---

{% inline-callout type="info" %}
This page is authored in Markdoc and translated by Lingo.dev.
{% /inline-callout %}

## Built from three pieces

Markdoc custom tags render as React components – even interactive ones.

Die CLI konfigurieren#

Installiere die CLI und melde dich an:

bash
npm install -g @lingo.dev/cli
lingo login

Erstelle anschließend das Konfigurationsgerüst und verknüpfe es mit deiner Engine:

bash
lingo init
lingo link

lingo init erstellt .lingo/config.json mit deiner Quell- und deinen Zielsprachen sowie den Dateimustern für die Übersetzung; lingo link ergänzt deinen orgId und engineId. Committe .lingo/config.json, damit dein Team und alle CI-Läufe dieselbe Konfiguration nutzen.

Für dieses Projekt definiert die Konfiguration zwei Dateimuster – eines für Markdoc-Inhalte und eines für den UI-String-Katalog:

json
{
  "orgId": "org_...",
  "engineId": "eng_...",
  "sourceLocale": "en",
  "targetLocales": ["es", "fr", "de"],
  "files": [
    { "pattern": "src/content/en/pages/*.md" },
    { "pattern": "src/content/en/blog/*.md" },
    { "pattern": "src/content/en/ui.json" }
  ]
}

Das Sprachsegment in jedem Pfad wird für jede Zielsprache ersetzt: src/content/en/pages/home.md wird zu src/content/es/pages/home.md, und src/content/en/ui.json wird zu src/content/de/ui.json. Der Quellpfad muss den Sprachcode enthalten. Die Formate werden anhand der Dateiendung automatisch erkannt, daher brauchen Markdoc-Dateien (.md) und JSON-Dateien (.json) keinen expliziten Typ. Details findest du unter Configuration und Formats.

Einzeldatei-Kataloge

Die neue CLI erwartet eine Datei pro Sprache, wobei der Sprachcode im Pfad enthalten ist (wie oben). Wenn deine UI-Strings in einer einzigen mehrsprachigen JSON-Datei liegen, wird dieses Layout (der frühere json-per-locale-Bucket) von der neuen CLI noch nicht unterstützt – bleib in dem Fall bei der legacy CLI und verfolge das Changelog, bis Support verfügbar ist. Empfohlen ist die Aufteilung in eine Datei pro Sprache.

Markdoc im App Router rendern#

Eine typische dynamische Route lädt ein Dokument und rendert den transformierten Baum. Das Demo-Repository stellt dafür einen kleinen Helper bereit:

ts
// src/lib/markdoc.ts
export async function loadDoc(
  locale: Locale,
  collection: "pages" | "blog",
  slug: string,
) {
  const raw = await fs.readFile(
    path.join(process.cwd(), "src/content", locale, collection, `${slug}.md`),
    "utf8",
  );
  const ast = Markdoc.parse(raw);
  const frontmatter = ast.attributes.frontmatter
    ? parseFrontmatter(ast.attributes.frontmatter)
    : {};
  const content = Markdoc.transform(ast, { ...schema, variables: { frontmatter } });
  return { frontmatter, content };
}

Die App Router-Seite ist ein schlanker Wrapper, der das Dokument mit sprachspezifischen UI-Strings kombiniert:

tsx
// src/app/[lang]/page.tsx
export default async function Home({ params }: PageProps<"/[lang]">) {
  const { lang } = await params;
  const doc = await loadDoc(lang, "pages", "home");
  const { home } = await getMessages(lang);

  return (
    <main>
      <h1>{doc.frontmatter.title}</h1>
      {renderMarkdoc(doc.content)}
    </main>
  );
}

Custom Tags in Markdoc (callout, bento, blog-hero usw.) werden in markdoc.schema.ts deklariert und mit React-Komponenten unter src/components/markdoc/ verknüpft. Die vollständige API finden Sie in den Markdoc schema docs.

Sprache in der Middleware erkennen#

Die Next.js-Middleware prüft die Anfrage, bevor eine Route gerendert wird. Verwenden Sie sie, um einfache Pfade anhand des Accept-Language-Headers zur am besten passenden Sprache weiterzuleiten:

ts
// src/middleware.ts
export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const hasLocale = locales.some(
    (locale) => pathname === `/${locale}` || pathname.startsWith(`/${locale}/`),
  );
  if (hasLocale) return;

  const locale = pickLocale(request); // parses Accept-Language
  const url = request.nextUrl.clone();
  url.pathname = `/${locale}${pathname === "/" ? "" : pathname}`;
  return NextResponse.redirect(url);
}

export const config = {
  matcher: ["/((?!_next|api|.*\\..*).*)", ],
};

Besucher landen auf /en, /es, /fr oder /de, ohne das Präfix selbst eingeben zu müssen.

Lokal übersetzen#

Führe nach lingo login einen Push aus. Beim ersten Durchlauf – oder nachdem du eine neue Zielsprache hinzugefügt hast – füllst du zunächst alles auf:

bash
lingo push --backfill-missing --wait

Bei späteren Durchläufen pushst du dann einfach nur das Delta:

bash
lingo push --wait

lingo push --wait liest alle Dateien, die Ihren Mustern entsprechen, erkennt mithilfe der Lock-Datei (.lingo/lock.json, versioniert) unübersetzte Einträge, übersetzt das Delta über Ihre Lokalisierungs-Engine, wartet auf den Abschluss und schreibt die Ergebnisse in das Verzeichnis jeder Zielsprache. Frontmatter-Schlüssel, benutzerdefinierte Markdoc-Tags und JSON-Strukturen bleiben erhalten – nur der übersetzbare Text ändert sich. Übersetzungen, die in CI erstellt werden, landen in Ihrem Checkout, sobald sie committet sind (git pull); lingo pull erfasst nur den letzten Lauf, der aus diesem Checkout gepusht wurde.

Um einen Durchlauf auf bestimmte Dateien einzugrenzen, übergib ein Glob-Muster:

bash
lingo push "src/content/en/blog/*.md"

In CI automatisieren#

Installiere die Lingo.dev GitHub App und verbinde sie mit deinem Repository. Sie liest .lingo/config.json und die verknüpfte engineId serverseitig aus und öffnet bei jeder Änderung der Quellinhalte automatisch einen Übersetzungs-Pull-Request – ganz ohne Workflow-Datei, Runner, API-Key-Secret oder manuelles Jonglieren mit Lock-Dateien.

Vor dem Deployment prüfen#

Verwende lingo check als Deployment-Gate, damit keine unübersetzten Inhalte in Produktion gehen. Der Befehl endet mit einem Nicht-Null-Status, wenn noch Einträge übersetzt werden müssen:

bash
lingo check

Fügen Sie dies vor Ihrem Next.js-Build als separaten CI-Schritt hinzu:

yaml
- name: Verify translations
  run: lingo check
  env:
    LINGO_API_KEY: ${{ secrets.LINGO_API_KEY }}
- name: Build
  run: pnpm build

Nächste Schritte#