Topics

Comment nous avons remplacé Google Translate API par la traduction LLM ― Multilingue avec Claude API + cache différentiel R2

  • column

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.

  1. Téléchargement du cache de traduction (un seul fichier JSON) depuis Cloudflare R2
  2. Lire chaque HTML sous dist/ avec cheerio et extraire le texte à traduire
  3. Utiliser le cache s'il existe, sinon envoyer uniquement les éléments manquants à l'API Claude
  4. Remplacer le texte par les résultats traduits et écrire dans dist/{locale}/
  5. 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.

  1. Remplacement manuel( data-i18n-key) — Les éléments HTML marqués avec data-i18n-key sont 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 ».
  2. 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.
  3. Cache R2 — S'il ne figure dans aucune des deux catégories précédentes, nous consultons le cache.
  4. 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_tokens et 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 comme 2026 年 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.

Auteur de cet article

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

Voir les articles de ce membre

Notre équipe fiable et nos capacités de réactivité font notre fierté

Chez Liberogic, nos équipes expérimentées sont reconnues pour diriger activement les projets et sont hautement appréciées par nos clients.
Nous assignons correctement un chef de projet et un directeur, et veillons à assurer le déroulement fluide de l'ensemble du projet. Nous évitons une augmentation inutile des coûts en engagements complets, en allouant les ressources de manière optimale. Notre approche est réputée pour sa rapidité dans la compréhension des besoins, la création et la soumission des devis.

* Veuillez noter que nous n'engageons pas activement de missions d'intégration type SES.

Slack, Teams, Redmine, Backlog, Asana, Jira, Notion, Google Workspace, Zoom, Webex, et pratiquement tous les principaux outils de gestion de projet et de communication que vous utilisez.

Consultez-nous pour toute question ou préoccupation concernant le web.

Études de cas