Diil Docs
  1. Документація
  2. Посібники

Мови, параметр lang і запасна мова

Оновлено:

Кожен текст у Diil може існувати кількома мовами, а API віддає їх простими картами: { "en": "Hello", "de": "Hallo" }. У цьому посібнику — як запитувати саме ті мови, що вам потрібні, що буває, коли перекладу немає, і як зібрати багатомовний сайт, який ніколи не покаже відвідувачеві порожній заголовок.

Параметр lang: одна, кілька чи всі мови

Кожен endpoint вмісту — сторінки, секції, блоки і блог — приймає однаковий query-параметр 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 }
]

Як виглядають карти мов

Скрізь, де текст можна перекласти, ви отримуєте об’єкт із ключами-кодами мов. Блоки text і 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 спрацює за будь-якого написання.

Помилки валідації

API навмисно суворий щодо lang: друкарська помилка має гучно падати під час розробки, а не тихо повертати порожню сторінку в продакшені. Усі три помилки — це 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 байдуже, як ви обираєте мову, — йому потрібен лише коректний код. Ось схема, яку легко зрозуміти, яка дружня до пошуковиків і дбайлива до відвідувачів.

URL — джерело істини

Додайте мову в шлях: /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 сирий сегмент URL: /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, а не новим деплоєм.