Je vous ai précédemment présenté comment rendre un site Astro SSG multilingue avec l'API de traduction Google. Le principe consistait à utiliser Google Cloud Translation API pour traduire le HTML statique généré après le build et produire une version pour chaque langue. Pour une approche multilingue simple et peu coûteuse, c'était tout à fait satisfaisant.
Cependant, après quelque temps d'exploitation, certains points sont devenus préoccupants, tant pour le référencement que pour la qualité des traductions. Pour comprendre pourquoi nous avons abandonné Google Traduction, veuillez consulter un autre article.
Article : Migration de la traduction automatique de Google Traduction vers Claude API
Cet article présente comment nous l'avons concrètement reconstruit. Nous avons remplacé le moteur Google Traduction par un LLM (Claude API) et supprimé les opérations manuelles d'exploitation qui étaient restées en suspens lors de la précédente version.
Traduction par LLM au moment du build, cache différentiel sur R2
Le principe reste le même que précédemment : la traduction s'effectue entièrement au moment du build, côté serveur. Nous n'appelons pas Claude API depuis le navigateur. Ce qui change, ce sont le moteur de traduction et l'emplacement du cache.
Voici le déroulement du build.
npm run build
├── fetch-microcms (microCMSから記事データ取得)
├── astro build (日本語HTMLを生成 → dist/)
├── translate (各ロケールのHTMLを生成 → dist/{locale}/)
└── update-xml (sitemap更新)
Voici, en gros, les étapes effectuées par translate et son script translate-html-llm.mjs, dans cet ordre.
- Télécharger depuis Cloudflare R2 le cache de traduction (un fichier JSON unique)
- Extraire le texte à traduire de chaque HTML situé sous
dist/à l'aide de cheerio - Utiliser le cache lorsqu'il existe et n'envoyer à l'API Claude que les éléments qui n'y figurent pas
- Remplacer le texte par la traduction obtenue et l'écrire dans
dist/{locale}/ - Fusionner les nouvelles traductions dans le cache puis le téléverser sur R2
Les deux points clés sont la traduction différentielle, qui ne traduit que ce qui a changé, et le stockage de ce cache sur R2.
① Comment rendre naturelles les traductions qui chevauchent des balises inline
C'est le point que nous souhaitions surtout améliorer.
Prenons par exemple un texte de corps contenant ce HTML.
<p>私たちは<strong>ウェブアクセシビリティ</strong>を重視しています</p>
Avec un traitement de traduction classique, on traduirait séparément trois fragments : 私たちは / ウェブアクセシビリティ / を重視しています. Le japonais et l'anglais ayant un ordre des mots différent, replacer les fragments traduits à leur position d'origine décale les endroits où <strong> s'applique, et rend parfois la phrase elle-même peu naturelle. Plus une phrase contient de balises inline, plus elle se dégrade. C'était la principale faiblesse de l'ancien système.
L'amélioration repose sur les deux étapes suivantes.
1re étape : découper par éléments de bloc. L'unité minimale de traduction n'est pas « l'espace entre les balises », mais le contenu complet (innerHTML) des éléments de bloc tels que p, h1 à h6, li, td et blockquote. Les phrases ne sont pas fragmentées.
2e étape : remplacer les balises en ligne par des marqueurs, pour transmettre la phrase entière comme une seule unité de traduction. Les <strong> et <a> présents dans le fragment sont d'abord remplacés par des marqueurs tels que ....
私たちは[[T1]]ウェブアクセシビリティ[[/T1]]を重視しています
Le LLM reçoit la consigne suivante : « Il s'agit d'une seule phrase. Traduis-la naturellement, puis réapplique les mêmes marqueurs sur les termes à mettre en valeur. Tu peux déplacer les marqueurs pour suivre l'ordre des mots de la traduction. » Une fois la traduction reçue, les marqueurs sont restaurés en <strong> et <a href="..."> d'origine. Les attributs (href et class) sont conservés tels quels.
Ainsi, <strong> s'applique désormais aux bons mots côté anglais également, et la phrase reste naturelle.
② Conception de la traduction différentielle et du cache R2
Si tout était retraduit à chaque fois, le coût deviendrait ingérable. La version précédente disposait déjà d'un cache, translate-cache.json, mais cette fois nous avons revu la manière de construire les clés et l'emplacement de stockage.
Un mécanisme qui refait aussi les traductions lorsque le prompt change
Les clés du cache sont construites à partir de la combinaison « japonais d'origine + locale + contenu du prompt de traduction ». Ainsi, si le texte japonais change, seule cette entrée est considérée comme absente du cache, et si les instructions de traduction ou la politique terminologique (le prompt) changent, toutes les entrées sont automatiquement considérées comme absentes et retraduites.
L'objectif est d'éviter l'erreur consistant à améliorer le prompt tout en laissant d'anciennes traductions persister dans le cache. En pratique, ces éléments sont hachés avec sha256, et la chaîne obtenue sert de clé.
La structure du cache est la suivante.
{"<sha256のキー>":{"value":"翻訳結果(マーカー込み)","locale":"en","model":"claude-haiku-4-5-20251001","translatedAt":"2026-06-12T..."}}
Mise en cache dans R2 et suppression des manipulations manuelles
Voici la reprise des points restés en suspens lors de la dernière fois.
La fois précédente, le cache était géré sous forme de fichiers JSON dans le dépôt. Par conséquent, lorsqu'un article était ajouté dans le CMS et que la compilation était déclenchée par le webhook de déploiement, le serveur ne pouvait accéder qu'au cache obsolète présent sur Git. Faute de mieux, nous avons contourné le problème avec une règle d'exploitation : après l'ajout d'un article, on lançait la compilation en local, puis on poussait le fichier de cache mis à jour sur Git.
Cette fois, le cache est stocké dans un blob JSON unique sur Cloudflare R2. À chaque compilation, il est récupéré depuis R2, puis réécrit une fois le traitement terminé. Ainsi, même pour les compilations déclenchées par webhook, le cache est persistant, et la manipulation manuelle consistant à compiler en local puis à pousser a complètement disparu. Il suffit d'ajouter un article et de pousser : seule la partie nouvelle est traduite, et le cache se met à jour automatiquement.
Priorité des traductions
Les variations de traduction (par exemple les écarts de graphie du nom de la société Liberogic) posaient déjà problème la fois précédente. Cette fois, le traitement se fait en quatre étapes.
- Substitution manuelle (
data-i18n-key) ― Pour les éléments HTML auxquels on a attribuédata-i18n-key, la traduction est fixée à l'avance, rédigée à la main. Cela permet de traiter les cas où l'on veut à tout prix cette traduction précise, sans laisser la décision au LLM ni au glossaire. - Glossaire terminologique ― Les libellés fixes qui reviennent régulièrement, comme ceux de la navigation ou les titres de page, sont figés dans le glossaire JSON. Il suffit de modifier le glossaire puis de recompiler pour que la modification soit appliquée immédiatement.
- Cache R2 ― Si aucune des solutions précédentes ne s'applique, on consulte le cache.
- API Claude ― Uniquement pour les éléments qui ne sont couverts par aucune des solutions précédentes, on fait appel en dernier recours au LLM.
L'harmonisation de la terminologie dans le texte ne peut pas être assurée par un simple dictionnaire ponctuel (les correspondances partielles ne sont pas prises en charge). Nous l'alignons donc de manière souple via des indications terminologiques dans le prompt système.
Composer avec les « travers » du LLM
Comme pour la fois précédente, la traduction automatique produit parfois des résultats incohérents, car les LLM ont leurs propres travers. Voici quelques-uns de ceux que nous avons observés en production.
- Des phrases techniques restent entièrement en anglais. Il arrive que le modèle applique trop largement la liste des termes à ne pas traduire (API, React, Vue) et renvoie la phrase entière en anglais. Nous y remédions en insistant davantage, dans le prompt utilisateur, sur la langue de sortie attendue.
- Les marqueurs se retrouvent inversés sur le plan du sens. Le modèle peut mal identifier le mot à mettre en valeur, et
<strong>s'applique alors à une portée inadaptée. Il suffit généralement de supprimer l'entrée concernée et de la retraduire. - Les réponses en langues CJK sont tronquées en cours de route. Les caractères kanji consomment davantage de jetons par caractère ; lorsqu'on envoie un lot trop important, la sortie atteint la limite et le JSON se casse. Nous avons augmenté
max_tokenset réduit le nombre d'éléments par lot. - Séquences littérales
et dates CJK mal formées.Des sauts de ligne se glissent parfois sous forme de chaîne de caractères, et2026 年 05 月 22 日apparaît avec des espaces superflus. Ces problèmes sont corrigés en bloc par un script de post-traitement.
Une leçon pratique : la méthode la plus efficace consiste à supprimer les entrées problématiques une par une puis à les retraduire. Tenter de corriger plusieurs erreurs en même temps incite le LLM à mal juger de la même manière des structures identiques. Aller lentement permet d'aller plus vite.
Les coûts
Nous utilisons Claude Haiku 4.5 comme moteur unique. Privilégiant le coût, nous commençons par améliorer le prompt et le dictionnaire lorsque la qualité pose problème. Le prompt système bénéficie du prompt caching, ce qui réduit le coût de la partie fixe répétée à chaque appel.
Voici les ordres de grandeur mesurés.
Contenu | Coût |
|---|---|
Traduction complète initiale d'une locale | Env. 2,5 à 3 $ |
Traduction complète initiale de toutes les locales | Env. 20 $ |
Déploiement standard (cache uniquement) | Quasi 0 $ |
Ajout d'1 article (quelques traductions) | Env. 0,01 $ |
Les déploiements quotidiens coûtent presque rien, et l'ajout d'articles ne revient qu'à quelques yens, voire moins. La reconstruction initiale demande un investissement conséquent, mais une fois cette étape franchie, les coûts de fonctionnement sont devenus plus légers qu'avant.
Conclusion
Le passage de Google Traduction à la traduction par LLM n'a pas entraîné de coûts d'exploitation importants, et la qualité des traductions s'est nettement améliorée.
Un LLM ne produit pas des résultats parfaits et automatiques pour autant. Un travail d'ajustement méthodique reste nécessaire pour repérer les travers et les corriger. Cependant, comme les points à modifier sont désormais regroupés dans des endroits clairs, à savoir le prompt et le glossaire, le cycle d'amélioration est devenu plus facile à mener.
Passé du DTP au Web, il maîtrise aussi bien le balisage que le frontend, la direction de projet et l'accessibilité : un véritable « sage de la technique ». Actif chez Liberogic depuis ses débuts, il est aujourd'hui une référence interne incontournable. Ces derniers temps, il explore avec passion l'optimisation par l'IA : « Ne pourrait-on pas davantage s'appuyer sur l'IA pour l'accessibilité ? » Sa technique comme sa réflexion continuent d'évoluer.
Ayumu Futamata
Spécialiste certifié en accessibilité web IAAP (WAS) / Intégrateur balisage / Ingénieur frontend / Directeur web