Nuestro blog "Yuru Log" en realidad ha tenido una versión en inglés desde hace poco. Esta vez escribo sobre cómo resolvimos un problema específico: "¿Explotarán los costos de la API cuando multilingüizamos un sitio con SSR?"
Premisa: estamos usando un CMS llamado EmDash
Este blog funciona con EmDash, un CMS basado en Astro. Escribimos artículos desde el panel de administración, y el frontend se renderiza en el servidor (SSR) con Astro.
La forma intuitiva de operarlo es similar a WordPress, y de hecho se dice que es el "sucesor espiritual de WordPress".
EmDash ya tiene un sistema de "contenido multilingüe" incorporado que permite mantener artículos, páginas, menús y taxonomías (categorías y etiquetas) como registros separados por localidad. Solo con vincularlos mediante un campo translationOf que diga "este artículo en inglés es la traducción de este artículo en japonés", se tratan automáticamente como versiones en diferentes idiomas del mismo artículo.
Migrar de microCMS a EmDash + Cloudflare
Cuando se trata de multilingüismo, es fácil si usas SSG
Por lo general, cuando se trata de "automatizar traducciones multilingües mediante API", si usas SSG (Static Site Generation), el tema es sencillo.
- Solo llamamos a la API de traducción una vez en el momento de la compilación
- Almacenar en caché los resultados como archivos estáticos
- Lo único que queda es servir ese HTML
Por lo tanto, sin importar cuántos artículos se agreguen, los costos de API solo se generan "cada vez que se construye". La frecuencia es fácil de controlar y el almacenamiento en caché es sencillo.
Pero Yurulog es SSR. Entonces, ¿se consulta la API en cada solicitud?
El problema está aquí. Las páginas CMS de EmDash se renderizan completamente mediante SSR (output: "server") por especificación. No es un modelo donde se crea la página en una compilación estática, sino un sistema donde cada vez que llega una solicitud, se obtiene el contenido más reciente de la API y base de datos del CMS y se renderiza.
Entonces, ¿qué pasaría si implementáramos de forma ingenua la idea de "mostrar la página en inglés traduciendo con Gemini sobre la marcha"?
- Se genera un cargo de API de traducción con cada solicitud
- La latencia también aumenta (hay que esperar la respuesta del LLM)
- Es posible que los resultados de traducción difieran ligeramente cada vez para el mismo artículo (falta de reproducibilidad)
Esto es completamente inaceptable. Si se adopta "consultar la API cada vez" solo porque es SSR, tanto los costos como la experiencia del usuario se verán comprometidos.
Solución: trasladar la traducción de «en el momento de la solicitud» a «durante la creación del contenido»
La respuesta es simple: no realizar la traducción en el momento de la solicitud.
En concreto, implementé este flujo de trabajo:
- Escribir y publicar artículos en japonés en el panel de control de EmDash
- Ejecutar el script de traducción (
scripts/translate.mjs) una sola vez- Obtener el Portable Text (texto enriquecido estructurado) del artículo en japonés desde la API de EmDash
- Extraer solo las partes de texto que deben traducirse y enviarlas a la API de Gemini (
gemini-3.6-flash) - Los bloques como imágenes y tarjetas de productos de Amazon se mantienen sin traducir
- Guardar el resultado de la traducción como un nuevo artículo en inglés en la base de datos de EmDash
- Especificar el ID del artículo original en japonés en
translationOfpara vincularlo como versión en inglés del mismo grupo de artículos - Se guarda como borrador, para que puedas revisar antes de publicar.
- Especificar el ID del artículo original en japonés en
Es decir, el resultado de la traducción se trata como «contenido en otro idioma almacenado en el CMS» en lugar de «algo generado sobre la marcha».
De esta manera, el procesamiento de solicitudes SSR en sí no difiere de la visualización normal de artículos. Solo extraes contenido en inglés ya almacenado en la BD usando la consulta de localización estándar de EmDash (getEmDashEntry(..., { locale: "en" })). La comunicación con la API de Gemini ocurre solo una vez al traducir el artículo, y nunca sucede durante la visualización.
En otras palabras, así es como se estructura.
Cuándo se ejecuta la traducción | Costo de API en el momento de la solicitud | |
|---|---|---|
SSG + traducción bajo demanda | En el momento de la compilación | Cero (distribución de archivos estáticos) |
SSR + traducción bajo demanda (ejemplo incorrecto) | Por solicitud | Se genera una vez por artículo × número de accesos 😱 |
SSR + traducción previa guardada en BD (método utilizado esta vez) | Al crear contenido (una sola vez) | Cero |
El aprendizaje clave de esta implementación fue llevar la idea de SSG de «almacenar en caché todo en tiempo de compilación» al mundo de SSR mediante «persistir el contenido traducido previamente en la BD». Aunque SSR genere contenido dinámicamente, si realizamos el trabajo pesado de la traducción de antemano, la entrega en sí es tan ligera como una página CMS normal.
Cuando la BD es diferente entre local y producción, y pasamos por el momento «¡Espera, desapareció!?» varias veces
Lo que más agotador fue en esta implementación fue precisamente esto. EmDash utiliza bases de datos diferentes para el entorno de desarrollo local y el entorno de producción. Aunque parezca obvio, esto causó que experimentáramos múltiples veces el «¡La función que había hace poco desapareció!».
Los síntomas suelen ser así
- La navegación del encabezado de repente queda vacía
- Las categorías del footer desaparecen
- Los widgets de la barra lateral de la página de artículos desaparecen
- Los elementos de menú y widgets locales que acababa de crear vuelven al estado anterior
Además, todo sucede justo cuando "estaba funcionando hace poco", así que al principio empecé a buscar la causa preguntándome si había roto algo.
Causa 1: El proceso de incorporación de la base de datos de producción al entorno local (pull.sh)
Durante el desarrollo, ejecuté varias veces un script que importaba completamente la base de datos de producción al entorno local porque quería ver los datos más recientes de producción también localmente. Cuando hago esto, la base de datos local se sobrescribe completamente con el estado de producción (es decir, cuando los artículos en inglés aún están en borrador, o la estructura del menú antes de publicación).
En otras palabras, cuando intentaba confirmar "una función que aún no está publicada en producción" localmente, inmediatamente después de hacer pull volvía al mismo estado que en producción, por lo que el trabajo que había avanzado solo localmente desaparecía temporalmente.
Causa 2: El proceso de "seed" que se ejecuta cada vez que ingreso nuevamente a la pantalla de administración local
La otra causa era que el mecanismo para volver a iniciar sesión en la pantalla de administración local (un login de derivación para desarrollo) también estaba leyendo nuevamente seed.json (el archivo de definición de datos iniciales) cada vez.
Lo molesto de esto era que,
- Elementos de menú y widgets → se sobrescriben para que coincidan exactamente con el contenido del lado del seed (es decir, los elementos agregados manualmente en local desaparecen)
- Taxonomías como categorías y etiquetas → se omiten los que ya existen, por lo que no desaparecen
es decir, el comportamiento difería según el elemento. Por eso ocurría este fenómeno aparentemente inexplicable donde «las categorías permanecían pero solo los menús y widgets desaparecían cada vez».
Medidas implementadas
Era difícil eliminar fundamentalmente esta estructura dual (sobrescritura por pull / sobrescritura por seed), así que como solución práctica adoptamos:
- Vaciar completamente el contenido de demostración ficticio (elementos de menú, widgets, categorías, etc.) desde
seed.json - Cada vez que volvemos al entorno local, recrear los menús y widgets necesarios a través de la API
de este modo nos las arreglamos. No es una solución radical, sino una medida paliativa, pero al menos logramos evitar el accidente de ser sobrescrito por datos de demostración no intencionados.
Conclusión
- EmDash es originalmente un CMS multilingüe que puede tener contenido para cada locale
- Con SSG, basta con «traducir en el momento de la compilación → cachear», pero con SSR, «traducir cada vez» se convierte en una pesadilla de facturación
- La solución es ejecutar la traducción solo una vez en el momento de crear contenido, no en el momento de solicitar, y guardar el resultado como contenido CMS normal en la base de datos
- El motor de traducción utilizado fue la API Gemini (
gemini-3.6-flash) - Sin embargo, al multilocalizar un CMS, no es suficiente simplemente organizar el flujo de traducción; también debe recordarse que el hecho de que la base de datos esté separada entre el entorno local y el de producción en sí mismo genera nuevas trampas
- Especialmente en el desarrollo del tipo «crear y trabajar por adelantado en local con un estado que aún no existe en producción», es fácil ser arrastrado por los efectos secundarios de los mecanismos de sincronización de entornos (pull, seed, caché, etc.)
La lección de esta vez es que, si se entienden de antemano estos dos aspectos del diseño de tiempo: «cuándo hacer la traducción» y «cuándo y qué sobrescribe la sincronización de entornos», es posible ofrecer soporte multilingüe incluso con SSR sin verse afectado por los costos operativos y los incidentes
¿Qué les parece multilingüizar sus sitios también!
Me dedico al desarrollo frontend con enfoque en el marcado, utilizando JavaScript, React y Next.js. ¡Me alegra mucho cuando los sitios en los que he trabajado se publican exitosamente! Mi hobby es tocar la guitarra. Me encantan los gatos y los boniatos🐱🍠
Hiraitch
Ingeniero de frontend / Incorporado en 2022