Our blog Yuru Log actually got an English version quietly launched recently. This time I'm writing about what went on behind the scenes, particularly how we solved the question: "Won't API costs explode when you add multiple languages to an SSR site?"
Context: We use a CMS called EmDash
This blog runs on EmDash, an Astro-based CMS. We write articles through the admin panel, and the frontend renders with Astro using server-side rendering (SSR).
The intuitive operation feels similar to WordPress, and it's sometimes called the "spiritual successor to WordPress."
EmDash already has a built-in system for "multilingual content," allowing articles, pages, menus, and taxonomies (categories and tags) to exist as separate records per locale. By using a translationOf field to link "this English article is a translation of this Japanese article," it treats them as alternate language versions of the same content.
Migrating from microCMS to EmDash + Cloudflare
When it comes to multilingual support, SSG makes it simple
Generally speaking, when you automate multilingual translation via API, SSG (Static Site Generation) makes things straightforward.
- You hit the translation API just once at build time
- Cache results as static files
- Then all you need to do is serve that HTML
So no matter how many articles you add, API costs only occur at build time. The frequency is easy to control, and caching just works.
But Yuru Log uses SSR. So does that mean we call the API on every request?
That's the issue. By default, EmDash's CMS pages are rendered entirely as SSR (output: "server"). Instead of pre-building pages at compile time, the system fetches the latest content from the CMS API and database on each incoming request and renders it.
So what if we took a straightforward approach: "When displaying the English page, have Gemini translate it on the spot and send it back"?
- Translation API charges incur with every request
- Latency increases (you have to wait for the LLM response)
- The same article might return slightly different translations each time (no consistency)
This is completely unworkable. Adopting "call the API every time" just because it's SSR breaks both costs and user experience.
Solution: Move translation from "request time" to "content creation time"
The answer is simple: don't translate at request time.
Specifically, here's the pipeline we implemented.
- Write and publish a Japanese article in the EmDash management console
- Run the translation script (
scripts/translate.mjs) once- Fetch the Japanese article's Portable Text (structured rich text) from the EmDash API
- Extract only the text portions that need translation and send them to the Gemini API (
gemini-3.6-flash) - Pass through blocks like images and Amazon product cards without translating them
- Save the translation result as a new English article in the EmDash database
- Specify the original Japanese article's ID in
translationOfto link it as the English version of the same article group - It's saved as a draft, so you can review it before publishing.
- Specify the original Japanese article's ID in
In other words, the translation result is treated not as "something generated on the fly" but as "content in another language that's already been saved in the CMS."
This way, the SSR request handling itself is no different from normal article display. It simply uses EmDash's standard locale query (getEmDashEntry(..., { locale: "en" })) to fetch the English content already stored in the database. Communication with the Gemini API happens only once when the article is translated, and never occurs when the page is viewed.
In other words, here's how it works
When translation runs | API cost per request | |
|---|---|---|
SSG + on-demand translation | At build time | Zero (static file delivery) |
SSR + on-demand translation (not recommended) | Per request | Generated once per article × page view 😱 |
SSR + pre-translated content saved to database (this approach) | Once at content creation | Zero |
The key insight from this implementation was recognizing that SSG's approach of "caching everything at build time" can be adapted to SSR by "persisting pre-translated content in the database." Even with SSR, the content itself doesn't need to be regenerated on every request—by handling the expensive translation process upfront, the actual delivery becomes as lightweight as serving a standard CMS page.
When local and production databases differ: a story of "Wait, it disappeared!?"
This was the part of the implementation that drained the most energy. EmDash uses separate databases for local development and production environments. While that's normal, it meant repeatedly encountering "The feature that was there a moment ago is gone!" We experienced this frustration more times than we'd like to admit.
The symptoms typically looked like this
- The header navigation suddenly becomes empty
- Footer Categories list disappears
- Sidebar widgets on article pages disappear
- Locally created menu items and widgets revert to their original state
What made it particularly frustrating is that these issues occurred at moments when everything had been working fine just moments before, so initially I found myself wondering "Did I break something?" while searching for the cause.
Root cause 1: The process of pulling production DB to local environment (pull.sh)
During development, I ran a script several times that pulled the entire production database into the local environment because I wanted to see the latest production data locally. When this happens, the local database is completely overwritten with the production state—meaning English articles are still in draft, menu structures are in their pre-release configuration, and so on.
In other words, even when trying to verify a feature that hasn't been publicly released in production, immediately after pulling it's back to the production state, so work that had progressed further locally temporarily becomes invisible.
Root cause 2: The 'seed' process that runs every time you log into the local admin panel
The other cause was that the mechanism for logging back into the local admin panel (a development bypass login) was reloading seed.json (the initial data definition file) every time.
This was particularly troublesome because,
- Menu items and widgets → overwritten to match seed data exactly (meaning any manually added local content gets deleted)
- Taxonomies like categories and tags → not deleted because existing ones are skipped
This meant the behavior varied depending on the data type. As a result, we got a counterintuitive situation where categories persisted while menu items and widgets alone disappeared each time.
What we did
Completely eliminating this dual-layer structure (pull-based overwrites and seed-based overwrites) was difficult, so as a practical workaround we:
- Emptied all demo dummy content (menu items, widgets, categories, etc.) from
seed.json - Recreated necessary menus and widgets via API each time the local environment was reinitialized
This wasn't a permanent fix, just a mitigation strategy, but it at least prevented accidents where unintended demo data overwrote local changes.
Summary
- EmDash is a headless CMS that natively supports multiple languages with per-locale content
- With SSG you can translate once at build time and cache it, but with SSR, translating on-demand becomes prohibitively expensive
- The solution is to run translation only once at content creation time, not at request time, and store the result in the database as regular CMS content.
- The translation engine used was Gemini API (
gemini-3.6-flash). - However, when localizing a CMS to multiple languages, remember that setting up the translation flow alone is not enough—the fact that local and production databases are separate creates new pitfalls.
- In particular, development workflows that involve preparing content locally before it exists in production are prone to side effects from environment synchronization mechanisms (pulls, seeding, caching, and so on).
The key takeaway from this experience is that if you plan ahead for two things—when to run translations and when/what environment synchronization will overwrite—you can support multiple languages in SSR without being derailed by operational costs and incidents.
How about adding multilingual support to your site as well?
I focus on frontend development with markup, JavaScript, React, and Next.js. I'm always happy when a site I've worked on goes live successfully! My hobbies are playing guitar, and I love cats and roasted sweet potatoes 🐱🍠
Hiraicchi
Frontend Engineer / Joined 2022