Zum Inhalt springen

Übersetzungssystem

Diese Website wird in 17 Sprachen veröffentlicht. Nicht von einem Übersetzerteam, sondern durch ein Python-Skript, ein Claude Opus-Modell und eine Dateistruktur, bei der alles nahtlos ineinandergreift. So funktioniert das Ganze.

Die Dokumentation befindet sich in src/content/docs/. Englisch ist die Wurzel — jede andere Sprache spiegelt sie exakt wider:

src/content/docs/
├── index.mdx ← English (root)
├── getting-started.md
├── features/
│ ├── play-chess.md
│ ├── multiplayer.md
│ └── ...
├── setup/
│ ├── tts-overview.md
│ └── ...
├── under-the-hood/
│ ├── architecture.md
│ └── ...
├── fr/ ← French
│ ├── index.mdx
│ ├── getting-started.md
│ ├── features/
│ │ ├── play-chess.md
│ │ └── ...
│ └── ...
├── ja/ ← Japanese
│ ├── index.mdx
│ ├── getting-started.md
│ └── ...
└── ... (16 language directories total)

Jede übersetzte Datei ist eine strukturelle Spiegelung ihrer englischen Quelle. Gleicher Dateiname, gleicher Unterverzeichnispfad, gleiche Frontmatter-Schlüssel. Der einzige Unterschied ist, dass der Fließtext in einer anderen Sprache verfasst ist.

Starlight (das Dokumentations-Framework) basiert auf dieser Symmetrie. Wenn ein Benutzer die Sprache wechselt, ersetzt Starlight /docs/getting-started/ durch /fr/docs/getting-started/ — gleicher Pfad, anderes Locale-Präfix. Wenn die französische Datei nicht exakt unter fr/getting-started.md existiert, funktioniert der Sprachwechsler nicht oder fällt stillschweigend auf Englisch zurück.

Die Sprachen sind nach der weltweiten schachspielenden Bevölkerung sortiert, basierend auf Daten von Lichess, Chess.com und FIDE-Registrierungen. Englisch steht als Quellsprache an erster Stelle; alles andere folgt der Schach-Rangliste:

RangCodeSpracheStil
1enEnglischReferenzquelle
2esSpanischStandard formal
3hiHindiDevanagari-Schrift
4ruRussischStandard formal
5deDeutschStandard formal
6frFranzösischStandard formal
7ptPortugiesischEuropäisches Portugiesisch
11plPolnischStandard formal
12itItalienischStandard formal
13ukUkrainischStandard formal
14trTürkischStandard formal
17koKoreanisch합니다/습니다-Form
18zhChinesisch (Vereinfacht)Vereinfachte Zeichen
zh-twChinesisch (Traditionell)Traditionelle Zeichen
23nbNorwegisch BokmålStandard Bokmål
beBelarussischStandard Belarussisch
34jaJapanischです/ます-Form

Die Spalte „Stil” ist wichtig. Japanisch und Koreanisch haben formelle Registeroptionen, die jeden Satz betreffen. Die Übersetzungsanweisung enthält diese Vorgaben, damit das Modell natürlichen, ausgefeilt formulierten Text produziert — keine steife Maschinenübersetzung.

Diese Reihenfolge bestimmt auch das Sprach-Dropdown in der Kopfzeile der Website. Die meistgesprochenen Schachsprachen erscheinen zuerst, sodass Benutzer ihre Sprache eher finden, ohne scrollen zu müssen.

Alle Dokumentation beginnt als englisches Markdown in src/content/docs/. Das Frontmatter enthält einen title und eine description:

---
title: "Getting Started"
description: "Install En Parlant~ and play your first game."
---
Download the latest release...

Ein Python-Skript (scripts/translate-docs.py) liest jede englische Quelldatei, sendet sie an die Claude API und schreibt das übersetzte Markdown:

Terminal-Fenster
python3 scripts/translate-docs.py \
--anthropic-key $ANTHROPIC_API_KEY \
--model claude-opus-4-6 \
--workers 5

Das Skript benötigt etwa 60–70 Minuten, um alle 28 Quelldateien in alle 16 Zielsprachen zu übersetzen (insgesamt 448 Dateien). Es führt 5 parallele API-Aufrufe durch, um innerhalb der Ratenlimits zu bleiben.

Für eine einzelne neue Sprache dauert es etwa 4 Minuten.

Terminal-Fenster
pnpm build

Astro liest das Quell-Markdown, rendert es durch die Starlight-Templates und gibt statisches HTML in dist/ aus. Der Build dauert etwa 30 Sekunden für alle ~500 Seiten.

Terminal-Fenster
pnpm run deploy

Überträgt die gebaute Website auf Cloudflare Workers.

Das Übersetzungsskript ist bewusst einfach gehalten — etwa 300 Zeilen Python. Hier ist der Ablauf:

┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Read English │────▶│ Claude API │────▶│ Write target │
│ source file │ │ (Opus 4.6) │ │ language file │
│ │ │ │ │ │
│ getting- │ │ "Translate into │ │ fr/getting- │
│ started.md │ │ French..." │ │ started.md │
└─────────────────┘ └──────────────────┘ └─────────────────┘
× 5 parallel
× 16 languages
× 28 files

Die Anweisung teilt Claude genau mit, was übersetzt und was beibehalten werden soll:

  • Übersetzen: Fließtext, Überschriften, Tabellenbeschriftungen, Frontmatter-Titel und -Beschreibung
  • Auf Englisch belassen: Codeblöcke, Inline-Code, Befehlsbeispiele, Dateipfade, URLs, Produktnamen, Personennamen, ASCII-Diagramme

Diese Trennung ist entscheidend. Ein Befehl wie pnpm build muss in jeder Sprache pnpm build bleiben. Ein Produktname wie „En Parlant~” oder „Stockfish” bleibt unübersetzt. Aber „Getting Started” wird im Japanischen zu „はじめに” und im Russischen zu „Начало работы”.

Claude Opus 4.6 produziert spürbar bessere Übersetzungen als schnellere Modelle. Der Unterschied zeigt sich in:

  • Natürliche Formulierung — Opus schreibt wie ein Muttersprachler, nicht wie ein Übersetzer. Es strukturiert Sätze um, wenn die englische Wortstellung in der Zielsprache unnatürlich klingen würde.
  • Fachliche Genauigkeit — Schachterminologie, TTS-Fachbegriffe und Software-Konzepte werden mit den korrekten fachspezifischen Begriffen übersetzt.
  • Konsistenz — Das formelle Register bleibt durchgehend einheitlich. Japanisch verwendet überall die です/ます-Form, ohne mitten im Absatz zwischen umgangssprachlich und höflich zu wechseln.
  • MDX-Handling — Opus bewahrt JSX-Komponenten-Tags (<Card>, <CardGrid>) und import-Anweisungen in .mdx-Dateien korrekt, ohne sie zu beschädigen.

Der Kostenunterschied ist real — etwa 28 $ für eine vollständige Website-Übersetzung mit Opus gegenüber 5 $ mit Sonnet — aber für 448 Dateien, die Benutzer tatsächlich lesen werden, ist die Qualität es wert.

Dies war der Teil, der am längsten gedauert hat, bis er richtig funktionierte.

Starlight erkennt die Sprache einer Seite anhand des ersten Segments ihres Content-Slugs. Wenn es fr/docs/getting-started sieht, weiß es, dass das Französisch ist, weil fr das erste Segment ist. Aber die ursprüngliche Implementierung erzeugte Slugs wie docs/fr/getting-started — das Locale war unter docs/ vergraben. Starlight sah docs als erstes Segment, behandelte alles als Englisch und generierte über 7.000 doppelte Seiten statt ~500.

Eine benutzerdefinierte generateId-Funktion in src/content.config.ts steuert, wie Dateipfade zu Content-Slugs werden:

generateId({ entry }) {
const slug = entry.replace(/\.[^.]+$/, "");
const firstSeg = slug.split("/")[0];
if (firstSeg && localeKeys.includes(firstSeg)) {
return `${firstSeg}/docs/${slug.slice(firstSeg.length + 1)}`;
}
return `docs/${slug}`;
}

Dies setzt das Locale-Präfix vor docs/:

DateipfadSlugURL
getting-started.mddocs/getting-started/docs/getting-started/
fr/getting-started.mdfr/docs/getting-started/fr/docs/getting-started/
ja/features/puzzles.mdja/docs/features/puzzles/ja/docs/features/puzzles/

Englisch verwendet defaultLocale: "root", was bedeutet, dass es kein Präfix gibt — es lebt direkt unter /docs/, nicht unter /en/docs/.

Ein localeKeys-Array in derselben Datei muss jedes nicht-englische Locale auflisten. Wenn ein Locale in Astros Konfiguration existiert, aber nicht in diesem Array, wird dessen übersetzter Inhalt als englischer Inhalt behandelt — der Sprachwechsler funktioniert nicht mehr und die Seitenzahlen explodieren.

Astro speichert Slug-Zuordnungen in .astro/data-store.json im Cache. Nach jeder Änderung der Locale-Konfiguration muss diese Datei vor dem Neuaufbau gelöscht werden, da der Build sonst mit veralteten (falschen) Routing-Daten erfolgreich durchläuft.

Die Seitenleiste wird in astro.config.mjs definiert. Einträge, die auf eine Seite verweisen (slug-Eigenschaft), verwenden automatisch den übersetzten Frontmatter-Titel der Seite — keine manuelle Übersetzung nötig:

{ slug: "docs/getting-started" }
// English: "Getting Started" (from English frontmatter)
// French: "Premiers pas" (from French frontmatter)
// Japanese: "はじめに" (from Japanese frontmatter)

Aber Gruppenbezeichnungen und externe Links benötigen explizite Übersetzungen:

{
label: "Features",
translations: {
fr: "Fonctionnalités",
es: "Características",
de: "Funktionen",
ja: "機能",
// ... all 16 languages
},
items: [
{ slug: "docs/features/play-chess" },
{ slug: "docs/features/multiplayer" },
// ...
],
}

Sieben Seitenleistenelemente benötigen manuelle Übersetzungen: Welcome, Features, App Menus, Setup Guides, Under the Hood, Credits und Accessibility. Das Hinzufügen einer neuen Sprache bedeutet, jeweils einen Eintrag zu diesen sieben Blöcken hinzuzufügen.

En Parlant~ verlinkt aus seinem Hilfe-Menü auf diese Dokumentation. Der Link ist locale-sensitiv — wenn Sie die App auf Französisch verwenden, öffnet sich die französische Dokumentation:

const docsLocalePrefix = useMemo(() => {
const lang = i18n.language; // e.g. "fr_FR", "zh_TW"
if (!lang || lang.startsWith("en")) return "";
if (lang === "zh_TW") return "/zh-tw";
return `/${lang.slice(0, 2)}`;
}, [i18n.language]);

Jede in der App verfügbare Sprache hat eine passende Übersetzung auf der Website. Die Zuordnung erfolgt automatisch — fr_FR wird auf /fr/docs/ abgebildet, ja_JP auf /ja/docs/. Der einzige Sonderfall ist Traditionelles Chinesisch: zh_TW wird auf /zh-tw/docs/ abgebildet (mit Bindestrich).

Die schwierigen Teile sind erledigt:

  1. Die Routing-Architektur ist gelöst. Die generateId-Funktion, das localeKeys-Array und die defaultLocale: "root"-Konfiguration arbeiten zusammen, damit Starlight die korrekte URL-Struktur generiert. Dies war der größte Schmerzpunkt — es erforderte die Analyse von über 6 Quelldateien in Starlight und Astro, um das Problem zu finden und zu beheben.

  2. Das Übersetzungsskript erledigt alles. Vollständige Neuübersetzung der Website, Hinzufügen einer einzelnen Sprache, Aktualisierung einzelner Dateien — alles mit demselben Skript und unterschiedlichen Flags. Es wiederholt bei Ratenlimits, parallelisiert über Worker und meldet Fehler klar.

  3. Eine neue Sprache hinzuzufügen erfordert vier Konfigurationsänderungen und einen Befehl. Das Locale in astro.config.mjs, content.config.ts und translate-docs.py hinzufügen, Seitenleisten-Übersetzungen ergänzen, das Skript ausführen. Etwa 10 Minuten Arbeit plus 4 Minuten Übersetzungszeit.

  4. Inhalte aktualisieren ist noch einfacher. Die englische Quelle bearbeiten, das Skript mit --files und --overwrite nur für die geänderten Dateien ausführen, neu bauen. Oder bei kleinen Textänderungen die übersetzten Dateien direkt bearbeiten.

  5. Die gesamte Pipeline ist in einem Skill festgehalten. Das Ausführen von /translate_docs führt durch den gesamten Prozess — welcher Modus verwendet werden soll, welche Flags übergeben werden, Vorab-Prüfungen, Nachübersetzungs-Verifizierung. Kein institutionelles Wissen erforderlich.

Die Gesamtübersetzung von 448 Dateien in 16 Sprachen kostete etwa 28 $ und dauerte ungefähr 70 Minuten. Eine einzelne neue Sprache kostet etwa 1,70 $. Eine einzelne Datei, die in alle Sprachen neu übersetzt wird, kostet etwa 1 $. Das sind die laufenden Kosten, um die Dokumentation in 17 Sprachen aktuell zu halten.