Przejdź do głównej zawartości

System tłumaczeń

Ta strona jest publikowana w 17 językach. Nie przez zespół tłumaczy, lecz za pomocą skryptu Python, modelu Claude Opus i struktury plików zaprojektowanej tak, aby wszystko pasowało na swoje miejsce. Oto jak to całe rozwiązanie działa.

Dokumentacja znajduje się w katalogu src/content/docs/. Angielski jest wersją źródłową — każdy inny język odzwierciedla go dokładnie:

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)

Każdy przetłumaczony plik jest strukturalnym lustrzanym odbiciem swojego angielskiego źródła. Ta sama nazwa pliku, ta sama ścieżka podkatalogu, te same klucze frontmatter. Jedyna różnica to tekst w innym języku.

Starlight (framework dokumentacji) opiera się na tej symetrii. Gdy użytkownik przełącza język, Starlight zamienia /docs/getting-started/ na /fr/docs/getting-started/ — ta sama ścieżka, inny prefiks lokalizacji. Jeśli plik francuski nie istnieje dokładnie pod ścieżką fr/getting-started.md, przełącznik języków się psuje lub po cichu wraca do wersji angielskiej.

Języki są uporządkowane według globalnej populacji graczy szachowych, na podstawie danych z Lichess, Chess.com oraz rejestracji FIDE. Angielski jest na pierwszym miejscu jako język źródłowy; reszta podąża za rankingami szachowymi:

PozycjaKodJęzykStyl
1enAngielskiWersja źródłowa
2esHiszpańskiStandardowy formalny
3hiHindiPismo dewanagari
4ruRosyjskiStandardowy formalny
5deNiemieckiStandardowy formalny
6frFrancuskiStandardowy formalny
7ptPortugalskiPortugalski europejski
11plPolskiStandardowy formalny
12itWłoskiStandardowy formalny
13ukUkraińskiStandardowy formalny
14trTureckiStandardowy formalny
17koKoreańskiforma 합니다/습니다
18zhChiński (uproszczony)Znaki uproszczone
zh-twChiński (tradycyjny)Znaki tradycyjne
23nbNorweski BokmålStandardowy Bokmål
beBiałoruskiStandardowy białoruski
34jaJapońskiforma です/ます

Kolumna „Styl” ma znaczenie. Japoński i koreański mają wybór rejestru formalnego, który wpływa na każde zdanie. Prompt tłumaczenia zawiera te instrukcje, dzięki czemu model generuje naturalną, dopracowaną prozę — a nie sztywny maszynowy tekst.

Ta kolejność kontroluje również listę rozwijaną języków w nagłówku strony. Języki najczęściej używane przez szachistów pojawiają się jako pierwsze, więc użytkownicy mają większą szansę znaleźć swój język bez przewijania.

Cała dokumentacja zaczyna się jako angielski markdown w src/content/docs/. Frontmatter zawiera pola title i description:

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

Skrypt Python (scripts/translate-docs.py) odczytuje każdy angielski plik źródłowy, wysyła go do API Claude i zapisuje przetłumaczony markdown:

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

Skrypt potrzebuje około 60–70 minut na przetłumaczenie wszystkich 28 plików źródłowych na 16 języków docelowych (łącznie 448 plików). Uruchamia 5 równoległych wywołań API, aby mieścić się w limitach zapytań.

Dla pojedynczego nowego języka zajmuje to około 4 minuty.

Okno terminala
pnpm build

Astro odczytuje źródłowy markdown, renderuje go przez szablony Starlight i generuje statyczny HTML do katalogu dist/. Budowanie zajmuje około 30 sekund dla wszystkich ~500 stron.

Okno terminala
pnpm run deploy

Publikuje zbudowaną stronę na Cloudflare Workers.

Skrypt tłumaczenia jest celowo prosty — około 300 linii Pythona. Oto przebieg działania:

┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ 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

Prompt mówi Claude dokładnie, co tłumaczyć, a co zostawić bez zmian:

  • Tłumaczyć: prozę, nagłówki, etykiety tabel, frontmatter title i description
  • Zostawić po angielsku: bloki kodu, kod inline, przykłady poleceń, ścieżki plików, adresy URL, nazwy produktów, imiona i nazwiska osób, diagramy ASCII

Ten podział jest kluczowy. Polecenie takie jak pnpm build musi pozostać pnpm build w każdym języku. Nazwa produktu jak „En Parlant~” czy „Stockfish” nie jest tłumaczona. Ale „Getting Started” staje się „はじめに” po japońsku i „Начало работы” po rosyjsku.

Claude Opus 4.6 generuje zauważalnie lepsze tłumaczenia niż szybsze modele. Różnica widoczna jest w:

  • Naturalne sformułowania — Opus pisze jak rodzimy użytkownik języka, nie jak tłumacz. Przebudowuje zdania, gdy angielski szyk wyrazów brzmiałby niezręcznie w języku docelowym.
  • Dokładność terminologiczna — Terminologia szachowa, żargon TTS i pojęcia programistyczne są tłumaczone z użyciem właściwych terminów dziedzinowych.
  • Spójność — Formalny rejestr jest utrzymany konsekwentnie w całym tekście. Japoński używa formy です/ます wszędzie, bez przeskakiwania między stylem potocznym a grzecznościowym w środku akapitu.
  • Obsługa MDX — Opus poprawnie zachowuje tagi komponentów JSX (<Card>, <CardGrid>) i instrukcje import w plikach .mdx, nie uszkadzając ich.

Różnica w kosztach jest realna — około 28$ za pełne tłumaczenie strony z Opus w porównaniu z 5$ przy użyciu Sonnet — ale dla 448 plików, które użytkownicy faktycznie będą czytać, jakość jest tego warta.

To część, która wymagała najwięcej czasu, aby działała poprawnie.

Starlight wykrywa język strony, sprawdzając pierwszy segment sluga treści. Gdy widzi fr/docs/getting-started, wie, że to francuski, ponieważ fr jest pierwszym segmentem. Jednak początkowa implementacja generowała slugi typu docs/fr/getting-started — lokalizacja zagrzebana pod docs/. Starlight widział docs jako pierwszy segment, traktował wszystko jako angielski i generował ponad 7000 zduplikowanych stron zamiast ~500.

Niestandardowa funkcja generateId w src/content.config.ts kontroluje, jak ścieżki plików stają się slugami treści:

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}`;
}

To umieszcza prefiks lokalizacji przed docs/:

Ścieżka plikuSlugURL
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/

Angielski używa defaultLocale: "root", co oznacza brak prefiksu — strona znajduje się bezpośrednio pod /docs/, nie pod /en/docs/.

Tablica localeKeys w tym samym pliku musi zawierać listę wszystkich nieanglojęzycznych lokalizacji. Jeśli lokalizacja istnieje w konfiguracji Astro, ale nie w tej tablicy, przetłumaczona treść jest traktowana jako treść anglojęzyczna — przełącznik języków się psuje i liczba stron eksploduje.

Astro buforuje mapowania slugów w pliku .astro/data-store.json. Po jakiejkolwiek zmianie konfiguracji lokalizacji ten plik musi zostać usunięty przed ponownym budowaniem, w przeciwnym razie budowanie zakończy się sukcesem z nieaktualnymi (błędnymi) trasami.

Pasek boczny jest zdefiniowany w astro.config.mjs. Elementy wskazujące na stronę (właściwość slug) automatycznie używają przetłumaczonego tytułu z frontmatter strony — nie trzeba ręcznie tłumaczyć:

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

Ale etykiety grup i linki zewnętrzne wymagają jawnych tłumaczeń:

{
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" },
// ...
],
}

Siedem elementów paska bocznego wymaga ręcznych tłumaczeń: Welcome, Features, App Menus, Setup Guides, Under the Hood, Credits i Accessibility. Dodanie nowego języka oznacza dodanie jednego wpisu do każdego z tych siedmiu bloków.

En Parlant~ linkuje do tej dokumentacji ze swojego menu Pomoc. Link jest świadomy lokalizacji — jeśli używasz aplikacji po francusku, otwiera francuską dokumentację:

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]);

Każdy język dostępny w aplikacji ma odpowiadające mu tłumaczenie na stronie. Mapowanie jest automatyczne — fr_FR mapuje się na /fr/docs/, ja_JP mapuje się na /ja/docs/. Jedynym przypadkiem specjalnym jest chiński tradycyjny: zh_TW mapuje się na /zh-tw/docs/ (z łącznikiem).

Trudne części są już za nami:

  1. Architektura routingu jest rozwiązana. Funkcja generateId, tablica localeKeys i konfiguracja defaultLocale: "root" współpracują ze sobą, dzięki czemu Starlight generuje poprawną strukturę URL. To był największy punkt bólu — wymagał śledzenia przez ponad 6 plików źródłowych w Starlight i Astro, aby znaleźć i naprawić problem.

  2. Skrypt tłumaczenia obsługuje wszystko. Pełne retłumaczenie strony, dodanie pojedynczego języka, aktualizacja poszczególnych plików — to ten sam skrypt z różnymi flagami. Ponawia próby przy limitach zapytań, zrównolegla pracę między workerami i raportuje błędy w czytelny sposób.

  3. Dodanie nowego języka to cztery edycje konfiguracji i jedno polecenie. Dodaj lokalizację do astro.config.mjs, content.config.ts i translate-docs.py, dodaj tłumaczenia paska bocznego, uruchom skrypt. Około 10 minut pracy plus 4 minuty tłumaczenia.

  4. Aktualizacja treści jest jeszcze prostsza. Edytuj angielskie źródło, uruchom skrypt z flagami --files i --overwrite tylko dla zmienionych plików, przebuduj. Albo przy drobnych poprawkach tekstu edytuj przetłumaczone pliki bezpośrednio.

  5. Cały pipeline jest utrwalony jako skill. Uruchomienie /translate_docs prowadzi przez cały proces — jaki tryb wybrać, jakie flagi przekazać, kontrole przed startem, weryfikacja po tłumaczeniu. Nie wymaga wiedzy instytucjonalnej.

Całkowite tłumaczenie 448 plików na 16 języków kosztowało około 28$ i zajęło około 70 minut. Pojedynczy nowy język kosztuje około 1,70$. Pojedynczy plik przetłumaczony na wszystkie języki kosztuje około 1$. To bieżące koszty utrzymywania dokumentacji aktualnej w 17 językach.