En Diil cualquier texto puede existir en varios idiomas, y la API te los da como mapas sencillos: { "en": "Hello", "de": "Hallo" }. Esta guía explica cómo pedir exactamente los idiomas que necesitas, qué pasa cuando falta una traducción y cómo montar un sitio multilingüe que nunca le enseñe a un visitante un titular vacío.
El parámetro lang: uno, varios o todos los idiomas
Todos los endpoints de contenido (páginas, secciones, bloques y el blog) aceptan el mismo parámetro de query lang. Solo recorta las traducciones de la respuesta; la estructura de la página se queda igual.
langquerystringopcionalPor defecto: todos los idiomas activos?lang=en), una lista separada por comas (?lang=en,de) o un parámetro repetido (?lang=en&lang=de). Hasta 50 códigos. Cada uno tiene que ser un idioma activo del sitio.| Envías | Recibes |
|---|---|
| nada | Todos los idiomas activos del sitio |
?lang=de | Solo alemán |
?lang=de,en | Alemán e inglés |
?lang=de&lang=en | Lo mismo, para las librerías a las que les gustan los parámetros repetidos |
curl "https://back.sitecog.com/content/v1/pages/home?lang=de,en" \
-H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"const params = new URLSearchParams({ lang: 'de,en' });
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?' + params, {
headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
});
const page = await res.json();¿Qué idiomas están activos? Pregúntaselo a GET /v1/langs. Los devuelve en el orden fijado en el CRM, y esa lista es la columna vertebral de todo lo que viene a continuación.
[
{ "key": "en", "title": "English", "index": 0 },
{ "key": "de", "title": "German", "index": 1 },
{ "key": "uk", "title": "Українська", "index": 2 }
]Qué pinta tienen los mapas de idiomas
Allí donde un texto se puede traducir, recibes un objeto con los códigos de idioma como claves. Bloques de texto y html, títulos de enlaces, títulos y descripciones de páginas, títulos del blog: todos tienen la misma forma.
"promo_title": {
"type": "text",
"content": { "en": "Free delivery this week", "de": "" }
}Fíjate bien: en este objetito hay tres situaciones distintas:
en: traducido, todo en orden.de: el editor borró el texto en alemán, así que es una cadena vacía.uk: nunca se rellenó, así que la clave directamente no está.
Los mapas del blog son algo más ordenados: allí las traducciones vacías se omiten del todo. En cualquier caso, trata "" y una clave ausente como lo mismo: «sin traducción». Todas las formas están en la página Tipos de bloque.
Orden de las claves y estabilidad del ETag
Las claves de idioma siempre llegan en el orden fijado en el CRM, da igual cómo las pongas en la petición. ?lang=de,en y ?lang=en,de devuelven exactamente el mismo cuerpo, byte a byte, con el mismo ETag, así que un 304 Not Modified funciona lo escribas como lo escribas.
Errores de validación
La API es estricta con lang a propósito: una errata tiene que fallar a lo grande en desarrollo, no devolver en silencio una página vacía en producción. Los tres errores son 400 con un cuerpo JSON.
invalid_lang: el código está mal formado
Un código tiene que tener la forma en o pt-BR: dos letras minúsculas, opcionalmente un guion y de 2 a 4 letras más. La comprobación distingue mayúsculas, así que EN no pasa.
{ "message": "invalid_lang", "lang": "EN" }unknown_lang: el idioma no está activo en el sitio
El código está bien formado, pero el sitio no tiene ese idioma o está desactivado en el CRM. El cuerpo, muy amable, te dice qué códigos eran desconocidos y cuáles puedes usar.
{
"message": "unknown_lang",
"lang": "fr",
"unknown": ["fr"],
"available": ["en", "de"]
}Aquí las mayúsculas también cuentan: si el idioma del sitio es pt-BR, entonces pt-br pasa la comprobación de formato, pero es otro código y recibes unknown_lang.
too_many_langs: más de 50 códigos
{ "message": "too_many_langs", "max": 50 }Si alguna vez te topas con este, seguramente lo que querías era omitir lang del todo: sin parámetro significa todos los idiomas.
No hay respaldo en el servidor
Si falta la traducción al alemán, la API no te cuela la inglesa en su lugar. Recibes exactamente lo que guardaron los editores: una cadena vacía o ninguna clave. Es deliberado: solo tú sabes si tu sitio debería mostrar el inglés, ocultar el bloque o poner un aviso de «todavía sin traducir».
La otra cara de la moneda: el respaldo es cosa tuya. Por suerte, son unas diez líneas.
Respaldo recomendado en el cliente
Te proponemos este orden, y es el que usan todos los ejemplos de esta documentación:
El idioma pedido
El visitante está en/de/, así que prueba primero conde.El idioma por defecto del sitio
El primer idioma de/v1/langs, el que el propietario puso arriba del todo en el CRM.Cualquier traducción no vacía
Mejor algo que nada. Si ni eso, una cadena vacía.
const API = 'https://back.sitecog.com/content';
export type SiteLang = { key: string; title: string; index: number };
export type LangMap = Record<string, string>;
/** Idiomas activos en el orden del CRM; el primero es el idioma por defecto del sitio */
export async function getLangs(): Promise<SiteLang[]> {
const res = await fetch(API + '/v1/langs', {
headers: { 'x-crm-key': process.env.CRM_KEY! },
next: { revalidate: 60 },
});
if (!res.ok) throw new Error('Content API: ' + res.status);
return res.json();
}
/** Idioma pedido → idioma de respaldo → primera traducción no vacía → '' */
export function t(map: LangMap | null | undefined, lang: string, fallback?: string): string {
if (!map) return '';
const own = map[lang];
if (own) return own;
const backup = fallback ? map[fallback] : '';
if (backup) return backup;
return Object.values(map).find((value) => value !== '') ?? '';
}
/** Valor de ?lang=: el idioma del visitante más el de por defecto, siempre en el orden del CRM */
export function langParam(lang: string, langs: SiteLang[]): string {
const fallback = langs[0]?.key;
return langs
.filter((item) => item.key === lang || item.key === fallback)
.map((item) => item.key)
.join(',');
}Este mismo t() mueve el renderizador universal de bloques, y pickMedia() aplica allí exactamente el mismo orden a las imágenes y vídeos por idioma.
Detectar el idioma del visitante
A la API le da igual cómo elijas el idioma: solo necesita un código válido. Aquí tienes un montaje fácil de razonar, amigable con los buscadores y considerado con los visitantes.
La URL manda
Pon el idioma en la ruta: /en/about, /de/about, /uk/about. Cada idioma tiene su propia dirección, los enlaces se pueden compartir y los buscadores indexan cada versión por separado. La lista de prefijos válidos es, sin más, /v1/langs.
import { notFound } from 'next/navigation';
import { getLangs, langParam, t } from '@/lib/langs';
export default async function Home({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const langs = await getLangs();
// Nunca pases un segmento de la URL tal cual a la API: /fr/ acabaría en un 400
if (!langs.some((item) => item.key === lang)) notFound();
const fallback = langs[0].key;
const res = await fetch(
'https://back.sitecog.com/content/v1/pages/home?lang=' + langParam(lang, langs),
{ headers: { 'x-crm-key': process.env.CRM_KEY! }, next: { revalidate: 60 } },
);
const page = await res.json();
const hero = page.content.hero.content;
return <h1>{t(hero.hero_title.content, lang, fallback)}</h1>;
}El dominio a secas elige un idioma
Solo la raíz / tiene que adivinar. Comprueba, en este orden: el idioma que el visitante eligió antes (una cookie), el Accept-Language del navegador y, por último, el idioma por defecto del sitio.
/** La mejor coincidencia para la cabecera Accept-Language: 'de-AT,de;q=0.9' → 'de' */
export function langFromHeader(header: string, langs: SiteLang[]): string | null {
const keys = langs.map((item) => item.key);
for (const part of header.split(',')) {
const code = part.split(';')[0].trim();
const base = code.split('-')[0];
if (keys.includes(code)) return code;
if (keys.includes(base)) return base;
}
return null;
}import { cookies, headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { getLangs, langFromHeader } from '@/lib/langs';
export default async function Root() {
const langs = await getLangs();
const saved = (await cookies()).get('lang')?.value;
const accept = (await headers()).get('accept-language') ?? '';
const lang =
langs.find((item) => item.key === saved)?.key ?? // el visitante ya lo eligió antes
langFromHeader(accept, langs) ?? // la preferencia del navegador
langs[0].key; // el idioma por defecto del sitio
redirect('/' + lang);
}El selector escribe esa cookie cada vez que alguien elige un idioma a mano. Los nombres vienen directamente del CRM:
'use client';
import { usePathname } from 'next/navigation';
import type { SiteLang } from '@/lib/langs';
export function LangSwitcher({ langs, current }: { langs: SiteLang[]; current: string }) {
// '/de/about/team' → 'about/team'
const rest = usePathname().split('/').slice(2).join('/');
const remember = (lang: string) => {
document.cookie = 'lang=' + lang + '; path=/; max-age=31536000; samesite=lax';
};
return (
<nav aria-label="Idioma">
{langs.map((item) => (
<a
key={item.key}
href={'/' + item.key + (rest ? '/' + rest : '')}
aria-current={item.key === current ? 'page' : undefined}
onClick={() => remember(item.key)}
>
{item.title}
</a>
))}
</nav>
);
}SEO: hreflang para cada idioma
Diles a los buscadores que /en/about y /de/about son la misma página en distintos idiomas. Así quien busca en alemán aterriza en la versión alemana, y las dos páginas no se tratan como duplicadas. Genera las etiquetas a partir de /v1/langs, y un idioma añadido en el CRM aparecerá aquí solo.
<html lang="de">
<head>
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="de" href="https://example.com/de/about" />
<link rel="alternate" hreflang="uk" href="https://example.com/uk/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
</head>// Códigos que el propietario puede haber escrito distinto de ISO 639-1
const HREFLANG_FIX: Record<string, string> = { ua: 'uk' };
export function alternates(path: string, langs: SiteLang[], origin: string) {
const links = langs.map((item) => ({
hreflang: HREFLANG_FIX[item.key] ?? item.key,
href: origin + '/' + item.key + path,
}));
if (langs[0]) links.push({ hreflang: 'x-default', href: origin + '/' + langs[0].key + path });
return links;
}- Cada versión de idioma lista todas las versiones, incluida ella misma.
x-defaultes la versión para todos los demás; el idioma por defecto del sitio es una elección sensata.- En Next.js esta misma lista va en
alternates.languagesdegenerateMetadata. - Pon también
<html lang>al idioma actual: los lectores de pantalla y las herramientas de traducción dependen de él.
Códigos de idioma, y una nota sobre el ucraniano
La API usa exactamente los códigos que el propietario del sitio configuró en el CRM. Dos letras minúsculas, opcionalmente con región: en, de, pt-BR. No escribas la lista a mano en tu lado: léela de /v1/langs, y añadir un idioma pasará a ser un ajuste del CRM en lugar de un despliegue.