Diil Docs
  1. Документация
  2. Руководства

Языки, параметр lang и запасной язык

Обновлено:

Любой текст в Diil может жить на нескольких языках, и API отдаёт переводы простыми картами: { "en": "Hello", "de": "Hallo" }. В этом руководстве — как запросить ровно те языки, что нужны, что происходит, когда перевода нет, и как собрать многоязычный сайт, который никогда не покажет посетителю пустой заголовок.

Параметр lang: один язык, несколько или все

Все запросы контента — страницы, секции, блоки и блог — понимают один и тот же параметр lang. Он только отсекает лишние переводы, структура страницы в ответе остаётся прежней.

langquerystringнеобязательноПо умолчанию: все активные языки
Один код (?lang=en), список через запятую (?lang=en,de) или повторённый параметр (?lang=en&lang=de). До 50 кодов. Каждый должен быть активным языком сайта.
Что отправилиЧто получили
ничегоВсе активные языки сайта
?lang=deТолько немецкий
?lang=de,enНемецкий и английский
?lang=de&lang=enТо же самое — для библиотек, которые любят повторять параметры
curl "https://back.sitecog.com/content/v1/pages/home?lang=de,en" \
  -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"

Какие языки активны? Спросите GET /v1/langs. Он отдаёт их в том порядке, что задан в CRM, и на этом списке держится всё, о чём пойдёт речь дальше.

GET /v1/langsjson
[
  { "key": "en", "title": "English", "index": 0 },
  { "key": "de", "title": "German", "index": 1 },
  { "key": "uk", "title": "Українська", "index": 2 }
]

Как выглядят карты переводов

Везде, где текст можно перевести, приходит объект с кодами языков в ключах. Текстовые и HTML-блоки, подписи ссылок, заголовки и описания страниц, заголовки статей блога — форма одна и та же.

Текстовый блок, ?lang=en,de,ukjson
"promo_title": {
  "type": "text",
  "content": { "en": "Free delivery this week", "de": "" }
}

Присмотритесь: в этом крошечном объекте сразу три разные ситуации.

  • en — перевод есть, всё хорошо.
  • de — редактор стёр немецкий текст, поэтому пустая строка.
  • uk — его вообще не заполняли, поэтому ключа просто нет.

В блоге карты чуть аккуратнее: пустые переводы там не попадают в ответ вовсе. Так или иначе, считайте "" и отсутствующий ключ одним и тем же — «перевода нет». Все формы содержимого собраны на странице «Типы блоков».

Порядок ключей и стабильный ETag

Ключи языков всегда идут в порядке, заданном в CRM, как бы вы ни перечислили их в запросе. ?lang=de,en и ?lang=en,de возвращают байт в байт одинаковый ответ с одинаковым ETag, так что 304 Not Modified срабатывает при любом порядке.

Ошибки проверки

К lang API относится строго — и это нарочно: опечатка должна громко падать при разработке, а не тихо отдавать пустую страницу на боевом сайте. Все три ошибки — 400 с JSON в теле.

invalid_lang — код записан неправильно

Код должен выглядеть как en или pt-BR: две строчные латинские буквы, а за ними при необходимости дефис и ещё 2–4 буквы. Регистр важен, поэтому EN не пройдёт.

GET /v1/pages/home?lang=EN → 400json
{ "message": "invalid_lang", "lang": "EN" }

unknown_lang — такого языка на сайте нет

Код записан правильно, но у сайта нет такого языка или он выключен в CRM. В ответе заботливо перечислено, какие коды не подошли и какие можно использовать.

GET /v1/pages/home?lang=fr → 400json
{
  "message": "unknown_lang",
  "lang": "fr",
  "unknown": ["fr"],
  "available": ["en", "de"]
}

Регистр важен и здесь: если язык сайта — pt-BR, то pt-br проверку формата пройдёт, но это уже другой код, и вы получите unknown_lang.

too_many_langs — больше 50 кодов

400json
{ "message": "too_many_langs", "max": 50 }

Если вы на это наткнулись, скорее всего, lang стоило просто не передавать: без параметра придут все языки.

Запасного языка на стороне сервера нет

Если немецкого перевода нет, API не подсунет вместо него английский. Вы получите ровно то, что сохранили редакторы: пустую строку или отсутствующий ключ. Так задумано: только вы знаете, что лучше для вашего сайта — показать английский, спрятать блок или написать «перевод скоро будет».

Обратная сторона: запасной язык — ваша забота. К счастью, это строк десять кода.

Запасной язык на стороне сайта: как мы советуем

Порядок такой — и именно так устроены все примеры в этой документации:

  1. Запрошенный язык

    Посетитель на /de/ — сначала пробуем de.
  2. Язык сайта по умолчанию

    Первый в /v1/langs — тот, что владелец поставил наверх в CRM.
  3. Любой непустой перевод

    Лучше что-то, чем ничего. Если не нашлось и его — пустая строка.
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>;

/** Активные языки в порядке CRM; первый — язык сайта по умолчанию */
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();
}

/** Запрошенный язык → запасной → первый непустой перевод → '' */
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 !== '') ?? '';
}

/** Значение для ?lang=: язык посетителя плюс язык по умолчанию, всегда в порядке 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(',');
}

Тот же t() работает в универсальном рендерере блоков, а pickMedia() там применяет точно такой же порядок к картинкам и видео, загруженным отдельно для каждого языка.

Как определить язык посетителя

API всё равно, как вы выбираете язык, — ему нужен только правильный код. Вот схема, в которой легко разобраться, которую любят поисковики и которая не раздражает посетителей.

Главный — адрес страницы

Вынесите язык в путь: /en/about, /de/about, /uk/about. У каждого языка свой адрес, ссылкой можно поделиться, а поисковики индексируют каждую версию отдельно. Список допустимых префиксов — это просто /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();

  // Кусок адреса в API напрямую не отдаём: /fr/ превратился бы в 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>;
}

Голый домен сам выбирает язык

Угадывать приходится только на корне /. Проверяем по порядку: язык, который посетитель уже выбирал (cookie), Accept-Language браузера и, наконец, язык сайта по умолчанию.

lib/langs.ts (продолжение)ts
/** Лучшее совпадение с 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 ?? // посетитель уже выбирал
    langFromHeader(accept, langs) ??                  // что предпочитает браузер
    langs[0].key;                                     // язык сайта по умолчанию

  redirect('/' + lang);
}

Переключатель записывает эту cookie каждый раз, когда язык выбирают вручную. Названия языков берутся прямо из 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="Язык">
      {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 для каждого языка

Подскажите поисковикам, что /en/about и /de/about — одна и та же страница на разных языках. Тогда немецкоязычный пользователь попадёт на немецкую версию, а две страницы не будут считаться дублями. Собирайте теги из /v1/langs — и язык, добавленный в CRM, появится здесь сам.

В <head> страницы /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 (продолжение)ts
// Коды, которые владелец мог записать не так, как в 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;
}
  • Каждая языковая версия перечисляет все версии, включая саму себя.
  • x-default — версия «для всех остальных»; язык сайта по умолчанию — разумный выбор.
  • В Next.js тот же список отправляется в alternates.languages внутри generateMetadata.
  • Не забудьте и <html lang> с текущим языком — на него опираются экранные дикторы и переводчики в браузере.

Коды языков и пара слов про украинский

API использует ровно те коды, которые владелец сайта завёл в CRM: две строчные буквы, при необходимости с регионом — en, de, pt-BR. Не зашивайте список языков у себя — читайте его из /v1/langs, и новый язык станет настройкой в CRM, а не поводом для выкладки.