Vor einiger Zeit habe ich einen Artikel geschrieben, in dem ich eine Astro-SSG-Website mit der Google Translate API mehrsprachig gemacht habe. Dabei wird die Google Cloud Translation API verwendet, um das statische HTML nach dem Build zu übersetzen und so die Versionen für jede Sprache zu erzeugen. Als vereinfachte Mehrsprachigkeit mit überschaubaren Kosten war das durchaus praxistauglich.
Mit der Zeit zeigten sich im Betrieb jedoch Punkte, die sowohl die SEO als auch die Übersetzungsqualität betrafen. Warum wir von Google Translate abgerückt sind, erklären wir in einem separaten Artikel.
Artikel: Maschinelle Übersetzung von Google Translate auf die Claude API umstellen
Dieser Artikel behandelt die Frage: "Wie genau wurde das neu aufgebaut?" Es geht um die Implementierung. Die Übersetzungs-Engine wurde von Google Translate auf ein LLM (Claude API) umgestellt, und zusätzlich wurden die manuellen Betriebsschritte beseitigt, die im letzten Projekt noch offen geblieben waren.
Übersetzung per LLM zur Build-Zeit, Differenz-Cache in R2
Das Grundprinzip ist dasselbe wie zuvor: Die Übersetzung wird vollständig zur Build-Zeit auf der Serverseite abgeschlossen. Der Browser ruft die Claude API nicht auf. Geändert haben sich der Ort der Übersetzungs-Engine und der des Caches.
Der Build-Ablauf sieht so aus.
npm run build
├── fetch-microcms (microCMSから記事データ取得)
├── astro build (日本語HTMLを生成 → dist/)
├── translate (各ロケールのHTMLを生成 → dist/{locale}/)
└── update-xml (sitemap更新)
Was im translate-Skript (translate-html-llm.mjs) grob in dieser Reihenfolge passiert, ist folgendes.
- Übersetzungs-Cache (eine einzelne JSON-Datei) von Cloudflare R2 herunterladen
- Lies jede HTML-Datei unter
dist/mit cheerio ein und extrahiert die übersetzbaren Texte - Vorhandene Einträge werden aus dem Cache verwendet; nur fehlende Texte werden an die Claude API gesendet
- Die übersetzten Ergebnisse ersetzen die Texte und werden nach
dist/{locale}/geschrieben - Die neu übersetzten Einträge werden in den Cache übernommen und anschließend nach R2 hochgeladen
Die beiden wichtigsten Punkte sind die differenzielle Übersetzung, bei der nur geänderte Stellen übersetzt werden, sowie die Ablage dieses Caches in R2.
① Wie Übersetzungen über Inline-Tags hinweg natürlich gestaltet werden
Genau das wollten wir am meisten verbessern.
Angenommen, der Text enthält folgendes HTML.
<p>私たちは<strong>ウェブアクセシビリティ</strong>を重視しています</p>
Bei der bisherigen Verarbeitung würden die drei Fragmente Wir legen / Wert auf Web-Accessibility / großen Wert einzeln übersetzt. Da sich die Wortstellung zwischen Japanisch und Deutsch unterscheidet, verschiebt sich beim Zurücksetzen der übersetzten Fragmente an ihre ursprüngliche Position der Bereich, den <strong> umschließt, oder der Satz wirkt insgesamt unnatürlich. Je mehr Inline-Tags ein Satz enthält, desto stärker bricht dies auseinander. Genau dieser Punkt war die größte Schwäche des alten Verfahrens.
Die Verbesserung erfolgt in zwei Schritten.
Erstens: Chunking auf Ebene der Blockelemente. Die kleinste Übersetzungseinheit ist nicht der „Abstand zwischen Tags“, sondern der vollständige Inhalt (innerHTML) von Blockelementen wie p, h1 bis h6, li, td und blockquote. Sätze werden nicht zerteilt.
Zweitens: Inline-Tags durch Marker ersetzen, damit der gesamte Satz als eine einzige Übersetzungseinheit übergeben wird. Innerhalb eines Chunks werden <strong>- und <a>-Tags zunächst durch Marker wie ... ersetzt.
私たちは[[T1]]ウェブアクセシビリティ[[/T1]]を重視しています
Dem LLM wird folgende Anweisung gegeben: „Dies ist ein einziger Satz. Übersetze ihn natürlich und setze dieselben Marker erneut auf die Wörter, die betont werden sollen. Die Position der Marker darf an die Wortstellung der Übersetzung angepasst werden.“ Sobald die Übersetzung zurückkommt, werden die Marker wieder zu den ursprünglichen <strong>- bzw. <a href="...">-Tags. Attribute (etwa href oder class) werden unverändert übernommen.
So bezieht sich <strong> nun auch auf der englischen Seite auf das richtige Wort, und der Satz liest sich natürlich.
② Differenzübersetzung und Design des R2-Caches
Würde bei jedem Durchlauf alles neu übersetzt, wären die Kosten untragbar. Schon zuvor gab es einen Cache namens translate-cache.json, diesmal wurden jedoch die Erstellung der Schlüssel und der Speicherort überarbeitet.
Mechanismus, der Übersetzungen neu erstellt, sobald der Prompt geändert wird
Der Cache-Schlüssel setzt sich aus dem Originaltext auf Japanisch, dem Locale und dem Inhalt des Übersetzungs-Prompts zusammen. Dadurch gilt ein Eintrag automatisch als „nicht im Cache“ und wird neu übersetzt, wenn sich der Originaltext ändert. Ändern sich dagegen die Übersetzungsanweisungen oder die Terminologie-Richtlinien (Prompt), werden alle Einträge neu übersetzt.
Ziel ist es, zu verhindern, dass veraltete Übersetzungen im Cache verbleiben, obwohl der Prompt verbessert wurde. Konkret wird aus diesen Werten ein mit sha256 gehashter String als Schlüssel erzeugt.
Der Inhalt des Caches sieht so aus.
{"<sha256のキー>":{"value":"翻訳結果(マーカー込み)","locale":"en","model":"claude-haiku-4-5-20251001","translatedAt":"2026-06-12T..."}}
Cache in R2 gelegt und manuelle Schritte abgeschafft
Hier werden die Punkte aus dem letzten Mal aufgegriffen.
Bisher wurde der Cache in einer JSON-Datei im Repository verwaltet. Wenn wir im CMS einen Artikel hinzufügten und per Deploy-Hook einen Build auslösten, konnte der Server deshalb nur den veralteten Cache aus Git sehen. Behelfsweise galt die Regel, nach dem Hinzufügen eines Artikels lokal zu bauen, die aktualisierte Cache-Datei zu committen und per Push nach Git zu übertragen.
Jetzt liegt der Cache als einzelner JSON-Blob in Cloudflare R2. Bei jedem Build wird er aus R2 geladen und danach zurückgeschrieben. Dadurch bleibt der Cache auch bei Builds über Webhooks dauerhaft erhalten, und der manuelle Ablauf aus lokalem Build und Push entfällt vollständig. Wird ein Artikel hinzugefügt und gepusht, werden nur die neuen Inhalte übersetzt, und der Cache aktualisiert sich automatisch.
Priorität der Übersetzung
Schwankende Terminologie, etwa bei der Schreibweise des Firmennamens Liberogic, war schon beim letzten Mal ein Problem. Diesmal werden die Übersetzungen in vier Stufen verarbeitet.
- Manuelle Überschreibung (über
data-i18n-key) – Elemente im HTML, die mitdata-i18n-keymarkiert sind, erhalten eine vorab festgelegte, handgeschriebene Übersetzung. Weder das LLM noch das Glossar kommen hier zum Zug, sodass sich gezielt sicherstellen lässt, dass bestimmte Stellen exakt so übersetzt werden. - Glossar – Wiederkehrende feste Bezeichnungen wie Navigationspunkte oder Seitentitel werden über das JSON-Glossar einheitlich übersetzt. Wird das Glossar angepasst und neu gebaut, ist die Änderung sofort wirksam.
- R2-Cache – Ist weder in der manuellen Überschreibung noch im Glossar ein Eintrag vorhanden, wird im Cache nachgesehen.
- Claude API – Erst wenn keine der vorherigen Stufen greift, wird der Text an das LLM übergeben.
Die Vereinheitlichung der im Text vorkommenden Fachbegriffe lässt sich mit einem reinen Punkt-Wörterbuch nicht vollständig erfassen (Teilübereinstimmungen werden nicht erkannt). Deshalb gleichen wir sie locker über Begriffshinweise im System-Prompt an.
Mit den "Eigenheiten" von LLMs umgehen
Dass maschinelle Übersetzungen seltsame Ergebnisse liefern, war schon beim letzten Mal ein Thema. LLMs haben jedoch ihre eigenen Eigenheiten. Im praktischen Betrieb sind uns einige davon aufgefallen, die wir hier vorstellen.
- Technische Sätze bleiben vollständig auf Englisch stehen. Wenn die Liste der "nicht zu übersetzenden Begriffe" wie API, React oder Vue zu weit angewendet wird, gibt das Modell manchmal den gesamten Satz unübersetzt zurück. Wir begegnen dem, indem wir im Benutzer-Prompt nachdrücklich darauf hinweisen, dass in der Zielsprache auszugeben ist.
- Die Position der Marker kehrt sich inhaltlich um. Bei der Entscheidung, welches Wort hervorgehoben werden soll, liegt das Modell manchmal falsch, sodass
<strong>auf einen unpassenden Bereich angewendet wird. Es reicht meist, nur diesen einen Eintrag zu entfernen und neu übersetzen zu lassen. - Antworten in CJK-Sprachen brechen mitten im Satz ab. Chinesische Schriftzeichen verbrauchen pro Zeichen mehr Tokens. Werden zu viele Einträge auf einmal gesendet, erreicht die Ausgabe das Limit und das JSON wird ungültig. Wir haben
max_tokenserhöht und die Anzahl der Einträge pro Batch reduziert. - Wörtliche
und fehlerhafte CJK-Datumsangaben.Zeilenumbrüche werden manchmal als Zeichenfolge eingefügt, und2026 年 05 月 22 日erscheint mit überflüssigen Leerzeichen. Diese Fehler bereinigen wir mit einem Nachbearbeitungsskript.
Eine Erkenntnis aus dem Betrieb: Fehlerhafte Stellen einzeln zu löschen und neu übersetzen zu lassen, bringt die höchste Erfolgsquote. Versucht man, mehrere auf einmal zu korrigieren, neigt das LLM dazu, dieselbe Struktur gleichermaßen falsch zu beurteilen. Gründlichkeit zahlt sich hier aus.
Zu den Kosten
Als Engine setzen wir durchgängig auf Claude Haiku 4.5. Der Fokus liegt auf Kosten. Treten Qualitätsprobleme auf, verbessern wir zuerst den Prompt und das Wörterbuch. Der System-Prompt nutzt Prompt Caching, wodurch die Kosten für den immer gleichen festen Anteil bei jedem Aufruf gesenkt werden.
Die folgenden Werte dienen als grobe Orientierung.
Inhalt | Kosten |
|---|---|
Erstübersetzung einer Sprache | ca. $2,5 bis 3 |
Erstübersetzung aller Sprachen | ca. $20 |
Regulärer Deployment (nur Cache-Treffer) | nahezu $0 |
Einen Artikel hinzufügen (einige Übersetzungen) | ca. $0,01 |
Die täglichen Deployments kosten nahezu nichts, und das Hinzufügen von Artikeln kostet nur einen Bruchteil eines Cents bis wenige Cent. Die erste vollständige Neuerstellung verursacht zwar einen nennenswerten Aufwand, doch danach sind die laufenden Kosten sogar niedriger als zuvor.
Fazit
Durch den Wechsel von Google Translate zu einer LLM-Übersetzung sind die Betriebskosten kaum gestiegen, und die Übersetzungsqualität hat sich deutlich verbessert.
Ein LLM übersetzt nicht automatisch alles perfekt. Immer wieder werden Eigenheiten entdeckt und durch gezielte Feinabstimmung behoben. Da sich die Stellschrauben jedoch auf zwei gut nachvollziehbare Orte konzentrieren – den Prompt und das Glossar –, lässt sich der Verbesserungszyklus deutlich leichter durchlaufen.
Vom DTP in die Webwelt: Heute beherrscht er Markup, Frontend, Projektleitung und Barrierefreiheit gleichermaßen – ein wahrer "Techniker-Guru". Seit den Anfängen von Liberogic vielseitig im Einsatz und heute eine lebende Enzyklopädie im Team. Derzeit tüftelt er mit großer Begeisterung an effizienteren Abläufen durch gezielte Prompts – etwa mit der Frage: "Können wir bei der Barrierefreiheit nicht mehr auf KI setzen?" Technik und Denkweise entwickeln sich weiter.
Ayumu Futamata
IAAP-zertifizierter Web Accessibility Specialist (WAS) / Markup-Entwickler / Frontend-Entwickler / Webdirektor