Pular para o conteúdo

Sistema de Tradução

Este site é publicado em 17 idiomas. Não por uma equipa de tradutores, mas por um script Python, um modelo Claude Opus e uma estrutura de ficheiros concebida para que tudo encaixe no sítio certo. Eis como todo o processo funciona.

A documentação reside em src/content/docs/. O inglês é a raiz — todos os outros idiomas espelham-no de forma exata:

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)

Cada ficheiro traduzido é um espelho estrutural da sua fonte em inglês. O mesmo nome de ficheiro, o mesmo caminho de subdiretório, as mesmas chaves de frontmatter. A única diferença é que a prosa está noutro idioma.

O Starlight (a framework de documentação) depende desta simetria. Quando um utilizador muda de idioma, o Starlight troca /docs/getting-started/ por /fr/docs/getting-started/ — o mesmo caminho, um prefixo de locale diferente. Se o ficheiro em francês não existir exatamente em fr/getting-started.md, o seletor de idioma falha ou recai silenciosamente para inglês.

Os idiomas estão ordenados pela população global de jogadores de xadrez, com base em dados do Lichess, Chess.com e registos da FIDE. O inglês surge em primeiro como idioma de origem; tudo o resto segue a classificação do xadrez:

PosiçãoCódigoIdiomaEstilo
1enInglêsFonte de verdade
2esEspanholFormal padrão
3hiHindiEscrita Devanagari
4ruRussoFormal padrão
5deAlemãoFormal padrão
6frFrancêsFormal padrão
7ptPortuguêsPortuguês europeu
11plPolacoFormal padrão
12itItalianoFormal padrão
13ukUcranianoFormal padrão
14trTurcoFormal padrão
17koCoreanoForma 합니다/습니다
18zhChinês (Simplificado)Caracteres simplificados
zh-twChinês (Tradicional)Caracteres tradicionais
23nbNorueguês BokmålBokmål padrão
beBielorrussoBielorrusso padrão
34jaJaponêsForma です/ます

A coluna “estilo” é importante. O japonês e o coreano têm escolhas de registo formal que afetam cada frase. O prompt de tradução inclui estas instruções para que o modelo produza prosa natural e polida — não uma tradução automática rígida.

Esta ordenação também controla o menu pendente de idiomas no cabeçalho do site. Os idiomas mais falados na comunidade de xadrez aparecem primeiro, para que os utilizadores tenham mais probabilidade de encontrar o seu sem precisar de deslocar a lista.

Toda a documentação começa como markdown em inglês em src/content/docs/. O frontmatter contém um title e uma description:

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

Um script Python (scripts/translate-docs.py) lê cada ficheiro fonte em inglês, envia-o para a API do Claude e escreve o markdown traduzido:

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

O script demora cerca de 60–70 minutos a traduzir todos os 28 ficheiros fonte para os 16 idiomas de destino (448 ficheiros no total). Executa 5 chamadas API em paralelo para respeitar os limites de taxa.

Para um único idioma novo, demora cerca de 4 minutos.

Terminal window
pnpm build

O Astro lê o markdown fonte, renderiza-o através dos modelos do Starlight e produz HTML estático em dist/. A compilação demora cerca de 30 segundos para as ~500 páginas.

Terminal window
pnpm run deploy

Envia o site compilado para o Cloudflare Workers.

O script de tradução é intencionalmente simples — cerca de 300 linhas de Python. Eis o fluxo:

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

O prompt indica ao Claude exatamente o que traduzir e o que manter inalterado:

  • Traduzir: prosa, títulos, etiquetas de tabelas, título e descrição do frontmatter
  • Manter em inglês: blocos de código, código inline, exemplos de comandos, caminhos de ficheiros, URLs, nomes de produtos, nomes de pessoas, diagramas ASCII

Esta separação é crítica. Um comando como pnpm build deve permanecer pnpm build em todos os idiomas. Um nome de produto como “En Parlant~” ou “Stockfish” não é traduzido. Mas “Getting Started” torna-se “はじめに” em japonês e “Начало работы” em russo.

O Claude Opus 4.6 produz traduções visivelmente melhores do que modelos mais rápidos. A diferença manifesta-se em:

  • Fraseado natural — O Opus escreve como um falante nativo, não como um tradutor. Reestrutura frases quando a ordem de palavras do inglês soaria estranha no idioma de destino.
  • Precisão técnica — A terminologia de xadrez, o jargão de TTS e os conceitos de software são traduzidos usando os termos corretos do domínio específico.
  • Consistência — O registo formal mantém-se consistente ao longo de todo o texto. O japonês usa a forma です/ます em todo o lado, sem alternar entre casual e polido a meio de um parágrafo.
  • Tratamento de MDX — O Opus preserva corretamente as tags de componentes JSX (<Card>, <CardGrid>) e as instruções import em ficheiros .mdx sem os corromper.

A diferença de custo é real — cerca de $28 para uma tradução completa do site com o Opus contra $5 com o Sonnet — mas para 448 ficheiros que os utilizadores vão efetivamente ler, a qualidade compensa.

Esta foi a parte que mais tempo levou a acertar.

O Starlight deteta o idioma de uma página verificando o primeiro segmento do seu slug de conteúdo. Quando vê fr/docs/getting-started, sabe que é francês porque fr é o primeiro segmento. Mas a implementação inicial produzia slugs como docs/fr/getting-started — o locale enterrado sob docs/. O Starlight via docs como primeiro segmento, tratava tudo como inglês e gerava mais de 7.000 páginas duplicadas em vez de ~500.

Uma função generateId personalizada em src/content.config.ts controla como os caminhos de ficheiros se tornam slugs de conteúdo:

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

Isto coloca o prefixo de locale antes de docs/:

Caminho do ficheiroSlugURL
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/

O inglês usa defaultLocale: "root", o que significa que não tem prefixo — vive diretamente em /docs/, não em /en/docs/.

Um array localeKeys no mesmo ficheiro deve listar todos os locales que não sejam inglês. Se um locale existe na configuração do Astro mas não neste array, o seu conteúdo traduzido é tratado como conteúdo em inglês — o seletor de idioma falha e a contagem de páginas dispara.

O Astro armazena em cache os mapeamentos de slugs em .astro/data-store.json. Após qualquer alteração na configuração de locales, este ficheiro deve ser eliminado antes de recompilar, caso contrário a compilação é bem-sucedida com encaminhamento obsoleto (errado).

A barra lateral é definida em astro.config.mjs. Os itens que apontam para uma página (propriedade slug) usam automaticamente o título do frontmatter da página traduzida — sem necessidade de tradução manual:

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

Mas as etiquetas de grupo e os links externos necessitam de traduções explícitas:

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

Sete elementos da barra lateral necessitam de traduções manuais: Welcome, Features, App Menus, Setup Guides, Under the Hood, Credits e Accessibility. Adicionar um novo idioma significa acrescentar uma entrada a cada um destes sete blocos.

O En Parlant~ liga a esta documentação a partir do seu menu Ajuda. O link é sensível ao locale — se estiver a usar a aplicação em francês, abre a documentação em francês:

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

Todos os idiomas disponíveis na aplicação têm uma tradução correspondente no site. O mapeamento é automático — fr_FR mapeia para /fr/docs/, ja_JP mapeia para /ja/docs/. O único caso especial é o chinês tradicional: zh_TW mapeia para /zh-tw/docs/ (com hífen).

As partes difíceis estão resolvidas:

  1. A arquitetura de encaminhamento está resolvida. A função generateId, o array localeKeys e a configuração defaultLocale: "root" trabalham em conjunto para que o Starlight gere a estrutura de URLs correta. Este foi o maior ponto problemático — exigiu rastrear mais de 6 ficheiros fonte no Starlight e no Astro para encontrar e corrigir.

  2. O script de tradução trata de tudo. Retradução completa do site, adição de um único idioma, atualização de ficheiros individuais — tudo o mesmo script com flags diferentes. Faz novas tentativas em caso de limites de taxa, paraleliza entre workers e reporta erros de forma clara.

  3. Adicionar um novo idioma são quatro edições de configuração e um comando. Adicionar o locale a astro.config.mjs, content.config.ts e translate-docs.py, acrescentar as traduções da barra lateral, executar o script. Cerca de 10 minutos de trabalho mais 4 minutos de tempo de tradução.

  4. Atualizar conteúdo é ainda mais simples. Editar o ficheiro fonte em inglês, executar o script com --files e --overwrite apenas para os ficheiros alterados, recompilar. Ou, para pequenas correções de texto, editar diretamente os ficheiros traduzidos.

  5. Todo o pipeline está capturado numa skill. Executar /translate_docs guia-nos por todo o processo — qual modo usar, que flags passar, verificações pré-voo, verificação pós-tradução. Sem necessidade de conhecimento institucional.

A tradução total de 448 ficheiros em 16 idiomas custou cerca de $28 e demorou aproximadamente 70 minutos. Um único idioma novo custa cerca de $1,70. Um único ficheiro retraduzido em todos os idiomas custa cerca de $1. Estes são os custos correntes de manter a documentação atualizada em 17 idiomas.