Salta ai contenuti

Sistema di traduzione

Questo sito è pubblicato in 17 lingue. Non da un team di traduttori, ma da uno script Python, un modello Claude Opus e una struttura di file progettata affinché tutto si incastri perfettamente. Ecco come funziona l’intero sistema.

La documentazione risiede in src/content/docs/. L’inglese è la radice — ogni altra lingua lo rispecchia esattamente:

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)

Ogni file tradotto è uno specchio strutturale del suo sorgente inglese. Stesso nome di file, stesso percorso di sottodirectory, stesse chiavi nel frontmatter. L’unica differenza è che la prosa è in un’altra lingua.

Starlight (il framework di documentazione) si basa su questa simmetria. Quando un utente cambia lingua, Starlight sostituisce /docs/getting-started/ con /fr/docs/getting-started/ — stesso percorso, prefisso locale diverso. Se il file francese non esiste esattamente in fr/getting-started.md, il selettore di lingua si interrompe o torna silenziosamente all’inglese.

Le lingue sono ordinate per popolazione mondiale di giocatori di scacchi, basandosi su dati di Lichess, Chess.com e registrazioni FIDE. L’inglese è al primo posto come lingua sorgente; tutto il resto segue le classifiche scacchistiche:

PosizioneCodiceLinguaStile
1enIngleseFonte di riferimento
2esSpagnoloFormale standard
3hiHindiScrittura Devanagari
4ruRussoFormale standard
5deTedescoFormale standard
6frFranceseFormale standard
7ptPortoghesePortoghese europeo
11plPolaccoFormale standard
12itItalianoFormale standard
13ukUcrainoFormale standard
14trTurcoFormale standard
17koCoreanoForma 합니다/습니다
18zhCinese (semplificato)Caratteri semplificati
zh-twCinese (tradizionale)Caratteri tradizionali
23nbNorvegese BokmålBokmål standard
beBielorussoBielorusso standard
34jaGiapponeseForma です/ます

La colonna “stile” è importante. Giapponese e coreano hanno scelte di registro formale che influenzano ogni frase. Il prompt di traduzione include queste istruzioni affinché il modello produca una prosa naturale e curata — non un output meccanico e rigido.

Questo ordinamento controlla anche il menu a discesa delle lingue nell’intestazione del sito. Le lingue scacchistiche più parlate appaiono per prime, così gli utenti hanno maggiori probabilità di trovare la propria senza scorrere.

Tutta la documentazione inizia come markdown in inglese in src/content/docs/. Il frontmatter contiene un title e una description:

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

Uno script Python (scripts/translate-docs.py) legge ogni file sorgente inglese, lo invia all’API di Claude e scrive il markdown tradotto:

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

Lo script impiega circa 60–70 minuti per tradurre tutti i 28 file sorgente in tutte le 16 lingue di destinazione (448 file in totale). Esegue 5 chiamate API in parallelo per rimanere entro i limiti di frequenza.

Per una singola nuova lingua, bastano circa 4 minuti.

Terminal window
pnpm build

Astro legge il markdown sorgente, lo renderizza attraverso i template di Starlight e produce HTML statico nella directory dist/. Il build impiega circa 30 secondi per tutte le ~500 pagine.

Terminal window
pnpm run deploy

Pubblica il sito compilato su Cloudflare Workers.

Lo script di traduzione è volutamente semplice — circa 300 righe di Python. Ecco il flusso:

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

Il prompt dice a Claude esattamente cosa tradurre e cosa lasciare invariato:

  • Tradurre: prosa, intestazioni, etichette delle tabelle, title e description del frontmatter
  • Mantenere in inglese: blocchi di codice, codice inline, esempi di comandi, percorsi di file, URL, nomi di prodotto, nomi di persone, diagrammi ASCII

Questa separazione è fondamentale. Un comando come pnpm build deve rimanere pnpm build in ogni lingua. Un nome di prodotto come “En Parlant~” o “Stockfish” resta non tradotto. Ma “Getting Started” diventa “はじめに” in giapponese e “Начало работы” in russo.

Claude Opus 4.6 produce traduzioni notevolmente migliori rispetto ai modelli più veloci. La differenza si nota in:

  • Formulazione naturale — Opus scrive come un madrelingua, non come un traduttore. Ristruttura le frasi quando l’ordine delle parole inglese risulterebbe innaturale nella lingua di destinazione.
  • Accuratezza tecnica — La terminologia scacchistica, il gergo TTS e i concetti software vengono tradotti utilizzando i termini corretti specifici del dominio.
  • Coerenza — Il registro formale rimane coerente ovunque. Il giapponese usa la forma です/ます dappertutto, senza alternare tra registro informale e formale a metà paragrafo.
  • Gestione MDX — Opus preserva correttamente i tag dei componenti JSX (<Card>, <CardGrid>) e le istruzioni import nei file .mdx senza alterarli.

La differenza di costo è reale — circa $28 per una traduzione completa del sito con Opus contro $5 con Sonnet — ma per 448 file che gli utenti leggeranno effettivamente, la qualità ne vale la pena.

Questa è la parte che ha richiesto più tempo per essere risolta correttamente.

Starlight rileva la lingua di una pagina controllando il primo segmento dello slug del contenuto. Quando vede fr/docs/getting-started, sa che è francese perché fr è il primo segmento. Ma l’implementazione iniziale produceva slug come docs/fr/getting-started — con la lingua sepolta sotto docs/. Starlight vedeva docs come primo segmento, trattava tutto come inglese e generava oltre 7.000 pagine duplicate invece di circa 500.

Una funzione personalizzata generateId in src/content.config.ts controlla come i percorsi dei file diventano slug di contenuto:

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

Questo posiziona il prefisso locale prima di docs/:

Percorso del fileSlugURL
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/

L’inglese usa defaultLocale: "root", il che significa nessun prefisso — risiede direttamente in /docs/, non in /en/docs/.

Un array localeKeys nello stesso file deve elencare ogni lingua non inglese. Se una lingua esiste nella configurazione di Astro ma non in questo array, il suo contenuto tradotto viene trattato come contenuto inglese — il selettore di lingua si interrompe e il conteggio delle pagine esplode.

Astro memorizza nella cache le mappature degli slug in .astro/data-store.json. Dopo qualsiasi modifica alla configurazione delle lingue, questo file deve essere eliminato prima di ricompilare, altrimenti il build riesce con un routing obsoleto (errato).

La barra laterale è definita in astro.config.mjs. Gli elementi che puntano a una pagina (proprietà slug) utilizzano automaticamente il titolo del frontmatter della pagina tradotta — nessuna traduzione manuale necessaria:

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

Ma le etichette dei gruppi e i link esterni necessitano di traduzioni esplicite:

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

Sette elementi della barra laterale necessitano di traduzioni manuali: Welcome, Features, App Menus, Setup Guides, Under the Hood, Credits e Accessibility. Aggiungere una nuova lingua significa aggiungere una voce a ciascuno di questi sette blocchi.

En Parlant~ collega questa documentazione dal suo menu Aiuto. Il link è sensibile alla lingua — se stai usando l’app in francese, apre la documentazione in francese:

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

Ogni lingua disponibile nell’app ha una traduzione corrispondente sul sito. La mappatura è automatica — fr_FR corrisponde a /fr/docs/, ja_JP corrisponde a /ja/docs/. L’unico caso speciale è il cinese tradizionale: zh_TW corrisponde a /zh-tw/docs/ (con trattino).

Le parti difficili sono state risolte:

  1. L’architettura di routing è definita. La funzione generateId, l’array localeKeys e la configurazione defaultLocale: "root" lavorano insieme affinché Starlight generi la struttura URL corretta. Questo è stato il problema più grande — ha richiesto di analizzare oltre 6 file sorgente in Starlight e Astro per individuare e correggere il problema.

  2. Lo script di traduzione gestisce tutto. Ritraduzione completa del sito, aggiunta di una singola lingua, aggiornamenti di singoli file — tutto con lo stesso script con flag differenti. Riprova automaticamente in caso di limiti di frequenza, parallelizza tra i worker e segnala gli errori in modo chiaro.

  3. Aggiungere una nuova lingua richiede quattro modifiche alla configurazione e un comando. Aggiungere la lingua a astro.config.mjs, content.config.ts e translate-docs.py, aggiungere le traduzioni della barra laterale, eseguire lo script. Circa 10 minuti di lavoro più 4 minuti di tempo di traduzione.

  4. Aggiornare i contenuti è ancora più semplice. Modificare il sorgente inglese, eseguire lo script con --files e --overwrite solo per i file modificati, ricompilare. Oppure per piccole correzioni alla prosa, modificare direttamente i file tradotti.

  5. L’intera pipeline è catturata in una skill. Eseguire /translate_docs guida attraverso l’intero processo — quale modalità usare, quali flag passare, controlli preliminari, verifica post-traduzione. Nessuna conoscenza pregressa necessaria.

La traduzione totale di 448 file in 16 lingue è costata circa $28 e ha richiesto circa 70 minuti. Una singola nuova lingua costa circa $1,70. Un singolo file ritradotto in tutte le lingue costa circa $1. Questi sono i costi correnti per mantenere la documentazione aggiornata in 17 lingue.