Précédemment, j'ai écrit un article intitulé Multilingue d'un site Astro SSG avec l'API Google Translate. Le système utilisait l'API Google Cloud Translation pour traduire des HTML statiques après la construction et générer des versions dans chaque langue. C'était suffisamment pratique comme approche multilingue économique.
Cependant, après une période d'exploitation, j'ai remarqué des points gênants tant au niveau du SEO que de la qualité de traduction. Pour comprendre « pourquoi nous avons abandonné Google Translate », consultez l'article séparé.
Article : Migration de la traduction automatique de Google Translate vers Claude API
Cet article traite de « comment nous avons réellement réimplémenté » sur le plan technique. Nous avons remplacé le moteur de traduction de Google Translate par un LLM (Claude API), et nous avons également éliminé les tâches manuelles d'exploitation qui restaient en attente.
Traduction par LLM lors de la construction, cache différentiel sur R2
La stratégie de base reste la même que précédemment : la traduction s'effectue au moment de la construction (côté serveur). Nous n'appelons pas Claude API depuis le navigateur. La différence réside dans le moteur de traduction et l'emplacement du cache.
Le flux de construction fonctionne comme suit.
npm run build
├── fetch-microcms (microCMSから記事データ取得)
├── astro build (日本語HTMLを生成 → dist/)
├── translate (各ロケールのHTMLを生成 → dist/{locale}/)
└── update-xml (sitemap更新)
Ce que fait l'intérieur de translate (translate-html-llm.mjs) se déroule grosso modo dans cet ordre.
- Téléchargement du cache de traduction (un seul fichier JSON) depuis Cloudflare R2
- Lire chaque HTML sous
dist/avec cheerio et extraire le texte à traduire - Utiliser le cache s'il existe, sinon envoyer uniquement les éléments manquants à l'API Claude
- Remplacer le texte par les résultats traduits et écrire dans
dist/{locale}/ - Fusionner les nouvelles traductions dans le cache et télécharger vers R2
Les deux points clés sont la « traduction différentielle » — traduire uniquement ce qui a changé — et héberger le cache sur R2.
① Comment rendre naturelles les traductions qui chevauchent des balises inline
C'est précisément ce que j'ai voulu améliorer le plus cette fois-ci.
Par exemple, supposons que le contenu contienne ce HTML.
<p>私たちは<strong>ウェブアクセシビリティ</strong>を重視しています</p>
Si on traite la traduction normalement, on finit par traduire trois fragments séparés : nous / web accessibility / valorisons. Comme l'ordre des mots diffère entre le japonais et le français, replacer les fragments traduits à leur position d'origine provoque des décalages dans les emplacements des balises <strong>, ou rend simplement la phrase contre nature. Plus il y a de balises inline, plus c'est cassé. C'était le plus grand problème de l'ancien système.
L'amélioration se fait en deux étapes.
Première étape : fragmenter par éléments de bloc. L'unité minimale de traduction n'est pas l'«espace entre les balises», mais le contenu entier (innerHTML) d'éléments de bloc comme p, h1–h6, li, td, blockquote. On ne divise pas les phrases.
Deuxième étape : remplacer les balises en ligne par des marqueurs, puis transmettre la phrase entière comme une seule unité de traduction. Les <strong> et <a> dans le fragment sont d'abord remplacés par des marqueurs comme ....
私たちは[[T1]]ウェブアクセシビリティ[[/T1]]を重視しています
On indique au LLM : « Ceci est une seule phrase. Traduisez-la naturellement et réappliquez les mêmes marqueurs aux termes à mettre en évidence. Vous pouvez déplacer les marqueurs en fonction de l'ordre des mots de la traduction. » Après réception de la traduction, les marqueurs sont restaurés aux <strong> et <a href="..."> d'origine. Les attributs (href ou class) sont conservés tels quels.
Ainsi, <strong> s'applique au bon mot en anglais également, et la phrase est naturelle.
② Conception de la traduction différentielle et de la mise en cache R2
Traduire tout à chaque fois serait un coût insoutenable. À la fois précédente, j'avais un cache appelé translate-cache.json, mais cette fois, j'ai revu la façon de créer les clés et leur emplacement de stockage.
Système de régénération de la traduction lorsque le prompt est modifié
La clé du cache est créée en combinant le texte japonais d'origine, la locale et le contenu du prompt de traduction. De cette façon, si le texte japonais change, seule cette entrée est retraduite ; si les instructions de traduction ou la politique terminologique (prompt) changent, toutes les entrées sont retraduits automatiquement puisqu'elles sont traitées comme « absentes du cache ».
L'objectif est d'éviter l'accident où une ancienne traduction reste en cache même après amélioration du prompt. En pratique, je crée une chaîne de caractères en utilisant sha256 pour hacher ces éléments comme clé.
Voici la structure du contenu du cache.
{"<sha256のキー>":{"value":"翻訳結果(マーカー込み)","locale":"en","model":"claude-haiku-4-5-20251001","translatedAt":"2026-06-12T..."}}
Nous avons placé le cache dans R2 et éliminé les tâches manuelles.
Voici la récupération du travail en attente depuis la dernière fois.
La dernière fois, nous gérions le cache via un fichier JSON dans le référentiel. Par conséquent, lorsque nous ajoutions un article via le CMS et déclenchions une génération via webhook, le serveur ne pouvait voir que l'ancien cache sur Git. Nous avons dû suivre une règle d'exploitation où « lorsque vous ajoutez un article, vous effectuez une génération locale, puis vous poussez le fichier cache mis à jour vers Git ».
Cette fois, nous avons placé le cache dans un seul blob JSON sur Cloudflare R2. À chaque génération, nous le récupérons depuis R2, et une fois terminé, nous le réécrivons. Cela permet au cache de persister même lors de générations via webhook, et les tâches manuelles de génération locale puis de push ont complètement disparu. Il suffit d'ajouter un article et de le pousser : seules les nouvelles parties sont traduites, et le cache se met à jour automatiquement.
Priorité de traduction
Les variations de termes (comme les différentes façons d'écrire le nom de l'entreprise Liberogic) étaient une source de préoccupation la dernière fois aussi. Cette fois, nous les traitons en quatre étapes.
- Remplacement manuel(
data-i18n-key) — Les éléments HTML marqués avecdata-i18n-keysont fixés avec une traduction écrite à l'avance. C'est une approche où nous ne comptons ni sur l'LLM ni sur le dictionnaire, et où nous pouvons dire « pour cet endroit précis, je veux absolument cette traduction ». - Dictionnaire de termes (glossaire) — Pour les étiquettes fixes qui réapparaissent comme les menus et les titres de pages, nous fixons la traduction des termes dans un dictionnaire JSON. Si nous modifions le dictionnaire et reconstruisons, les changements sont appliqués immédiatement.
- Cache R2 — S'il ne figure dans aucune des deux catégories précédentes, nous consultons le cache.
- API Claude — Seuls les éléments qui ne figurent nulle part ailleurs sont envoyés en dernier à l'LLM.
L'uniformisation des termes qui apparaissent dans le texte ne peut pas être entièrement capturée par un dictionnaire classique (qui ne gère pas les correspondances partielles), nous les harmonisons donc en fonction des indices terminologiques fournis par le système d'invite.
Gérer les « habitudes » des LLM
La traduction automatique produit des traductions mauvaises, c'était aussi le cas la fois précédente, mais les LLM ont leurs propres habitudes. Nous vous en présentons quelques-unes visibles dans nos opérations réelles.
- Les phrases techniques restent entièrement en anglais. Il arrive que nous appliquions une « liste de termes à ne pas traduire » (comme API, React, Vue) de manière excessive et que le modèle retourne la phrase entière en anglais. Nous le résolvons en renforçant l'instruction dans l'invite utilisateur pour que la sortie soit entièrement en langue cible.
- La position des marqueurs s'inverse sémantiquement. Il arrive que le modèle se trompe sur le terme à mettre en avant et que
<strong>encadre une plage incorrecte. Supprimer cette entrée et la retranslatger la corrige. - La réponse s'interrompt au milieu pour les langues CJK. Les caractères CJK consomment davantage de jetons par caractère, et lorsqu'on en traite un lot, la sortie atteint la limite et le JSON se corrompt. Nous augmentons
max_tokenset réduisons le nombre d'éléments par lot pour y remédier. - Dégradation des sauts de ligne littéraux
et des dates CJK. Des sauts de ligne peuvent s'introduire sous forme de chaîne, ou des espaces inutiles peuvent apparaître dans les dates comme2026 年 05 月 22 日. Nous nettoyons tout cela en masse avec des scripts de post-traitement.
Un enseignement opérationnel qui s'est avéré utile : le taux de réussite est le plus élevé en supprimant et retradusant les parties incorrectes une par une. Lorsque nous tentons de corriger plusieurs problèmes à la fois, le LLM tend à appliquer le même jugement erroné à la même structure partout. Lentement mais sûrement, nous avons réussi.
Considérations de coût
Nous avons unifié le moteur sur Claude Haiku 4.5. Nous privilégions le coût et en cas de problème de qualité, nous commençons par améliorer le système d'invite et le dictionnaire. Nous utilisons la mise en cache d'invite pour compresser le coût de la partie fixe qui se répète à chaque appel.
Voici à quoi ressemblent les estimations réelles.
Contenu | Coût |
|---|---|
Traduction complète initiale pour une locale | Environ 2,50 $ à 3 $ |
Traduction complète initiale pour toutes les locales | Déploiement standard (accès au cache uniquement) |
Déploiement normal (hits de cache uniquement) | Pratiquement 0 $ |
Ajout d'un article (quelques traductions) | Environ 0,01 $ |
Les déploiements quotidiens coûtent presque rien, et l'ajout d'articles ne coûte que 1 à quelques centimes. Bien que la refonte initiale ait nécessité un investissement important, une fois cette étape franchie, les coûts d'exploitation se sont même allégés par rapport à avant.
Conclusion
Nous avons remplacé Google Traduction par une traduction basée sur LLM, ce qui a entraîné des coûts d'exploitation très raisonnables et une amélioration notable de la qualité de traduction.
Le recours à LLM ne signifie pas que tout est automatique et parfait. Les ajustements méticuleux pour identifier et corriger les anomalies persistent. Cependant, puisque les points à modifier se concentrent désormais sur des endroits clairs — le prompt et le dictionnaire — les cycles d'amélioration sont devenus plus fluides.
Passé du DTP au monde du web, il s'est avéré être un « sage des techniques » maîtrisant le markup, le frontend, la direction et l'accessibilité. Actif depuis la fondation de Liberogic, il est devenu une référence incontournable en interne. Récemment, il explore l'optimisation via des prompts IA, se demandant « Pourrions-nous déléguer davantage la conformité en accessibilité à l'IA ? ». Sa technologie et sa réflexion continuent d'évoluer.
Ayumu Futamata
Spécialiste en accessibilité web certifié par l'IAAP (WAS) / Ingénieur markup / Ingénieur frontend / Directeur web