Eerder schreef ik een artikel over meertaligheid van Astro SSG-sites met Google Translate API. Dit was een systeem dat statische HTML na de build vertaalde met Google Cloud Translation API om versies in verschillende talen te genereren. Als goedkope benadering voor meertaligheid was het volkomen praktisch.
Na enige tijd merkte ik echter punten op die zowel vanuit SEO als vertaalkwaliteit zorgwekkend waren. Kijk in een ander artikel voor het verhaal over "waarom we Google Translate hebben stopgezet".
Artikel: Migratie van automatische vertaling van Google Translate naar Claude API
Dit artikel gaat over "hoe we het eigenlijk hebben herbouwd" — de implementatiedetails. We vervingen de engine van Google Translate door een LLM (Claude API) en eliminerden daarbij ook de handmatige werkzaamheden die eerder achter waren gebleven.
Vertalen met LLM tijdens de build, differentiaalbuffering in R2
De basisaanpak is hetzelfde als voorgaande keer: vertaling vindt plaats tijdens de build (aan serverzijde). We roepen Claude API niet aan vanuit de browser. Wat verschilt is de vertaalengine en waar de buffering is opgeslagen.
De buildstroom ziet er als volgt uit.
npm run build
├── fetch-microcms (microCMSから記事データ取得)
├── astro build (日本語HTMLを生成 → dist/)
├── translate (各ロケールのHTMLを生成 → dist/{locale}/)
└── update-xml (sitemap更新)
Wat translate doet (binnenin translate-html-llm.mjs) gebeurt ruwweg in deze volgorde.
- Download het vertaalbuffer (één JSON-bestand) van Cloudflare R2
- Laad elk HTML-bestand onder
dist/in met cheerio en extraheer de tekst die moet worden vertaald - Als het in de cache aanwezig is, gebruik dit; voor ontbrekende items stuurt u alleen naar Claude API
- Vervang tekst door vertaalde resultaten en schrijf naar
dist/{locale}/ - Merge de nieuw vertaalde inhoud in de cache en upload naar R2
Het sleutelaspect is tweeledig: alleen de gewijzigde onderdelen vertalen (verschilvertaling) en de cache in R2 plaatsen.
① Hoe natuurlijke vertalingen maken wanneer inline tags voorkomen
Dit was de belangrijkste verbetering die ik graag wilde doorvoeren.
Stel bijvoorbeeld dat de brondtekst dit HTML bevat.
<p>私たちは<strong>ウェブアクセシビリティ</strong>を重視しています</p>
Als we normaal met vertaling zouden omgaan, zouden wij / webtoegankelijkheid / serieus nemen drie afzonderlijke fragmenten apart vertalen. Omdat het Nederlands en het Japans verschillende woordvolgorde hebben, zouden de vertaalde fragmenten op hun oorspronkelijke plaats niet natuurlijk overkomen en zou <strong> op verkeerde plaatsen terechtkomen. Hoe meer inline-tags in een zin, hoe meer het kapotgaat. Dit was de grootste frustratie met het oude systeem.
De verbeteringsmethode bestaat uit twee fasen.
Eerste stap: chunk de inhoud per blokelement. De minimale eenheid voor vertaling is niet de "ruimte tussen tags", maar de volledige inhoud (innerHTML) van blokelementen zoals p h1–h6 li td blockquote. We splitsen geen zinnen.
Tweede stap: vervang inline-tags door markers en geef de hele zin als één vertaaleenheid door. We vervangen de <strong> en <a> in een chunk tijdelijk door markers zoals ....
私たちは[[T1]]ウェブアクセシビリティ[[/T1]]を重視しています
We instrueren het LLM: "Dit is één zin. Vertaal het natuurlijk en plaats dezelfde markers opnieuw bij woorden die nadruk behoeven. Je mag de markerposities verplaatsen naar de woordvolgorde van de doeltaal." Nadat we de vertaling ontvangen, herstellen we de markers naar de originele <strong> en <a href="...">. Attributen (zoals href en class) behouden we ongewijzigd.
Zo werkt het: <strong> verwijst nu naar het juiste woord ook aan de Engelse kant, en de zin klinkt natuurlijk.
② Differentiaalbepaling van vertalingen en R2-cachebeheer
Als we alles elke keer vertaalden, zouden we nooit genoeg budget hebben. We gebruikten eerder al een cache met de naam translate-cache.json, maar deze keer hebben we de sleutelstructuur en opslaglocatie herzien.
Wanneer je de prompt aanpast, worden de vertalingen opnieuw gegenereerd
De cachesleutel wordt samengesteld uit "originele Japanse tekst + landinstelling + inhoud van het vertaalprompt". Op deze manier wordt automatisch alleen het overeenkomstige item als "niet in cache" behandeld als de Japanse tekst verandert, en worden alle items opnieuw vertaald als de vertaalinstructies of terminologierichtlijnen (prompt) wijzigen.
Het doel is om ongelukken te voorkomen waarbij oude vertalingen in de cache blijven hangen nadat we de prompt hebben verbeterd. In de praktijk gebruiken we hiervoor strings die zijn gehasht met sha256 als sleutel.
De cachestructuur ziet er zo uit:
{"<sha256のキー>":{"value":"翻訳結果(マーカー込み)","locale":"en","model":"claude-haiku-4-5-20251001","translatedAt":"2026-06-12T..."}}
Cache in R2 gezet, handmatig werk geëlimineerd
Dit is het opruimen van de vorige achterstand.
Vorige keer beheerden we de cache via JSON-bestanden in de repository. Daarom, wanneer we artikelen toevoegden in het CMS en via deploy hooks bouwden, kon de server alleen de oude cache op Git zien. We werkten rond dit probleem met de regel: 'als je een artikel toevoegt, build dan lokaal en push het bijgewerkte cachebestand naar Git'.
Deze keer plaatsten we de cache in een enkele JSON blob op Cloudflare R2. Bij elke build halen we deze uit R2 op en schrijven hem terug wanneer klaar. Dit zorgt ervoor dat de cache persistent blijft, zelfs bij builds via webhooks, en het handmatige lokale build→push-werk is volledig verdwenen. Voeg een artikel toe en push het, dan worden alleen de nieuwe items vertaald en wordt de cache automatisch bijgewerkt.
Vertaalprioriteitstabel
Inconsistenties in vertalingen (zoals schommelingen in het merk Liberogic) waren ook vorige keer een pijnpunt. Dit keer hanteren we vier niveaus.
- Handmatige overschrijving (
data-i18n-key) ― HTML-elementen metdata-i18n-keygebruiken handgeschreven vertalingen die van tevoren zijn voorbereid. Geen LLM, geen woordenboek — dit garandeert dat 'deze vertaling exact dit moet zijn'. - Termenlijst (glossary) ― Voor herhaalde vaste labels zoals navigatie en paginatitels fixeren we vertalingen in een JSON woordenboek. Update het woordenboek en herbouw — de wijzigingen zijn onmiddellijk zichtbaar.
- R2 cache ― Als geen van beide bovenstaande niveaus van toepassing is, controleren we de cache.
- Claude API ― Alleen items die nergens anders voorkomen, gaan naar het LLM.
Consistentie van termen in de tekst kan niet volledig worden bereikt met puntgerichte woordenboeken (die geen gedeeltelijke overeenkomsten ondersteunen), dus gebruiken we termijnhints aan de systeempromptkant om ze losjes af te stemmen.
Omgaan met de 'gewoonten' van LLM's
Machine translation produceert vreemde vertalingen, maar dat was eerder ook al het geval. LLM's hebben hun eigen gewoonten. Hier zijn enkele ervan die in de praktijk naar voren zijn gekomen.
- Technische zinnen blijven helemaal in het Engels achter. Door de lijst met 'niet-te-vertalen termen' zoals API, React en Vue te veel toe te passen, retourneert het LLM soms hele zinnen in het Engels. We pakken dit aan door in de gebruikersprompt sterker te benadrukken 'uitvoer in de doeltaallocale'.
- De positie van markers draait semantisch om. Wanneer het LLM de te benadrukken woorden verkeerd beoordeelt, kan
<strong>over een vreemde reeks gaan. Dit wordt opgelost door de betrokken invoer te verwijderen en opnieuw te vertalen. - Reacties in CJK-talen worden halverwege afgekapt. Elk Chinees/Japans teken verbruikt meer tokens; wanneer je er veel tegelijk invoert, bereikt de uitvoer de limiet en wordt JSON beschadigd. We lossen dit op door
max_tokenste verhogen en het aantal items per batch te verminderen. - Verlies van letterlijke
en vervormde CJK-datums. Regelbreaks kunnen als tekenreeksbinnenslopen, en CJK-datums kunnen onnodig spaties krijgen, zoals22 mei 2026. Deze worden in na-verwerkingsscripts opgeschoond.
Een operationeel inzicht dat hielp was dat problematische onderdelen één voor één verwijderen en opnieuw vertalen het hoogste succespercentage heeft. Wanneer je probeert meerdere dingen tegelijk op te lossen, gaat het LLM dezelfde structuur vaak op dezelfde manier verkeerd beoordelen. Haast je langzaam.
Over kosten
We gebruiken Claude Haiku 4.5 als standaard. Met kostenfocus verbeteren we eerst via prompts en woordenboeken als kwaliteitsproblemen ontstaan. We gebruiken prompt caching op de systeempromptkant om de vaste delen telkens opnieuw in te voeren zonder kosten.
Hier is een indicatie van de werkelijke kosten.
Inhoud | Kosten |
|---|---|
Eerste volledige vertaling van één locale | ongeveer $2,50-3 |
Eerste volledige vertaling van alle locales | ongeveer $20 |
Standaard deployment (alleen cache hits) | vrijwel $0 |
Eén artikel toevoegen (enkele vertalingen) | ongeveer $0,01 |
Dagelijkse implementaties zijn vrijwel gratis, en het toevoegen van artikelen kost slechts 1 tot enkele yen. Hoewel de initiële heraanleg aanzienlijke kosten met zich mee bracht, zijn de bedrijfskosten daarna juist lager geworden.
Samenvatting
Door over te schakelen van Google Translate naar LLM-vertaling zijn de bedrijfskosten beperkt gebleven en is de vertaalkwaliteit aanzienlijk verbeterd.
Omdat het LLM is, is niet alles automatisch en perfect. We blijven geduldig aanpassingen doorvoeren, ongebruikelijke patronen opsporen en verhelpen. Omdat we nu alleen de prompt en het woordenboek hoeven aan te passen, is de verbeteringscyclus veel sneller geworden.
Vanuit DTP de wereld van het web in gestapt en merkte al snel dat hij markering, frontend, directie en accessibility allemaal beheerst — een echte 'meester van techniek'. Sinds de oprichting van Liberogic een multitalent en inmiddels een levend naslagwerk in het bedrijf. Tegenwoordig is hij geïnteresseerd in de vraag "Kunnen we accessibility-implementatie meer aan AI overlaten?" en experimenteert hij graag met efficiëntie via prompts. Zowel technisch als mentaal nog volop in ontwikkeling.
Ayumi Futamata
IAAP-gecertificeerd webtoegankelijkheidsspecialist (WAS) / Opmaakingenieur / Frontend-ingenieur / Webdirecteur