Structure-preserving LLM translation pipeline for IOAI contest documents.
Built for and battle-tested at IOAI 2026, where it translated every task statement, hint, and FAQ into 44 languages across three LLM providers. Designed so a future organising team can adopt it by editing two config files.
Contest documents are exam material: a mistranslated constraint costs a contestant points, and a mangled code block breaks their pipeline. This tool adds the guarantees a plain chat prompt cannot:
- Technical content is byte-identical by construction. Code blocks, inline code, LaTeX/math, URLs, and HTML comments are frozen into placeholder tokens before the LLM sees the text, and restored afterwards. The model physically cannot alter your code.
- No cross-language contamination. Every language is one fully independent API call — separate prompt, separate context. A validator additionally checks non-Latin outputs are written in the expected script.
- Structural validation. Every output is compared against the source:
heading levels and order, table dimensions, list counts, numbers in prose,
placeholder integrity. Failures are reported per language in
_report.md. - Delegation-driven model choice. Each language maps to the LLM family its national delegation trusts (configurable registry: OpenAI / Anthropic / Google / Qwen, with per-language fallbacks).
- Translator notes. Models flag genuine ambiguities and source errors into
a separate
.notes.mdfor organiser review — stripped from the contestant-facing file. - Operational hardening learnt the hard way: fast-fail request timeouts,
automatic corrective retry when a placeholder is mishandled, resume with
--only-missing, stale-notes cleanup, per-language provider fallback.
pip install ./ioai-translate # or: pip install ioai-translate (if published)Requires Python 3.10+. Provider SDKs (openai, anthropic, google-genai)
are installed automatically; you only need API keys for the providers your
registry actually uses.
# 1. Scaffold an editable config
ioai-translate init
# 2. Add API keys
cp ioai-translate-config/.env.example ioai-translate-config/.env
# edit the file, then: set -a; source ioai-translate-config/.env; set +a
# 3. Translate
ioai-translate run statement.md
# 4. Build deliverable folders (one per language, + figures, + English)
ioai-translate organize output/statement --source path/to/task_dirOutputs land in output/<stem>/:
| File | Purpose |
|---|---|
<stem>.<code>.md |
the translation, structure-consistent with the source |
<stem>.<code>.notes.md |
translator notes for organiser review (if any) |
<stem>.en.md |
snapshot of the source that was translated |
_report.md |
per-language status + validation results |
by_language/ |
after organize: French [fr]/, English [en]/, … |
ioai-translate run doc.md --langs fr,ko,ar # subset of languages
ioai-translate run doc.md --only-missing # resume an interrupted run
ioai-translate run doc.md --dry-run # inspect prompts, no API calls
ioai-translate run doc.md --context ioai-translate-config/prompts/context_support_doc.md
# for FAQs / logistics docs
ioai-translate validate doc.md output/doc/ # re-check after manual editsioai-translate init copies an editable config directory:
ioai-translate-config/
├── languages.json # language registry + model registry
├── .env.example # API key template
└── prompts/
├── base_prompt.md # the general translation prompt
├── language_extensions.md# per-language instructions (## fr — French …)
├── skill.md # translator checklist distilled from human review
└── context_support_doc.md# example document-type context (FAQ-style docs)
For a new IOAI edition you typically edit only:
languages.json— add/remove languages; set each language'sfamily(which provider/model translates it),script(for the contamination check), and optionalfallback_family/alias_of. Update themodel_registryto the current best model of each provider family — verify model IDs against the live provider APIs, and override at runtime with theenv_overridevariables without touching the file.prompts/language_extensions.md— per-language terminology conventions, registers, glossaries. Sections are keyed## <code> — <Name>.
The base prompt encodes fidelity rules (no additions/omissions/hints, preserve ambiguity, exam register, prompt-injection guard, notes protocol) and rarely needs changes.
| Variable | Purpose |
|---|---|
OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY / DASHSCOPE_API_KEY |
provider credentials |
IOAI_MODEL_OPENAI etc. |
override a model id without editing languages.json |
IOAI_REQUEST_TIMEOUT |
per-attempt timeout in seconds (default 240; raise to ~600 for very long documents in token-heavy scripts like Amharic) |
During the contest, team leaders review and fix the AI translations. Those edits are training signal — three subcommands close the loop:
# 1. Extract every human edit, per language, with automatic country→language
# matching. Edits that merely mirror an official English statement revision
# (detected via the ISC/English reference file) are tagged [OFFICIAL-UPDATE].
ioai-translate diff <contest-root> --out review/diffs
# <contest-root> = task folders each containing AI-Translation/ (the
# pipeline's by_language output) and Translations/ (the human-fixed files)
# 2. LLM-synthesize the diffs into guidance: per-language terminology rules,
# recurring-mistake lists, and a ready-to-paste updated prompt-extension
# section; plus a global report with base-prompt amendments and a skill.md
# checklist (only cross-language patterns with cited evidence).
ioai-translate lessons review/diffs --out review/lessons
# 3. Leakage detector: audit every genuine (non-official) edit for added
# information — extra hints, examples, or solution leakage that the English
# source never contained. Outputs audit.md + machine-readable audit.json
# with per-finding severity (info-added / hint / solution-leak /
# content-removed).
ioai-translate audit <contest-root> --sources <dir-of-task-folders> --out review/auditThe review-stage model is set by IOAI_REVIEW_MODEL (default
openai:gpt-5.6-sol; format provider:model).
The shipped prompts already contain one full cycle of this loop. At IOAI
2026 this workflow processed 353 document pairs (1,554 edits, half of them
official-update mirrors): the distilled results live in the package as the
"LESSONS FROM HUMAN REVIEW" section of base_prompt.md, reviewer-derived
update blocks in 29 sections of language_extensions.md, and
prompts/skill.md (the 10-point translator checklist). The 2026 audit
verdict, for calibration: 0 solution leaks, 4 marginal hints, and 55 edits
where reviewers accidentally dropped source nuance — the auditor watches
both directions.
- Cost: a ~1,500-token statement ≈ $3 per 44-language run; a ~4,500-token FAQ ≈ $8–10. Output tokens dominate (~85%), so prompt caching helps little; OpenAI's Batch API (−50%) is the lever if cost matters.
- Fund accounts before contest week and enable auto-recharge — the single most common failure mode is a provider running out of credit mid-run. Google's free tier (20 requests/day) cannot sustain a run; the key must belong to a billing-enabled project.
- Images: relative image references survive translation verbatim;
organize --sourcecopies figure files into every language folder so they render. HTML<img>tags are preserved by prompt (not by freezing) — the validator will tell you if one was damaged. - Warn-level checks are advisory. Number warnings in Japanese/Korean/Thai are usually grammatical counters, not data loss; the validator never blocks on them. FAIL-level checks (headings, tables, placeholders) are real.
- Run
Kernel-style smoke tests early:--langs fris a cheap probe that the keys, quota, and pipeline all work before you commit to 44 languages.
pip install -e ./ioai-translate
python -m pytest ioai-translate/tests -qMIT — see LICENSE.