Cómo Gestiono 12 Idiomas en Sanity Sin Duplicar una Sola Página

Programación· 6 min de lectura

El Problema Que Nadie Quiere Admitir

La semana pasada estaba revisando el código de un proyecto que recibimos para optimizar. El cliente quería añadir portugués a su web que ya tenía inglés, español y francés.

Abro el repositorio. 47 páginas duplicadas por idioma. Tres carpetas separadas. Tres sistemas de routing. Un infierno de mantenimiento.

Le pregunté: "¿Por qué no usáis traducciones a nivel de campo?"

Respuesta: "No sabíamos que Sanity podía hacer eso."

Y aquí está el problema: la mayoría de developers tratan el contenido multiidioma como si estuviéramos en 2015. Duplican todo. Una página en inglés, otra en español, otra en francés.

Hasta que necesitas actualizar algo. Y tienes que hacerlo 12 veces.

La Diferencia Entre Traducir Contenido y Traducir Estructura

Cuando construyes multiidioma, hay dos enfoques:

Enfoque 1: Duplicación total

  • Creas `/es/about`, `/en/about`, `/fr/about`
  • Cada página es un documento separado en tu CMS
  • Cambias un título → lo cambias en 12 lugares
  • Añades un campo nuevo → actualizas 12 schemas

Enfoque 2: Traducciones a nivel de campo (lo que uso yo)

  • Un solo documento con campos traducibles
  • El routing se genera dinámicamente
  • Cambias un título → actualizas un solo campo en múltiples idiomas
  • Añades un campo → se replica automáticamente para todos los idiomas

La diferencia no es solo técnica. Es estratégica.

Cómo Funcionan las Traducciones a Nivel de Campo en Sanity

En lugar de duplicar documentos, defines qué campos necesitan traducción:

```javascript // schemas/post.js import { defineType, defineField } from 'sanity'

export default defineType({ name: 'post', type: 'document', fields: [ defineField({ name: 'title', type: 'object', fields: [ { name: 'es', type: 'string', title: 'Español' }, { name: 'en', type: 'string', title: 'English' }, { name: 'fr', type: 'string', title: 'Français' }, { name: 'de', type: 'string', title: 'Deutsch' }, { name: 'it', type: 'string', title: 'Italiano' }, { name: 'pt', type: 'string', title: 'Português' }, // ... hasta 12 idiomas ] }), defineField({ name: 'slug', type: 'slug', options: { source: 'title.es' } }), defineField({ name: 'content', type: 'object', fields: [ { name: 'es', type: 'array', of: [{ type: 'block' }] }, { name: 'en', type: 'array', of: [{ type: 'block' }] }, // ... resto de idiomas ] }) ] }) ```

Ahora tienes UN documento con múltiples traducciones integradas.

El Poder de GROQ Para Contenido Localizado

Aquí es donde la cosa se pone interesante. GROQ (el lenguaje de queries de Sanity) te permite extraer exactamente el idioma que necesitas:

```javascript // utils/sanity.js export async function getLocalizedPost(slug, locale = 'es') { const query = ` *[_type == "post" && slug.current == $slug][0] { "title": title.${locale}, "content": content.${locale}, publishedAt, "author": author->name, "image": mainImage.asset->url } `

return await client.fetch(query, { slug }) } ```

Este query: 1. Busca el post por slug (único, no duplicado por idioma) 2. Extrae solo los campos del idioma solicitado 3. Devuelve un objeto limpio listo para usar

No hay lógica compleja. No hay if/else para cada idioma. Es un string interpolado.

Fallbacks Inteligentes Cuando Faltan Traducciones

La realidad es que no siempre tendrás todas las traducciones el día 1. Aquí está mi patrón de fallback:

```javascript export async function getLocalizedContent(slug, locale = 'es', fallback = 'en') { const query = ` *[_type == "post" && slug.current == $slug][0] { "title": coalesce(title.${locale}, title.${fallback}), "content": coalesce(content.${locale}, content.${fallback}), "locale": select( defined(title.${locale}) => "${locale}", "${fallback}" ) } `

return await client.fetch(query, { slug }) } ```

El `coalesce()` en GROQ intenta el idioma primario, y si no existe, usa el fallback. El `select()` te dice qué idioma acabaste usando.

Resultado: nunca devuelves contenido vacío. Siempre muestras algo.

Routing Dinámico en Next.js

Para el frontend, uso App Router de Next.js con un patrón simple:

```javascript // app/[locale]/blog/[slug]/page.js import { getLocalizedPost } from '@/utils/sanity'

export default async function BlogPost({ params }) { const { locale, slug } = params const post = await getLocalizedPost(slug, locale)

return ( <article> <h1>{post.title}</h1> <div>{/* render content */}</div> </article> ) }

export async function generateStaticParams() { const posts = await getAllPosts() const locales = ['es', 'en', 'fr', 'de', 'it', 'pt']

return posts.flatMap(post => locales.map(locale => ({ locale, slug: post.slug.current })) ) } ```

Una sola página de Next.js genera rutas para todos los idiomas. No duplicas componentes. No duplicas lógica.

Por Qué Esto Importa Más Allá de la Traducción

Lo que aprendí construyendo esto:

1. El contenido es código

Cuando tratas el contenido como datos estructurados (no como páginas HTML), puedes aplicar los mismos principios DRY que usas en tu código.

2. Los CMSs headless cambian el juego

Hace 5 años, implementar esto requería plugins custom, bases de datos complejas, y mucho código. Hoy es configuración.

3. La escalabilidad no es solo técnica

Cuando añado un idioma nuevo, no refactorizo rutas ni duplico contenido. Añado campos al schema de Sanity y actualizo mi lista de locales. 15 minutos.

Lo Que Cambiaría Si Empezara Hoy

Si estuviera construyendo esto desde cero ahora mismo:

1. Usaría un helper centralizado para idiomas En lugar de hardcodear `title.es`, tendría un objeto de configuración con todos los idiomas soportados.

2. Implementaría detección automática de idioma del navegador Con middleware de Next.js para redirigir al idioma correcto en la primera visita.

3. Añadiría un campo "translation status" Para marcar qué contenido está traducido al 100%, qué está en progreso, qué necesita revisión.

4. Crearía un dashboard de completitud de traducciones Una vista en Sanity Studio que muestre qué posts tienen qué idiomas completos.

El Verdadero Beneficio

Después de 6 meses con este sistema en producción:

  • Gestiono contenido en 12 idiomas
  • Añadir un post nuevo toma el mismo tiempo que antes (uno solo)
  • Añadir un idioma nuevo: menos de 30 minutos
  • Cero bugs de sincronización entre idiomas (porque no hay duplicación)
  • Los editores de contenido no necesitan entender el código

La conclusión no es "usa Sanity" (aunque lo recomiendo). Es: deja de duplicar contenido.

Las herramientas modernas ya resolvieron este problema. Si todavía estás manteniendo carpetas separadas para cada idioma, estás trabajando más duro, no más inteligente.

Y en 2025, cuando Claude puede escribir la configuración inicial en 3 minutos, no hay excusa para seguir haciéndolo mal.

Brian Mena

Brian Mena

Ingeniero informatico construyendo productos digitales rentables: SaaS, directorios y agentes de IA. Todo desde cero, todo en produccion.

LinkedIn