Topics

Waarom we de Google Translate API hebben vervangen door LLM-vertaling – meertalig maken met Claude API en R2-differentiële caching

  • column

Eerder schreef ik een artikel over het meertalig maken van een Astro SSG-site met de Google Translate API. Hierbij werd Google Cloud Translation API gebruikt om de statische HTML na de build te vertalen en zo versies in elke taal te genereren. Als eenvoudige en goedkope manier om meertalig te worden, werkte dit prima in de praktijk.

Na enige tijd in productie kwamen er echter punten naar voren die me zowel qua SEO als qua vertaalkwaliteit zorgden. Waarom we Google Translate hebben stopgezet, lees je in een afzonderlijk artikel.

Artikel: Machinevertaling migreren van Google Translate naar Claude API

Dit artikel gaat over de implementatie: hoe we het opnieuw hebben opgebouwd. We hebben de vertaalengine vervangen door een LLM (Claude API) en tegelijkertijd de handmatige operationele stappen weggewerkt die we vorig keer nog open hadden laten staan.

Vertaling tijdens de build met LLM, differentiële cache in R2

Het uitgangspunt is hetzelfde als vorige keer: de vertaling wordt volledig aan de serverkant tijdens de build uitgevoerd. We roepen de Claude API niet vanuit de browser aan. Wat wel anders is, is de vertaalengine en de plek waar de cache wordt opgeslagen.

De buildflow ziet er als volgt uit.

npm run build
  ├── fetch-microcms   (microCMSから記事データ取得)
  ├── astro build      (日本語HTMLを生成 → dist/)
  ├── translate        (各ロケールのHTMLを生成 → dist/{locale}/)
  └── update-xml       (sitemap更新)

Wat translate (in translate-html-llm.mjs) in grote lijnen doet, verloopt in deze volgorde.

  1. Het vertaalcachebestand (een enkel JSON-bestand) downloaden van Cloudflare R2
  2. Lees elk HTML-bestand onder dist/ in met cheerio en haal de teksten op die vertaald moeten worden
  3. Gebruik de cache als die bestaat en stuur alleen de ontbrekende teksten naar de Claude API
  4. Vervang de tekst door de vertaling en schrijf het resultaat weg naar dist/{locale}/
  5. Voeg de nieuw vertaalde teksten samen met de cache en upload die naar R2

Het gaat om twee punten: alleen vertalen wat is veranderd (differentiële vertaling) en de cache opslaan in R2.

① Hoe je vertalingen natuurlijk maakt wanneer ze inline tags overspannen

Dit is precies wat we het liefst wilden verbeteren.

Stel dat de hoofdtekst de volgende HTML bevat.

<p>私たちは<strong>ウェブアクセシビリティ</strong>を重視しています</p>

Als je de vertaling gewoon zo verwerkt, worden 私たちは / ウェブアクセシビリティ / を重視しています als drie losse fragmenten apart vertaald. Omdat de woordvolgorde in het Japans en het Engels verschilt, komt de tag <strong> bij het terugplaatsen van de vertaalde fragmenten op de verkeerde plek te staan, of klinkt de zin gewoon onnatuurlijk. Hoe meer inline tags een zin heeft, hoe erger het misgaat. Dit was het grootste probleem van het oude systeem.

De verbetering bestaat uit twee stappen.

1e: chunk per blokelement. De kleinste vertaaleenheid is niet de 'ruimte tussen tags', maar de volledige inhoud (innerHTML) van blokelementen zoals p, h1 t/m h6, li, td en blockquote. Zinnen worden niet gesplitst.

2e: vervang inline tags door markers, zodat de volledige zin als één vertaaleenheid wordt aangeboden. Inline <strong> en <a> binnen de chunk worden eerst vervangen door markers zoals ....

私たちは[[T1]]ウェブアクセシビリティ[[/T1]]を重視しています

Het LLM krijgt de instructie: 'Dit is één zin. Vertaal deze natuurlijk en plaats dezelfde markers opnieuw op de woorden die nadruk verdienen. De positie van de markers mag worden aangepast aan de woordvolgorde van de vertaling.' Wanneer de vertaling terugkomt, worden de markers hersteld naar de oorspronkelijke <strong> en <a href="...">. Attributen (zoals href of class) blijven behouden.

Zo wordt <strong> nu ook aan de Engelse kant correct aan het juiste woord gekoppeld, en leest de zin natuurlijk.

② Ontwerp van differentiële vertaling en R2-cache

Als alles telkens opnieuw vertaald werd, zouden de kosten snel uit de hand lopen. Vorige keer gebruikten we ook al een cache, translate-cache.json, maar dit keer hebben we de manier waarop sleutels worden gemaakt en waar ze worden opgeslagen herzien.

Een mechanisme dat vertalingen opnieuw maakt wanneer de prompt wordt aangepast

De cachesleutel wordt opgebouwd uit het oorspronkelijke Japanse tekst, de locale en de inhoud van de vertaalprompt. Wanneer de Japanse tekst verandert, wordt alleen dat item opnieuw vertaald. Wanneer de vertaalinstructies of terminologiebeleid (prompt) veranderen, worden automatisch alle items als 'niet in cache' behandeld en opnieuw vertaald.

Het doel is te voorkomen dat een verbeterde prompt wordt genegeerd omdat oude vertalingen in de cache blijven staan. In de praktijk wordt de combinatie gehasht met SHA-256 en die hash wordt als sleutel gebruikt.

De inhoud van de cache ziet er als volgt uit.

{"<sha256のキー>":{"value":"翻訳結果(マーカー込み)","locale":"en","model":"claude-haiku-4-5-20251001","translatedAt":"2026-06-12T..."}}

Cache opgeslagen in R2 en handmatig werk afgeschaft

Hier pakken we de openstaande punten van vorige keer op.

Vorige keer werd de cache beheerd in een JSON-bestand in de repository. Wanneer je in de CMS een artikel toevoegde en een build via de deploy hook startte, kon de server dus alleen de oude cache in Git zien. Om dat op te lossen hadden we een werkafspraak: na het toevoegen van een artikel bouwen we lokaal en pushen we het bijgewerkte cachebestand naar Git.

Nu staat de cache als één JSON-blob in Cloudflare R2. Bij elke build wordt deze uit R2 opgehaald en na afloop teruggeschreven. Hierdoor blijft de cache ook bewaard bij builds via webhook, en is de handmatige stap van lokaal bouwen en pushen volledig verdwenen. Voeg een artikel toe en push: alleen de nieuwe content wordt vertaald en de cache wordt automatisch bijgewerkt.

Prioriteit van vertalingen

Inconsistente vertalingen, zoals wisselende schrijfwijzes van de bedrijfsnaam Liberogic, waren ook vorige keer al een probleem. Nu verwerken we vertalingen in vier stappen.

  1. Handmatige overschrijving (data-i18n-key) ― Elementen in de HTML die een data-i18n-key krijgen, worden vastgezet op een vooraf geschreven vertaling. Zo hoeft er niet naar de LLM of het woordenboek te worden gekeken, en kun je exact bepalen welke vertaling op die plek moet staan.
  2. Woordenlijst (glossary) ― Vaste labels die vaak voorkomen, zoals navigatie-items en paginatitels, krijgen een vaste vertaling in het JSON-woordenboek. Pas het woordenboek aan en bouw opnieuw, dan is de wijziging direct zichtbaar.
  3. R2-cache ― Als er in geen van de bovenstaande bronnen een vertaling staat, wordt de cache geraadpleegd.
  4. Claude API ― Alleen voor teksten die nergens anders gevonden worden, wordt als laatste stap de LLM gebruikt.

Consistent terminology across the body text can't be fully handled by a simple dictionary lookup (partial matches aren't supported), so we loosely align it through terminology hints in the system prompt.

Working with the quirks of LLMs

Machine translation producing odd translations was also an issue last time, but LLMs have their own quirks. Here are a few things we've noticed in practical use.

  • Entire technical sentences are left in English. By over-applying the list of "do not translate" terms such as API, React, and Vue, the model sometimes returns whole sentences in English. We addressed this by strongly reinforcing in the user prompt that output must be in the target locale's language.
  • Marker positions get semantically reversed. The model misjudges which words should be emphasized, causing <strong> to wrap the wrong span. Deleting the affected entry and retranslating it fixes the problem.
  • Responses get cut off mid-way for CJK languages. Kanji consume more tokens per character, so when sent in bulk, the output hits the limit and the JSON breaks. We raised max_tokens and reduced the number of items per batch to handle this.
  • Literal and broken CJK dates. Line breaks sometimes leak in as the literal string , or extra spaces appear in dates like 2026 年 05 月 22 日. We clean these up in bulk with a post-processing script.

One operational lesson that worked well: fixing problematic entries one at a time and retranslating them has the highest success rate. When trying to correct several at once, the LLM tends to misjudge the same structure in the same way across the batch. Slow and steady won the race.

About costs

We've standardized on Claude Haiku 4.5 as the engine. Since cost is a priority, our approach is to improve quality issues first through the prompt and dictionary. The system prompt uses prompt caching so the fixed portion repeated on every request costs less.

Dit zijn de ongeveer verwachte kosten op basis van metingen.

Inhoud

Kosten

Eerste volledige vertaling van één locale

ca. $2,5-3

Eerste volledige vertaling voor alle locales

ca. $20

Reguliere deployment (alleen cache-hits)

bijna $0

Eén artikel toegevoegd (enkele vertalingen)

ca. $0,01

Day-to-day deployments are nearly free, and adding an article costs only a few yen. The initial rebuild requires a sizable investment, but once that hurdle is passed, running costs have actually become lighter than before.

Conclusie

Overstap van Google Translate naar LLM-vertaling heeft geleid tot lagere operationele kosten en een aanzienlijk betere vertaalkwaliteit.

Omdat het een LLM is, wordt niet alles automatisch perfect. Het blijft nodig om steeds opnieuw eigenaardigheden op te sporen en te corrigeren. Maar doordat de aanpassingen zijn samengebracht op de duidelijke plekken, namelijk de prompt en het woordenboek, verloopt de verbetercyclus een stuk soepeler.

Over de auteur

Van DTP naar het web: al snel werkte hij aan markup, frontend, projectregie en accessibility, en beheerst hij alles. Actief bij Liberogic sinds de beginjaren en nu een levende encyclopedie binnen het bedrijf. Recentelijk verdiept hij zich, met prompts als hulpmiddel, in de vraag: "Kunnen we voor accessibility niet meer op AI leunen?" Technologie en denkwijze blijven zich ontwikkelen.

Ayumu Futamata

IAAP-gecertificeerd webaccessibility-specialist (WAS) / Markup-engineer / Frontend-engineer / Webdirecteur

Bekijk de artikelen van deze medewerker

Ons grootste pluspunt is een betrouwbaar team en een snelle reactie

Bij Liberogic wordt er door onze ervaren medewerkers proactief aan projecten gewerkt, waardoor klanten ons hoog waarderen.
We zorgen ervoor dat er zorgvuldig een projectmanager en een director worden toegewezen, zodat het gehele project soepel verloopt. We voorkomen onnodige kostenstijging door middel van volledige inzet en verdelen de middelen op een manier die past bij de taak. Ook staan we bekend om ons snelle inzicht in de werkzaamheden en het snel opstellen en indienen van offertes.

* Liberogic zet zich niet in voor SES-achtige detacheringsopdrachten op locatie bij klanten. Houd hier alstublieft rekening mee.

U kunt vrijwel alle belangrijke tools voor projectmanagement en chat gebruiken, waaronder Slack, Teams, Redmine, Backlog, Asana, Jira, Notion, Google Workspace, Zoom, Webex en meer.

Neem gerust contact op met uw vragen over het web.

Casestudies