번역 시스템
이 사이트는 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)모든 번역 파일은 영어 원본의 구조적 미러입니다. 동일한 파일명, 동일한 하위 디렉토리 경로, 동일한 프론트매터 키. 유일한 차이점은 본문이 다른 언어로 되어 있다는 것입니다.
미러링이 중요한 이유
섹션 제목: “미러링이 중요한 이유”Starlight(문서 프레임워크)는 이 대칭성에 의존합니다. 사용자가 언어를 전환하면, Starlight는 /docs/getting-started/를 /fr/docs/getting-started/로 교체합니다 — 같은 경로, 다른 로케일 접두사. 프랑스어 파일이 정확히 fr/getting-started.md에 존재하지 않으면, 전환 기능이 깨지거나 조용히 영어로 폴백됩니다.
17개 언어
섹션 제목: “17개 언어”언어는 전 세계 체스 인구를 기준으로 정렬되어 있으며, Lichess, Chess.com, FIDE 등록 데이터를 기반으로 합니다. 영어가 원본 언어로 첫 번째이고, 나머지는 체스 순위를 따릅니다:
| 순위 | 코드 | 언어 | 스타일 |
|---|---|---|---|
| 1 | en | 영어 | 원본 |
| 2 | es | 스페인어 | 표준 격식체 |
| 3 | hi | 힌디어 | 데바나가리 문자 |
| 4 | ru | 러시아어 | 표준 격식체 |
| 5 | de | 독일어 | 표준 격식체 |
| 6 | fr | 프랑스어 | 표준 격식체 |
| 7 | pt | 포르투갈어 | 유럽식 포르투갈어 |
| 11 | pl | 폴란드어 | 표준 격식체 |
| 12 | it | 이탈리아어 | 표준 격식체 |
| 13 | uk | 우크라이나어 | 표준 격식체 |
| 14 | tr | 터키어 | 표준 격식체 |
| 17 | ko | 한국어 | 합니다/습니다 형 |
| 18 | zh | 중국어(간체) | 간체자 |
| — | zh-tw | 중국어(번체) | 번체자 |
| 23 | nb | 노르웨이어 보크몰 | 표준 보크몰 |
| — | be | 벨라루스어 | 표준 벨라루스어 |
| 34 | ja | 일본어 | です/ます 형 |
“스타일” 열은 중요합니다. 일본어와 한국어는 모든 문장에 영향을 미치는 격식체 선택이 있습니다. 번역 프롬프트에 이 지침이 포함되어 있어 모델이 자연스럽고 세련된 문장을 생성합니다 — 딱딱한 기계 번역이 아닙니다.
이 순서는 사이트 헤더의 언어 드롭다운도 제어합니다. 가장 많이 사용되는 체스 언어가 먼저 표시되므로, 사용자가 스크롤 없이 자신의 언어를 찾을 가능성이 높아집니다.
번역 파이프라인
섹션 제목: “번역 파이프라인”1단계: 영어 작성
섹션 제목: “1단계: 영어 작성”모든 문서는 src/content/docs/에 영어 마크다운으로 시작합니다. 프론트매터에는 title과 description이 있습니다:
---title: "Getting Started"description: "Install En Parlant~ and play your first game."---
Download the latest release...2단계: 스크립트 실행
섹션 제목: “2단계: 스크립트 실행”Python 스크립트(scripts/translate-docs.py)가 모든 영어 원본 파일을 읽고, Claude API로 전송한 후, 번역된 마크다운을 작성합니다:
python3 scripts/translate-docs.py \ --anthropic-key $ANTHROPIC_API_KEY \ --model claude-opus-4-6 \ --workers 5이 스크립트는 28개 원본 파일을 16개 대상 언어로 번역하는 데 약 60–70분이 소요됩니다(총 448개 파일). 속도 제한 내에서 유지하기 위해 5개의 병렬 API 호출을 실행합니다.
새로운 단일 언어의 경우, 약 4분입니다.
3단계: 빌드
섹션 제목: “3단계: 빌드”pnpm buildAstro가 원본 마크다운을 읽고, Starlight 템플릿을 통해 렌더링하며, dist/에 정적 HTML을 출력합니다. 약 500개 전체 페이지의 빌드에 약 30초가 소요됩니다.
4단계: 배포
섹션 제목: “4단계: 배포”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에게 무엇을 번역하고 무엇을 그대로 둘지 정확히 알려줍니다:
- 번역 대상: 본문, 제목, 표 레이블, 프론트매터 title과 description
- 영어 유지: 코드 블록, 인라인 코드, 명령어 예시, 파일 경로, URL, 제품명, 사람 이름, ASCII 다이어그램
이 분리는 매우 중요합니다. pnpm build 같은 명령어는 모든 언어에서 pnpm build로 유지되어야 합니다. “En Parlant~“나 “Stockfish” 같은 제품명은 번역하지 않습니다. 하지만 “Getting Started”는 일본어에서 “はじめに”가 되고, 러시아어에서 “Начало работы”가 됩니다.
왜 Opus인가?
섹션 제목: “왜 Opus인가?”Claude Opus 4.6은 더 빠른 모델보다 눈에 띄게 나은 번역을 생성합니다. 차이는 다음과 같은 부분에서 나타납니다:
- 자연스러운 표현 — Opus는 번역가가 아닌 원어민처럼 씁니다. 영어 어순이 대상 언어에서 어색하게 들릴 때 문장을 재구성합니다.
- 기술적 정확성 — 체스 용어, TTS 전문 용어, 소프트웨어 개념이 올바른 도메인별 용어로 번역됩니다.
- 일관성 — 격식체가 전체적으로 일관되게 유지됩니다. 일본어는 문단 중간에 반말과 존댓말을 오가지 않고 모든 곳에서 です/ます 형을 사용합니다.
- MDX 처리 — Opus는
.mdx파일에서 JSX 컴포넌트 태그(<Card>,<CardGrid>)와import문을 손상시키지 않고 올바르게 보존합니다.
비용 차이는 실재합니다 — Opus로 전체 사이트 번역 시 약 $28 대 Sonnet으로 $5 — 하지만 사용자가 실제로 읽을 448개 파일에 대해서는 품질이 그만한 가치가 있습니다.
로케일 라우팅 아키텍처
섹션 제목: “로케일 라우팅 아키텍처”이 부분이 제대로 만드는 데 가장 오래 걸렸습니다.
Starlight는 콘텐츠 슬러그의 첫 번째 세그먼트를 확인하여 페이지의 언어를 감지합니다. fr/docs/getting-started를 보면, fr이 첫 번째 세그먼트이므로 프랑스어라는 것을 알 수 있습니다. 하지만 초기 구현에서는 docs/fr/getting-started와 같은 슬러그가 생성되었습니다 — 로케일이 docs/ 아래에 묻힌 것입니다. Starlight는 docs를 첫 번째 세그먼트로 인식하고 모든 것을 영어로 처리하여, 약 500개 대신 7,000개 이상의 중복 페이지를 생성했습니다.
해결책
섹션 제목: “해결책”src/content.config.ts의 커스텀 generateId 함수가 파일 경로가 콘텐츠 슬러그로 변환되는 방식을 제어합니다:
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/ 앞에 위치합니다:
| 파일 경로 | 슬러그 | URL |
|---|---|---|
getting-started.md | docs/getting-started | /docs/getting-started/ |
fr/getting-started.md | fr/docs/getting-started | /fr/docs/getting-started/ |
ja/features/puzzles.md | ja/docs/features/puzzles | /ja/docs/features/puzzles/ |
영어는 defaultLocale: "root"를 사용하므로 접두사가 없습니다 — /en/docs/가 아닌 /docs/에 직접 위치합니다.
localeKeys 배열
섹션 제목: “localeKeys 배열”같은 파일의 localeKeys 배열에 영어가 아닌 모든 로케일이 나열되어야 합니다. Astro 설정에는 로케일이 존재하지만 이 배열에 없으면, 번역된 콘텐츠가 영어 콘텐츠로 처리됩니다 — 언어 전환 기능이 깨지고 페이지 수가 폭발합니다.
데이터 스토어 캐시
섹션 제목: “데이터 스토어 캐시”Astro는 .astro/data-store.json에 슬러그 매핑을 캐시합니다. 로케일 설정 변경 후에는 이 파일을 삭제한 후 다시 빌드해야 합니다. 그렇지 않으면 빌드는 성공하지만 오래된(잘못된) 라우팅이 적용됩니다.
메뉴 구조
섹션 제목: “메뉴 구조”사이드바는 astro.config.mjs에 정의되어 있습니다. 페이지를 가리키는 항목(slug 속성)은 자동으로 번역된 페이지의 프론트매터 제목을 사용합니다 — 수동 번역이 필요 없습니다:
{ 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" }, // ... ],}7개의 사이드바 요소에 수동 번역이 필요합니다: Welcome, Features, App Menus, Setup Guides, Under the Hood, Credits, Accessibility. 새로운 언어를 추가하면 이 7개 블록 각각에 하나의 항목을 추가해야 합니다.
앱 연동
섹션 제목: “앱 연동”En Parlant~은 도움말 메뉴에서 이 문서로 연결됩니다. 링크는 로케일을 인식합니다 — 프랑스어로 앱을 사용 중이라면 프랑스어 문서가 열립니다:
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/로 매핑됩니다(하이픈 포함).
왜 이제 쉬워졌는가
섹션 제목: “왜 이제 쉬워졌는가”어려운 부분은 이미 완료되었습니다:
-
라우팅 아키텍처가 해결되었습니다.
generateId함수,localeKeys배열,defaultLocale: "root"설정이 함께 작동하여 Starlight가 올바른 URL 구조를 생성합니다. 이것이 가장 큰 난제였습니다 — Starlight와 Astro의 6개 이상의 소스 파일을 추적하여 찾아내고 수정해야 했습니다. -
번역 스크립트가 모든 것을 처리합니다. 전체 사이트 재번역, 단일 언어 추가, 개별 파일 업데이트 — 모두 같은 스크립트에 다른 플래그를 사용합니다. 속도 제한 시 재시도하고, 워커 간 병렬 처리하며, 오류를 명확히 보고합니다.
-
새 언어 추가는 4번의 설정 편집과 하나의 명령어입니다.
astro.config.mjs,content.config.ts,translate-docs.py에 로케일을 추가하고, 사이드바 번역을 추가한 후 스크립트를 실행합니다. 약 10분의 작업과 4분의 번역 시간입니다. -
콘텐츠 업데이트는 더 간단합니다. 영어 원본을 편집하고, 변경된 파일만
--files와--overwrite플래그로 스크립트를 실행한 후 다시 빌드합니다. 또는 작은 문장 수정의 경우, 번역된 파일을 직접 편집합니다. -
전체 파이프라인이 스킬로 캡처되어 있습니다.
/translate_docs를 실행하면 전체 프로세스를 안내합니다 — 어떤 모드를 사용할지, 어떤 플래그를 전달할지, 사전 점검, 번역 후 검증. 기관 지식이 필요하지 않습니다.
448개 파일의 16개 언어 전체 번역 비용은 약 $28이며 약 70분이 소요되었습니다. 새로운 단일 언어 비용은 약 $1.70입니다. 모든 언어에 걸쳐 단일 파일 재번역 비용은 약 $1입니다. 이것이 17개 언어로 문서를 최신 상태로 유지하는 데 드는 지속적인 비용입니다.