Traduction par lots
Traduire un ensemble de documentation, c'est quelques centaines d'appels à
/v1/chat/completions. Les appels sont la partie facile. Cette page porte sur les trois choses qui décident si le résultat est utilisable : connaître le coût avant de le dépenser, vérifier que le Markdown a survécu et vérifier que le modèle a réellement traduit quelque chose.
Chaque chiffre de cette page a été mesuré le 2026-09-23 sur l'arborescence de documentation de ce site — 17 fichiers, 134,713 caractères. Le script complet est celui que nous utilisons, et vous pouvez le télécharger : translate-docs.py (licence de type MIT, aucune dépendance en dehors de la bibliothèque standard).
Ce que cela coûte, et ce qui s'est réellement passé
Nous avons traité l'ensemble complet le 2026-09-23. Voici la sortie réelle, pas une estimation :
| Source | 17 fichiers / 134,713 caractères |
| Modèle | gpt-4o-mini |
| Tarif mesuré | 18 AP pour 2,500 caractères |
| Ensemble complet, une langue | ≈ 970 AP (environ US$0.10) |
| Fichiers écrits | 15 |
| Fichiers refusés par les vérifications | 2 |
Deux fichiers sur dix-sept sont revenus endommagés et n'ont pas été écrits. Les deux avaient l'air parfaitement normaux : début correct, fin correcte, finish_reason: stop, aucune erreur d'aucune sorte. C'est la raison pour laquelle cette page parle surtout de vérification plutôt que des appels.
L'appel
Un fichier, une requête. Pas de streaming, pas de découpage — une page de documentation tient dans la fenêtre de contexte, et c'est en la découpant qu'on perd des niveaux de titres et des colonnes de tableaux aux jointures.
curl -X POST https://api.aicu.ai/v1/chat/completions \
-H "Authorization: Bearer aicu_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"temperature": 0,
"max_tokens": 8000,
"messages": [
{"role": "system", "content": "You translate technical API documentation into Korean. Translate prose only. Reproduce byte-for-byte: YAML frontmatter, anything inside ``` fences, URLs, file paths, HTTP methods and status codes, and API names in backticks. Keep the Markdown structure identical. Output the translated document only."},
{"role": "user", "content": "<the whole .md file>"}
]
}'
temperature: 0 compte davantage ici que dans la plupart des usages. Vous voulez que la même entrée donne la même sortie, pour que relancer le travail ne produise pas un diff fait de synonymes.
Le coût en AP de l'appel figure dans la réponse, vous n'avez donc jamais à l'estimer après coup :
X-AICU-AP-Cost: 18
X-AICU-AP-Balance: 6263161
Vérifier que la structure a survécu
Une mauvaise traduction se signale par un Markdown cassé bien avant de se signaler par une mauvaise prose. Un bloc de code dont la clôture disparaît avale le reste de la page ; un tableau qui perd sa ligne de séparation s'affiche comme un paragraphe de barres verticales.
Comparez donc la sortie à l'entrée de façon mécanique, et refusez d'écrire le fichier si la vérification échoue :
def structure_check(src: str, out: str) -> list[str]:
bad = []
if src.count("```") != out.count("```"):
bad.append("code fences")
if src.count("|---") != out.count("|---"):
bad.append("tables")
for level in ("\n## ", "\n### "):
if src.count(level) != out.count(level):
bad.append(f"headings {level.strip()}")
if src.startswith("---") and not out.startswith("---"):
bad.append("frontmatter")
for url in set(re.findall(r"https?://[^\s)\"'`]+", src)):
if url not in out:
bad.append(f"lost URL {url}")
break
if out.strip().startswith("```"):
bad.append("whole document wrapped in a fence")
return bad
Un fichier qui échoue est signalé et n'est pas écrit. Ne pas traduire une page se rattrape ; publier une page cassée, non.
Ce que cela a détecté, en pratique
Les échecs n'étaient pas des troncatures. Les deux documents ont été traduits jusqu'à la dernière ligne et se sont arrêtés normalement. Le modèle a simplement supprimé des éléments au milieu.
Un tableau entier, disparu. pricing.md est revenu plus court de 172 lignes, soit un tableau — le dernier tableau, « comment l'estimation est construite », était absent, alors que le paragraphe suivant était traduit et présent. Lignes de séparation 16 → 14, lignes de tableau 42 → 38.
Un lien réduit à du texte brut. opencode.md a conservé chaque tableau, chaque ligne et chaque titre. Dans une ligne, ceci :
| API key | `aicu_live_xxx` (issue one in the [dashboard](https://api.aicu.ai/dashboard/keys) — **required**) |
est revenu ainsi :
| API 키 | `aicu_live_xxx` (대시보드에서 발급 — **필수**) |
La phrase dit toujours au lecteur d'obtenir une clé dans le tableau de bord. Le lien vers le tableau de bord a disparu. Le nombre de lignes, de titres, de blocs de code et la longueur sont tous inchangés — seule la vérification de survie des URL voit celui-ci.
C'est l'argument pour exécuter plusieurs vérifications peu coûteuses plutôt qu'une seule vérification astucieuse. Chacun des trois échecs sur ce site a été détecté par une vérification différente, et aucune vérification n'en a détecté plus d'un.
L'une de nos propres vérifications était fausse
Cela vaut la peine d'être dit, car c'est l'autre moitié de la même leçon. Lors de la première exécution, un troisième fichier a échoué avec lost URL https://api.aicu.ai/dashboard/keys. — notez le point final. La source avait l'URL en fin de phrase, et l'expression régulière avait avalé le point comme s'il faisait partie de l'URL. La traduction terminait la phrase avec sa propre ponctuation, donc la chaîne littérale était absente et un fichier parfaitement correct a été rejeté.
url = url.rstrip(".,;:!?、。)」") # la ponctuation de la phrase ne fait pas partie de l'URL
Une vérification qui fait échouer une bonne sortie finit par être désactivée. Réglez-les pour zéro faux positif et acceptez de manquer certaines choses — les vérifications présentées ici ont été testées sur 15 traductions connues comme bonnes avant qu'on leur confie le droit de rejeter quoi que ce soit.
Ce qu'il ne faut pas vérifier
La longueur. C'est l'idée évidente, et elle ne fonctionne pas sur du Markdown : sur les 15 bonnes traductions, le rapport du nombre de lignes allait de 0.934 à 1.000, car les paragraphes anglais coupés à la main sont réunis en une seule ligne dans la langue cible. Le fichier au tableau supprimé était à 0.820 — détectable — mais le fichier au lien supprimé était à 0.974, bien à l'intérieur de la plage normale. Un seuil assez serré pour attraper le second rejetterait plusieurs bons fichiers.
Comptez plutôt les éléments de structure. Les lignes de tableau, les puces et les titres correspondaient exactement sur les 15 bonnes traductions, ce qui fait de toute différence un vrai signal :
for label, count in (("table rows", lambda t: sum(1 for l in t.splitlines() if l.lstrip().startswith("|"))),
("bullets", lambda t: sum(1 for l in t.splitlines() if re.match(r"\s*[-*] ", l))),
("headings", lambda t: sum(1 for l in t.splitlines() if l.startswith("#")))):
if count(src) != count(out):
bad.append(f"{label} {count(src)}→{count(out)}")
Le frontmatter revient intact — y compris les parties que les lecteurs voient
Le prompt système reproduit le frontmatter YAML octet pour octet, ce qui est juste pour sidebar_position, service_status et related, et faux pour title et description — ceux-là s'affichent sur la page et dans les résultats de recherche. Traduisez ces deux champs ensuite, ou exemptez-les dans le prompt. Quel que soit votre choix, vérifiez la page affichée dans la langue cible plutôt que le fichier.
Une chose qui n'est pas un échec
Le modèle traduit les chaînes en langage humain à l'intérieur des blocs de code — valeurs de prompt, commentaires — tout en laissant intacts les identifiants, les noms de paramètres et les noms de modèles :
- "prompt": "a red apple on a white table, product photo",
+ "prompt": "하얀 테이블 위에 빨간 사과, 제품 사진",
Le prompt système demande les blocs de code octet pour octet, donc à strictement parler c'est une désobéissance. C'est aussi ce qu'un lecteur coréen attend d'un exemple. Nous l'acceptons, et nous vérifions ce qui casserait réellement : le nombre de blocs de code, et le fait que "character": "SakiNoir" et gpt-4o-mini reviennent intacts. Choisissez votre camp et écrivez la vérification en conséquence — mais choisissez, plutôt que de le découvrir plus tard sur une page affichée.
Vérifier que le texte a réellement été traduit
C'est la vérification qu'on oublie facilement, et celle dont l'apprentissage nous a coûté le plus cher.
Lors d'un banc d'essai de quatre modèles en anglais → coréen, gpt-5-mini a obtenu 1.34 / 0.49 / 0.69 là où les autres obtenaient environ 1.85 / 1.55 / 0.27. On dirait une prose médiocre. Ce n'en était pas. La sortie contenait zéro caractère hangeul et faisait 2,499 caractères pour une entrée de 2,500 caractères : le modèle avait renvoyé la source anglaise inchangée.
structure_check laisse passer cette sortie parfaitement — la structure est identique puisque le document est l'original. Écrivez-la, et i18n/ko/ se remplit d'anglais portant un nom de fichier coréen. Rien en aval ne s'en plaint.
Deux tests peu coûteux la détectent :
def translated_check(src: str, out: str, locale: str) -> list[str]:
bad = []
# 1. La prose est-elle simplement la source ? (indépendant de la langue)
a, b = strip_code_and_urls(src), strip_code_and_urls(out)
same = sum(1 for x, y in zip(a, b) if x == y) / max(len(a), len(b))
if same > 0.9:
bad.append(f"source returned unchanged ({same:.0%} identical)")
# 2. L'écriture cible apparaît-elle vraiment ?
rng = TARGET_SCRIPT.get(locale) # ko : hangeul, zh : han, ja : kana
if rng and not bad:
lo, hi = rng
n = sum(1 for c in out if lo <= ord(c) <= hi)
if n / max(len(out), 1) < 0.03:
bad.append(f"almost no target-script characters ({n})")
return bad
Le premier test fonctionne pour n'importe quelle paire de langues. Le second ne fonctionne que si la langue cible a sa propre écriture — le français, l'espagnol et le portugais partagent l'alphabet latin avec la source, donc là, vous n'avez que le test d'identité. Cela suffit : un modèle qui ne traduit pas échoue en renvoyant l'écho de la source, et c'est précisément ce que le test 1 détecte.
La forme générale de la leçon mérite d'être énoncée clairement, car elle s'applique à chaque travail par lots que vous lancez sur un modèle de langage :
Un travail qui échoue bruyamment coûte peu. Un travail qui réussit faussement coûte cher. Consacrez votre effort de vérification aux sorties qui ressemblent à un succès.
Ne pas payer deux fois pour un fichier qui n'a pas changé
Enregistrez le SHA-256 de chaque fichier source à côté de la langue dans laquelle vous l'avez traduit, et ignorez tout ce qui correspond :
digest = hashlib.sha256(src.encode()).hexdigest()
if manifest.get(str(path)) == digest and dest.exists():
continue # inchangé depuis la dernière exécution — ignorer
La deuxième exécution sur une arborescence inchangée coûte 0 AP. La traduction passe ainsi d'un événement à une habitude : modifiez trois pages, relancez, payez trois pages.
Gardez le manifeste sous contrôle de version. C'est l'enregistrement de quelle traduction correspond à quelle révision de la source — la question à laquelle vous voudrez vraiment une réponse six mois plus tard.
Choisir le modèle
Mesurez, sur vos propres documents. Quatre modèles, les mêmes quatre fichiers, anglais → coréen, notés par Jev :
| modèle | fidèle | naturel | non traduit | AP / 2,500 car. | s |
|---|---|---|---|---|---|
gpt-4o-mini | 1.83 | 1.55 | 0.26 | 18 | 6.9 |
gpt-4o | 1.84 | 1.56 | 0.30 | 278 | 9.9 |
gpt-5.6-sol | 1.87 | 1.66 | 0.25 | 849 | 9.2 |
gpt-5-mini | 1.34 | 0.49 | 0.69 | 46 | 9.6 |
gpt-5.6-sol est le meilleur sur les trois axes. Il coûte aussi 47× le prix de gpt-4o-mini pour des écarts de +0.04, +0.11 et −0.01 — 970 AP contre 45,700 AP pour l'ensemble complet.
La ligne gpt-5-mini est celle de la section précédente : ce ne sont pas des notes de prose, ce sont les notes obtenues en rendant la source.
Nous utilisons gpt-4o-mini par défaut et recourons à gpt-5.6-sol pour les pages où la formulation pèse. La différence qu'il apporte est réelle mais faible, et elle se manifeste dans le registre plutôt que dans l'exactitude — fournir un sujet que l'anglais laissait implicite, couper en deux une longue phrase à tirets, choisir la particule idiomatique.
Noter la sortie
Les trois notes ci-dessus proviennent de /v1/jev, qui répond à des questions typées au lieu de renvoyer une prose qu'il faudrait ensuite analyser :
{
"state": {"source": "...", "translation": "..."},
"questions": {
"faithful": {
"type": "score",
"instructions": "Does the translation preserve the meaning of the source, including technical details?",
"criteria": ["Meaning is distorted", "Mostly faithful", "Faithful throughout"]
},
"natural": {
"type": "score",
"instructions": "Does the translation read naturally to a developer who speaks that language?",
"criteria": ["Stilted", "Readable but awkward", "Natural"]
},
"untranslated": {
"type": "noul",
"instructions": "Is there source-language prose left untranslated that should have been translated? (Code, URLs and parameter names are correctly left as-is.)"
}
}
}
Environ 8 AP par fichier : noter un ensemble complet coûte moins cher que d'en traduire une seule page.
Utilisez-le pour comparer des modèles et repérer les valeurs aberrantes, pas comme barrière. Les vérifications mécaniques décident si un fichier est écrit ; une note est un chiffre à regarder ensuite. Un modèle qui renvoie la source inchangée est détecté par quatre lignes de comptage de caractères, pas en demandant à un autre modèle si cela a l'air correct.
L'exécuter
Téléchargez translate-docs.py — un seul fichier, bibliothèque standard uniquement.
export AICU_API_KEY=aicu_live_... # scope llm
python3 scripts/translate-docs.py --to ko # dry-run : estimation du coût uniquement
python3 scripts/translate-docs.py --to ko --apply # traduire et écrire
python3 scripts/translate-docs.py --to ko --apply --judge # ... et noter chaque fichier
python3 scripts/translate-docs.py --to ko --apply --only pricing.md
python3 scripts/translate-docs.py --to ko --apply --model gpt-5.6-sol --force
Le dry-run est le comportement par défaut. Il affiche le nombre de fichiers, le nombre de caractères et l'estimation en AP, puis se termine sans rien appeler. Tout script de traitement par lots que vous écrivez pour une API payante devrait commencer ainsi.
La sortie va dans i18n/<locale>/docusaurus-plugin-content-docs/current/, là où Docusaurus l'attend. Adaptez le chemin et vous avez le même travail pour n'importe quelle arborescence de Markdown.
Ce qui vaut pour tout travail par lots
La traduction est l'exemple ; la démarche est générale.
- Estimez avant de dépenser. Dry-run par défaut,
--applypour valider. - Vérifiez la sortie mécaniquement, et refusez d'écrire ce qui échoue. Ne pas faire le travail vaut mieux que le faire mal.
- Testez les faux succès, pas seulement les erreurs. La sortie qui passe toutes les vérifications et qui reste fausse est celle qui arrive en production.
- Rendez le travail incrémental. Hachez les entrées ; inchangé veut dire gratuit.
- Choisissez le modèle en mesurant sur vos propres données. Le meilleur modèle sur tous les axes coûtait 47× le prix pour une erreur d'arrondi.
Voir aussi
- API Chat (LLM) — modèles, en-têtes, limites
- Jev — notation typée sans analyse de prose
- Tarifs — comment les AP sont facturés et lus dans les en-têtes de réponse