Unser Blog „Yuru Log" – wir haben kürzlich heimlich eine englische Version erstellt. Diesmal schreibe ich über die Hintergründe, insbesondere darüber, wie wir das Problem gelöst haben: „Führt Mehrsprachigkeit auf einer SSR-Website zu explodierenden API-Kosten?"
Voraussetzung: Wir nutzen das CMS EmDash
Dieser Blog läuft auf EmDash, einem Astro-basierten CMS. Artikel werden über die Admin-Oberfläche geschrieben, und das Frontend wird mit Astro server-side rendering (SSR) bereitgestellt.
Das intuitive Bedienungsgefühl ähnelt WordPress, und es wird gesagt, dass es als „geistiger Nachfolger von WordPress" gilt.
EmDash verfügt bereits über einen integrierten Mechanismus für „mehrsprachige Inhalte", mit dem Artikel, Seiten, Menüs und Taxonomien (Kategorien und Tags) als separate Datensätze pro Gebietsschema gespeichert werden können. Mit dem Feld translationOf können Sie einfach kennzeichnen, dass ein englischer Artikel eine Übersetzung eines japanischen Artikels ist – und der Artikel wird dann als separate Sprachversion desselben Inhalts behandelt.
Migration von microCMS zu Emdash + Cloudflare
Multilingualisierung ist mit SSG eigentlich ganz einfach, nicht wahr?
In der Regel ist es recht einfach, wenn man „die Übersetzung der Mehrsprachunterstützung über eine API automatisieren" möchte – zumindest bei SSG (Static Site Generation).
- Die Übersetzungs-API wird während des Builds nur einmal aufgerufen
- Ergebnisse als statische Dateien zwischenspeichern
- Dann wird einfach das HTML bereitgestellt
Daher entstehen API-Kosten, egal wie viele Artikel hinzukommen, nur "bei jedem Build". Die Häufigkeit lässt sich leicht kontrollieren, und das Caching funktioniert reibungslos.
Aber Yuru Log ist SSR. Also wird die API bei jeder Anfrage aufgerufen?
Das ist das Problem. EmDash CMS-Seiten werden nach Spezifikation vollständig mit SSR ((output: "server")) gerendert. Es ist kein System, das Seiten durch statisches Bauen vorkonfiguriert, sondern ein Mechanismus, der bei jeder Anfrage die neuesten Inhalte von der CMS API und Datenbank abruft und darstellt.
Was würde also passieren, wenn wir einen einfachen Ansatz implementieren würden: "Bei der Anzeige einer englischen Seite die Übersetzung sofort von Gemini durchführen und ausgeben"?
- Bei jeder Anfrage fallen Gebühren für die Übersetzungs-API an
- Die Latenz nimmt zu (wir müssen auf die Antwort des LLM warten)
- Möglicherweise liefert dieselbe Artikel jedes Mal leicht unterschiedliche Übersetzungsergebnisse zurück (keine Reproduzierbarkeit)
Das ist völlig inakzeptabel. Wenn man bei SSR den Ansatz "API bei jeder Anfrage aufrufen" wählt, scheitern sowohl die Kosten als auch die Benutzererfahrung.
Lösung: Übersetzung von „bei Anfrage" zu „bei der Inhaltserstellung" verschieben
Die Antwort ist einfach: Die Übersetzung nicht zum Zeitpunkt der Anfrage durchführen.
Konkret haben wir die Pipeline so gestaltet.
- Japanische Artikel in der EmDash-Verwaltungsoberfläche schreiben und veröffentlichen
- Das Übersetzungsskript (
scripts/translate.mjs) nur einmal ausführen- Strukturierte Rich-Text-Daten (Portable Text) von japanischen Artikeln aus der EmDash-API abrufen
- Extrahieren Sie nur den zu übersetzenden Textabschnitt und geben Sie ihn an die Gemini API ((
gemini-3.6-flash)) weiter. - Blöcke wie Bilder und Amazon-Produktkarten werden unverändert durchgeleitet und nicht übersetzt
- Übersetzungsergebnis als neuen englischen Artikel in der EmDash-Datenbank speichern
- Geben Sie
translationOfdie ID des ursprünglichen japanischen Artikels an, um ihn als englische Version derselben Artikelgruppe zu verlinken - Sie wird als Entwurf gespeichert, damit Sie sie überprüfen und veröffentlichen können.
- Geben Sie
Mit anderen Worten: Das Übersetzungsergebnis wird nicht „spontan generiert", sondern als „ein weiterer Sprachinhalt, der im CMS gespeichert ist" behandelt.
Auf diese Weise unterscheidet sich die SSR-Anfrageverarbeitung selbst nicht von der normalen Artikelanzeige. Mit EmDashs standardmäßiger Locale-Abfrage (getEmDashEntry(..., { locale: "en" })) werden einfach die bereits in der Datenbank gespeicherten englischen Inhalte abgerufen. Die Kommunikation mit der Gemini API erfolgt nur einmal beim Übersetzen des Artikels und tritt beim Abrufen überhaupt nicht auf.
Das heißt, die Konstellation ist so
Zeitpunkt der Übersetzungsausführung | API-Kosten pro Anfrage | |
|---|---|---|
SSG + On-Demand-Übersetzung | Zur Build-Zeit | Zero (statische Dateiauslieferung) |
SSR + Übersetzung bei jedem Request (Schlechtes Beispiel) | Pro Anfrage | Artikel × Zugriffszahl 😱 |
SSR + Vorausübersetzung in der Datenbank gespeichert (dieser Ansatz) | Bei der Inhaltserstellung (einmalig) | Null |
Der Gedanke von SSG, "alles beim Build zu cachen", lässt sich in die Welt von SSR übertragen, indem man "übersetzte Inhalte dauerhaft in der Datenbank speichert". Das war die Erkenntnis dieses Mal. Selbst bei SSR werden Inhalte nicht ständig dynamisch generiert – wenn man nur die "schwere Verarbeitung" der Übersetzung vorher erledigt, ist die Auslieferung genauso leicht wie bei einer normalen CMS-Seite.
Die Geschichte, wie sich die Datenbank zwischen lokal und Produktion unterschied und wir mehrmals "Moment, das ist weg!?" erlebten
Die Implementierung dieses Mal hat mich am meisten bei diesem Punkt gezehrt. EmDash hat jeweils separate Datenbanken für die lokale Entwicklungsumgebung und die Produktionsumgebung. Das ist zwar logisch, aber genau das führte dazu, dass wir mehrfach erlebt haben: "Die Funktion, die gerade noch da war, ist weg!"
Die Symptome sah meistens so aus:
- Die Navigation im Header ist plötzlich leer
- Footer-Kategorieliste verschwindet
- Seitenleisten-Widget auf der Artikelseite verschwindet
- Lokal erstellte Menüpunkte und Widgets sind wieder auf ihren ursprünglichen Zustand zurückgesetzt
Außerdem treten diese Probleme immer genau dann auf, wenn "es gerade noch funktioniert hat", weshalb ich anfangs verdächtigt habe, selbst etwas kaputt gemacht zu haben, und danach lange nach der Ursache gesucht habe.
Ursache 1: Prozess zum Importieren der Produktions-DB lokal (pull.sh)
Während der Entwicklung habe ich mehrmals ein Skript ausgeführt, das die Produktions-DB vollständig lokal importiert, um "die neuesten Produktionsdaten auch lokal sehen zu können". Dies führt dazu, dass die lokale DB vollständig mit dem Produktionszustand überschrieben wird (d. h. englische Artikel sind noch im Entwurfsstadium, Menüstrukturen vor der Veröffentlichung usw.).
Mit anderen Worten: Wenn ich versuche, "auf der Produktionsseite noch nicht veröffentlichte Funktionen" lokal zu überprüfen, befinden sich diese unmittelbar nach dem Pull wieder im Produktionszustand, wodurch die Arbeiten, die ich lokal voraus gemacht hatte, vorübergehend nicht sichtbar sind.
Ursache 2: "Seed"-Prozess, der jedes Mal beim Anmelden an der lokalen Admin-Oberfläche ausgeführt wird
Die andere Ursache war, dass der Mechanismus zum erneuten Anmelden an der lokalen Admin-Oberfläche (Bypass-Login für die Entwicklung) gleichzeitig die seed.json (Initialdaten-Definitionsdatei) jedes Mal neu einlas.
Das war problematisch, weil:
- Menüpunkte und Widgets → werden so überschrieben, dass sie «vollständig übereinstimmen» mit der Seed-Seite (= lokal manuell hinzugefügte Einträge werden gelöscht)
- Taxonomien wie Kategorien und Tags → «bereits existierende werden übersprungen», daher verschwinden sie nicht
Das war eine Spezifikation mit unterschiedlichem Verhalten je nach Eintrag. Dadurch entstand das auf den ersten Blick unerklärliche Phänomen, dass «Kategorien erhalten bleiben, aber Menüs und Widgets bei jedem Durchlauf verschwinden».
Getroffene Maßnahmen
Diese doppelte Struktur (Überschreiben durch Pull / Überschreiben durch Seed) grundsätzlich zu beseitigen war schwierig. Daher wählten wir als pragmatischen Kompromiss:
seed.jsonvon allen Demo-Dummy-Inhalten (Menüpunkte, Widgets, Kategorien usw.) leeren- Bei jedem Eintritt in die lokale Umgebung erforderliche Menüs und Widgets über die API neu erstellen
Mit dieser Betriebsweise kamen wir zurecht. Es ist zwar keine Grundlösung, sondern nur eine Symptombekämpfung, aber zumindest konnten wir den Unfall verhindern, dass «unbeabsichtigte Demo-Daten überschrieben werden».
Fazit
- EmDash ist ursprünglich ein CMS mit Mehrsprachenfähigkeit, das Inhalte pro Gebietsschema speichern kann
- Bei SSG reicht «Übersetzung beim Build → Caching», aber bei SSR wird «jedes Mal übersetzen» zum Billing-Albtraum
- Die Lösung besteht darin, die Übersetzung nur einmal bei der Inhaltserstellung und nicht bei jeder Anfrage durchzuführen und das Ergebnis als normalen CMS-Inhalt in der Datenbank zu speichern.
- Die verwendete Übersetzungs-Engine war Gemini API (
gemini-3.6-flash) - Allerdings sollte man bei der Mehrsprachigkeit eines CMS nicht vergessen, dass die Tatsache selbst, dass die Datenbank zwischen lokaler und Produktionsumgebung getrennt ist, neue Fallstricke schafft.
- Besonders bei einer Entwicklung, bei der man "einen Zustand, der noch nicht in der Produktion vorhanden ist, lokal voraus erstellt", wird man leicht von Nebenwirkungen der Umgebungssynchronisierungsmechanismen (Pull, Seeding, Caching usw.) beeinflusst.
Die Lehre aus diesem Fall ist: Wenn man nur diese beiden Timing-Designs vorab versteht – "wann die Übersetzung stattfindet" und "wann und was die Umgebungssynchronisierung überschreibt" –, dann kann man auch bei SSR mehrsprachige Unterstützung ohne Betriebskosten und Zwischenfälle umsetzen.
Wie wäre es mit Mehrsprachigkeit auch auf Ihrer Website?
Ich konzentriere mich auf Markup und entwickle Frontends mit JavaScript, React und Next.js. Es freut mich immer, wenn die Websites, an denen ich mitgearbeitet habe, erfolgreich veröffentlicht werden! Mein Hobby ist Gitarrespielen. Ich mag Katzen und gebackene Süßkartoffeln 🐱🍠
Hiraicchi
Frontend-Engineer / Eintritt 2022