Next.js App Router-Lokalisierung mit Markdoc
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:
| Ebene | Inhalt | Beispieldatei |
|---|---|---|
| Umfangreiche Inhalte | Marketingseiten, Dokumentation, Blogbeiträge | src/content/en/pages/home.md |
| UI-Strings | Navbar-Labels, CTAs, Button-Status | src/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#
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.
Node.js prüfen
Die CLI setzt Node.js 22 oder höher voraus:
node -vIhr 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.
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:
---
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:
npm install -g @lingo.dev/cli
lingo loginErstelle anschließend das Konfigurationsgerüst und verknüpfe es mit deiner Engine:
lingo init
lingo linklingo 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:
{
"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:
// 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:
// 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:
// 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:
lingo push --backfill-missing --waitBei späteren Durchläufen pushst du dann einfach nur das Delta:
lingo push --waitlingo 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:
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:
lingo checkFügen Sie dies vor Ihrem Next.js-Build als separaten CI-Schritt hinzu:
- name: Verify translations
run: lingo check
env:
LINGO_API_KEY: ${{ secrets.LINGO_API_KEY }}
- name: Build
run: pnpm build