Перейти к содержимому

Система перевода

Этот сайт опубликован на 17 языках. Не командой переводчиков, а Python-скриптом, моделью Claude Opus и файловой структурой, в которой всё встаёт на свои места. Вот как это устроено.

Документация хранится в src/content/docs/. Английский язык — это корень, все остальные языки точно повторяют его структуру:

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)

Каждый переведённый файл — это структурное зеркало английского оригинала. Те же имена файлов, те же пути подкаталогов, те же ключи frontmatter. Единственное отличие — текст написан на другом языке.

Starlight (фреймворк документации) опирается на эту симметрию. Когда пользователь переключает язык, Starlight заменяет /docs/getting-started/ на /fr/docs/getting-started/ — тот же путь, другой префикс локали. Если французский файл не находится точно по пути fr/getting-started.md, переключатель ломается или молча откатывается к английской версии.

Языки упорядочены по численности шахматного сообщества в мире на основе данных Lichess, Chess.com и регистраций FIDE. Английский идёт первым как исходный язык; остальные следуют в порядке шахматного рейтинга:

РангКодЯзыкСтиль
1enАнглийскийПервоисточник
2esИспанскийСтандартный формальный
3hiХиндиПисьменность деванагари
4ruРусскийСтандартный формальный
5deНемецкийСтандартный формальный
6frФранцузскийСтандартный формальный
7ptПортугальскийЕвропейский португальский
11plПольскийСтандартный формальный
12itИтальянскийСтандартный формальный
13ukУкраинскийСтандартный формальный
14trТурецкийСтандартный формальный
17koКорейскийФорма 합니다/습니다
18zhКитайский (упрощённый)Упрощённые иероглифы
zh-twКитайский (традиционный)Традиционные иероглифы
23nbНорвежский букмолСтандартный букмол
beБелорусскийСтандартный белорусский
34jaЯпонскийФорма です/ます

Столбец «Стиль» имеет значение. В японском и корейском языках есть формальные регистры, которые влияют на каждое предложение. Промпт для перевода включает эти инструкции, чтобы модель создавала естественный, отполированный текст, а не сухой машинный перевод.

Этот порядок также определяет выпадающий список языков в шапке сайта. Языки с наибольшим шахматным сообществом отображаются первыми, поэтому пользователи с большей вероятностью найдут свой язык без прокрутки.

Вся документация начинается как markdown на английском языке в src/content/docs/. Frontmatter содержит title и description:

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

Python-скрипт (scripts/translate-docs.py) считывает каждый английский исходный файл, отправляет его в API Claude и записывает переведённый markdown:

Окно терминала
python3 scripts/translate-docs.py \
--anthropic-key $ANTHROPIC_API_KEY \
--model claude-opus-4-6 \
--workers 5

Скрипту требуется около 60–70 минут для перевода всех 28 исходных файлов на все 16 целевых языков (448 файлов в сумме). Он выполняет 5 параллельных API-запросов, чтобы не превышать лимиты частоты обращений.

Для одного нового языка — около 4 минут.

Окно терминала
pnpm build

Astro считывает исходный markdown, рендерит его через шаблоны Starlight и генерирует статический HTML в dist/. Сборка занимает около 30 секунд для всех ~500 страниц.

Окно терминала
pnpm run deploy

Отправляет собранный сайт на Cloudflare Workers.

Скрипт перевода намеренно прост — около 300 строк на Python. Вот его логика:

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

Промпт точно указывает Claude, что переводить, а что оставлять без изменений:

  • Переводить: прозу, заголовки, подписи таблиц, frontmatter title и description
  • Оставлять на английском: блоки кода, инлайн-код, примеры команд, пути файлов, URL-адреса, названия продуктов, имена людей, ASCII-диаграммы

Это разделение критически важно. Команда вроде pnpm build должна оставаться pnpm build на любом языке. Название продукта «En Parlant~» или «Stockfish» не переводится. Но «Getting Started» становится «はじめに» на японском и «Начало работы» на русском.

Claude Opus 4.6 даёт заметно лучшие переводы, чем более быстрые модели. Разница проявляется в следующем:

  • Естественные формулировки — Opus пишет как носитель языка, а не как переводчик. Он перестраивает предложения, когда английский порядок слов звучал бы неестественно в целевом языке.
  • Техническая точность — шахматная терминология, жаргон TTS и программные концепции переводятся с использованием корректных предметно-специфичных терминов.
  • Последовательность — формальный регистр сохраняется единообразно на протяжении всего текста. Японский использует форму です/ます повсюду, не переключаясь между разговорным и вежливым стилем посреди абзаца.
  • Обработка MDX — Opus корректно сохраняет теги JSX-компонентов (<Card>, <CardGrid>) и выражения import в файлах .mdx, не искажая их.

Разница в стоимости ощутима — около $28 за полный перевод сайта с Opus против $5 с Sonnet — но для 448 файлов, которые пользователи действительно будут читать, качество того стоит.

Это та часть, на отладку которой ушло больше всего времени.

Starlight определяет язык страницы по первому сегменту slug контента. Когда он видит fr/docs/getting-started, он понимает, что это французский, потому что fr — первый сегмент. Но изначальная реализация генерировала slug вида docs/fr/getting-started — локаль оказывалась вложена внутрь docs/. Starlight видел docs как первый сегмент, считал всё английским и генерировал 7000+ дублирующихся страниц вместо ~500.

Пользовательская функция generateId в src/content.config.ts управляет тем, как пути файлов превращаются в slug контента:

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

Это помещает префикс локали перед docs/:

Путь файлаSlugURL
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/

Английский использует defaultLocale: "root", что означает отсутствие префикса — он живёт непосредственно по пути /docs/, а не /en/docs/.

Массив localeKeys в том же файле должен перечислять каждую неанглийскую локаль. Если локаль существует в конфигурации Astro, но отсутствует в этом массиве, её переведённый контент будет обработан как английский — переключатель языков сломается и количество страниц резко возрастёт.

Astro кэширует маппинг slug в .astro/data-store.json. После любого изменения конфигурации локалей этот файл необходимо удалить перед пересборкой, иначе сборка завершится успешно, но с устаревшей (неправильной) маршрутизацией.

Боковая панель определяется в astro.config.mjs. Элементы, указывающие на страницу (свойство slug), автоматически используют заголовок из frontmatter переведённой страницы — ручной перевод не требуется:

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

Но подписи групп и внешние ссылки требуют явных переводов:

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

Семь элементов боковой панели требуют ручного перевода: Welcome, Features, App Menus, Setup Guides, Under the Hood, Credits и Accessibility. Добавление нового языка означает добавление одной записи в каждый из этих семи блоков.

En Parlant~ ссылается на эту документацию из меню Help. Ссылка учитывает локаль — если вы используете приложение на французском, откроется французская документация:

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

Для каждого языка, доступного в приложении, на сайте есть соответствующий перевод. Маппинг выполняется автоматически — fr_FR ведёт на /fr/docs/, ja_JP ведёт на /ja/docs/. Единственный особый случай — традиционный китайский: zh_TW ведёт на /zh-tw/docs/ (через дефис).

Сложные части уже решены:

  1. Архитектура маршрутизации отлажена. Функция generateId, массив localeKeys и конфигурация defaultLocale: "root" работают вместе, чтобы Starlight генерировал корректную структуру URL. Это была самая болезненная проблема — для её обнаружения и исправления потребовалось проследить логику через 6+ исходных файлов Starlight и Astro.

  2. Скрипт перевода справляется со всем. Полный повторный перевод сайта, добавление одного языка, обновление отдельных файлов — один и тот же скрипт с разными флагами. Он повторяет запросы при превышении лимитов, параллелизирует работу между воркерами и чётко сообщает об ошибках.

  3. Добавление нового языка — это четыре правки в конфигурации и одна команда. Добавьте локаль в astro.config.mjs, content.config.ts и translate-docs.py, добавьте переводы боковой панели, запустите скрипт. Около 10 минут работы плюс 4 минуты на перевод.

  4. Обновление контента ещё проще. Отредактируйте английский исходник, запустите скрипт с флагами --files и --overwrite только для изменённых файлов, пересоберите. Или для небольших правок текста редактируйте переведённые файлы напрямую.

  5. Весь конвейер описан в навыке. Запуск /translate_docs проведёт через весь процесс — какой режим использовать, какие флаги передать, предварительные проверки, верификация после перевода. Никаких скрытых знаний не требуется.

Полный перевод 448 файлов на 16 языков обошёлся примерно в $28 и занял около 70 минут. Один новый язык стоит около $1.70. Повторный перевод одного файла на все языки стоит около $1. Таковы текущие расходы на поддержание документации в актуальном состоянии на 17 языках.