Diil Docs
  1. Documentación
  2. Guías

Idiomas, el parámetro lang y el idioma de respaldo

Actualizado:

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
Un código (?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íasRecibes
nadaTodos los idiomas activos del sitio
?lang=deSolo alemán
?lang=de,enAlemán e inglés
?lang=de&lang=enLo 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"

¿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.

GET /v1/langsjson
[
  { "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.

Un bloque de texto, ?lang=en,de,ukjson
"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.

GET /v1/pages/home?lang=EN → 400json
{ "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.

GET /v1/pages/home?lang=fr → 400json
{
  "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

400json
{ "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:

  1. El idioma pedido

    El visitante está en /de/, así que prueba primero con de.
  2. El idioma por defecto del sitio

    El primer idioma de /v1/langs, el que el propietario puso arriba del todo en el CRM.
  3. Cualquier traducción no vacía

    Mejor algo que nada. Si ni eso, una cadena vacía.
lib/langs.tsts
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.

app/[lang]/page.tsxtsx
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.

lib/langs.ts (continuación)ts
/** 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;
}
app/page.tsxtsx
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:

components/LangSwitcher.tsxtsx
'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.

En el <head> de /de/abouthtml
<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>
lib/langs.ts (continuación)ts
// 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-default es 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.languages de generateMetadata.
  • 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.