Topics

Comment créer un blog multilingue avec EmDash – réduire les coûts des API de traduction même avec SSR

  • column

Notre blog « ゆるろぐ », nous avons en fait créé discrètement une version anglaise récemment. Cette fois, je vais vous parler des coulisses, en particulier comment nous avons résolu le problème suivant : « L'API coûte-t-elle une fortune lorsqu'on rend multilingue un site SSR ? »

Contexte : nous utilisons un CMS appelé EmDash

Ce blog fonctionne avec EmDash, un CMS basé sur Astro. Nous rédigeons les articles à partir du panneau d'administration, et le frontend est rendu côté serveur (SSR) avec Astro.

L'interface est intuitive, semblable à WordPress, et on dit que c'est le « successeur spirituel de WordPress ».

EmDash dispose déjà d'un système de « contenu multilingue » qui vous permet de conserver les articles, pages, menus et taxonomies (catégories et étiquettes) comme des enregistrements distincts pour chaque locale. Avec un champ translationOf, vous spécifiez simplement « cet article en anglais est la traduction de cet article en japonais », et ils sont alors traités comme des versions dans des langues différentes du même article.

Migration de microCMS vers EmDash + Cloudflare

Concernant la multilingue, c'est facile avec du SSG

En général, quand on parle « d'automatiser la traduction multilingue via une API », c'est simple avec du SSG (Static Site Generation).

  • Vous appelez l'API de traduction qu'une seule fois, au moment de la génération.
  • Mettre en cache les résultats sous forme de fichiers statiques
  • Il ne reste plus qu'à servir ce HTML

Par conséquent, peu importe le nombre d'articles, les coûts d'API ne sont facturés « qu'à chaque build ». La fréquence est facile à contrôler et la mise en cache est simple.

Mais Yurulog utilise SSR. Donc faut-il appeler l'API à chaque requête ?

C'est là que se pose le problème. Par conception, les pages CMS d'EmDash sont toutes rendues en SSR (output: "server"). Au lieu de créer des pages par un processus de build statique, le système récupère le contenu le plus récent de l'API et de la base de données du CMS à chaque requête, puis l'affiche.

Alors, que se passerait-il si nous implémentions simplement « afficher la page en anglais en faisant traduire par Gemini sur le moment » ?

  • Des frais d'API de traduction sont facturés à chaque requête
  • La latence augmente aussi (nous devons attendre la réponse du LLM)
  • Chaque fois que nous recevons une traduction du même article, les résultats peuvent être légèrement différents (manque de reproductibilité)

C'est complètement inacceptable. Adopter l'approche « appeler l'API à chaque fois » simplement parce qu'on utilise SSR ruinerait le coût et l'expérience utilisateur.

Solution : déplacer la traduction du « moment de la requête » au « moment de la création du contenu »

La réponse est simple : ne pas faire la traduction au moment de la requête.

Concrètement, voici le pipeline que nous avons mis en place.

  1. Rédiger et publier l'article en japonais dans l'interface d'administration d'EmDash
  2. Exécuter le script de traduction (scripts/translate.mjs) une seule fois
    • Récupérer l'article en japonais au format Portable Text (texte riche structuré) via l'API EmDash
    • Extraire uniquement les portions de texte à traduire et les envoyer à l'API Gemini (gemini-3.6-flash)
    • Faire passer les blocs tels que les images et les cartes de produits Amazon sans les traduire
  3. Enregistrer le résultat de la traduction comme un nouvel article en anglais dans la base de données d'EmDash
    • Spécifier l'ID de l'article japonais original dans translationOf pour le lier comme version anglaise du même groupe d'articles
    • Il est enregistré en tant que brouillon, vous pouvez donc le relire avant de le publier.

En d'autres termes, le résultat de la traduction n'est pas « généré sur place », mais traité comme « un contenu dans une autre langue stocké dans le CMS ».

De cette façon, le traitement des requêtes SSR lui-même ne change pas par rapport à l'affichage normal des articles. Les requêtes locale standard d'EmDash (getEmDashEntry(..., { locale: "en" })) récupèrent simplement le contenu en anglais déjà stocké dans la base de données. La communication avec l'API Gemini ne se produit qu'une seule fois lors de la traduction de l'article, elle ne se produit jamais lors de la consultation.

C'est-à-dire cette structure

Moment d'exécution de la traduction

Coût API à la demande

SSG + traduction à chaque fois

À la compilation

Zéro (service de fichiers statiques)

SSR + traduction à chaque fois (exemple problématique)

Par requête

Généré par article × nombre d'accès 😱

SSR + pré-traduction stockée en base de données (approche utilisée ici)

À la création du contenu (une seule fois)

Zéro

L'insight de cette implémentation était d'appliquer l'approche SSG du « cache par batch au moment du build » au monde SSR en « persistant le contenu traduit dans la base de données ». Même en SSR, le contenu lui-même n'est pas constamment régénéré de manière dynamique ; en effectuant à l'avance le « traitement lourd » de la traduction, la livraison devient aussi légère qu'une page CMS ordinaire.

Quand la base de données diffère entre le local et la production, et que tu dis « Quoi, c'est disparu !? » plusieurs fois

Ce qui m'a vraiment épuisé dans cette implémentation, c'était cela. EmDash utilise une base de données différente pour l'environnement de développement local et l'environnement de production. C'est normal, pour ainsi dire, mais cela a causé l'expérience répétée de « la fonctionnalité qui était là il y a peu a disparu ! ».

Les symptômes ressemblaient généralement à ceci

  • Le navigation du header se vide soudainement
  • Les catégories du footer disparaissent
  • Les widgets de la barre latérale de la page d'article disparaissent
  • Les éléments de menu locaux et les widgets que j'avais créés reviennent à leur état initial

Et comme cela se produit toujours au moment où je dis « ça fonctionnait encore il y a une minute », j'ai d'abord commencé à chercher la cause en me demandant « Aurais-je cassé quelque chose ? »

Cause 1 : Le processus d'importation de la base de données de production en local (pull.sh)

Pendant le développement, j'ai exécuté plusieurs fois un script pour importer l'intégralité de la base de données de production en local, car je voulais voir les dernières données de production localement aussi. Lorsque je fais cela, la base de données locale est complètement écrasée par l'état de la production (c'est-à-dire que les articles en anglais sont encore en brouillon, la structure des menus n'est pas encore finalisée, etc.).

En d'autres termes, même si j'essayais de vérifier localement une fonctionnalité qui n'est pas encore publiée en production, immédiatement après un pull, le système revient au même état qu'en production, ce qui signifie que le travail que j'avais avancé localement disparaît temporairement.

Cause 2 : Le processus d'initialisation (seed) qui s'exécute chaque fois que j'accède à nouveau à l'interface d'administration locale

L'autre cause était que le mécanisme pour me reconnecter à l'interface d'administration locale (une connexion de contournement pour le développement) rechargeait à chaque fois le fichier seed.json (le fichier de définition des données initiales).

Et c'est là que les choses deviennent délicates,

  • Éléments de menu et widgets → sont écrasés « pour correspondre exactement » au contenu côté seed (= les éléments ajoutés manuellement localement disparaissent)
  • Les taxonomies telles que catégories et tags → « les éléments existants sont ignorés » donc ne disparaissent pas

C'était une spécification où le comportement différait selon l'élément. Par conséquent, nous avons constaté ce phénomène apparemment inexplicable : « les catégories persistent, mais les menus et widgets disparaissent à chaque fois ».

Mesures prises

Il était difficile d'éliminer fondamentalement cette double structure (écrasement par pull / écrasement par seed), donc comme solution pragmatique nous avons opté pour :

  • Vider complètement tous les contenus de démonstration factices (éléments de menu, widgets, catégories, etc.) depuis seed.json
  • À chaque réaccès à l'environnement local, recréer les menus et widgets nécessaires via l'API

Nous avons géré la situation avec cette approche. Ce n'est pas une solution définitive mais une mesure d'atténuation qui empêche au moins les accidents d'écrasement par des données de démonstration involontaires.

Conclusion

  • EmDash est initialement un CMS multilingue capable de disposer de contenu pour chaque locale
  • Avec la SSG, « traduction au moment de la compilation → mise en cache » suffit, mais avec la SSR, « traduction à chaque fois » devient un cauchemar de facturation
  • La solution consiste à exécuter la traduction une seule fois lors de la création du contenu, et non lors de la demande, puis à enregistrer le résultat dans la base de données en tant que contenu CMS ordinaire.
  • Le moteur de traduction utilisé était l'API Gemini (gemini-3.6-flash)
  • Cependant, lors de la multilingualisation d'un CMS, il ne suffit pas d'organiser le flux de traduction ; il faut aussi se souvenir que le simple fait que la base de données soit différente en local et en production crée de nouveaux pièges.
  • En particulier, le type de développement qui « prépare d'avance en local des états qui n'existent pas encore en production » est facilement perturbé par les effets secondaires des mécanismes de synchronisation d'environnement (pull, seed, cache, etc.).

La leçon de cette expérience est que, si vous avez une compréhension préalable de ces deux points de synchronisation — « quand effectuer la traduction » et « quand et ce que la synchronisation d'environnement va remplacer » — vous pouvez alors prendre en charge la multilingualisation même avec SSR sans être submergé par les coûts opérationnels ni les incidents.

Et vous, n'envisageriez-vous pas de multilingualiser votre site !

Auteur de cet article

Je me concentre principalement sur le balisage, et je développe le frontend en utilisant JavaScript, React et Next.js. Je suis toujours ravi quand un site auquel j'ai participé est lancé avec succès ! Mon hobby est de jouer de la guitare. J'aime les chats et les patates douces🐱🍠

Hira

Ingénieur frontend / Embauché en 2022

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