Кожен текст у 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"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();Які мови активні? Запитайте GET /v1/langs. Він повертає їх у порядку, заданому в CRM, і цей список — основа всього, про що йдеться нижче.
[
{ "key": "en", "title": "English", "index": 0 },
{ "key": "de", "title": "German", "index": 1 },
{ "key": "uk", "title": "Українська", "index": 2 }
]Як виглядають карти мов
Скрізь, де текст можна перекласти, ви отримуєте об’єкт із ключами-кодами мов. Блоки text і html, заголовки посилань, назви й описи сторінок, заголовки дописів блогу — усе має таку саму форму.
"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 не пройде.
{ "message": "invalid_lang", "lang": "EN" }unknown_lang — мова не активна на сайті
Формат коду правильний, але такої мови на сайті немає або її вимкнено в CRM. Тіло відповіді люб’язно підкаже, які коди невідомі, а якими можна користуватися.
{
"message": "unknown_lang",
"lang": "fr",
"unknown": ["fr"],
"available": ["en", "de"]
}Регістр тут теж важить: якщо мова сайту — pt-BR, то pt-br пройде перевірку формату, але це інший код, і ви отримаєте unknown_lang.
too_many_langs — понад 50 кодів
{ "message": "too_many_langs", "max": 50 }Якщо ви колись на це натрапите, найімовірніше, ви хотіли взагалі не передавати lang: без параметра — усі мови.
Запасної мови на сервері немає
Якщо німецького перекладу немає, API не підсовує тихенько англійський замість нього. Ви отримуєте рівно те, що зберегли редактори: порожній рядок або взагалі жодного ключа. Це навмисно — лише ви знаєте, чи має ваш сайт показати натомість англійську, сховати блок чи вивести примітку «ще не перекладено».
Зворотний бік медалі: запасна мова — ваша турбота. На щастя, це приблизно десять рядків.
Рекомендована запасна мова на клієнті
Пропонуємо такий порядок — саме його використовують усі приклади в цій документації:
Запитана мова
Відвідувач на/de/, тож спершу пробуємоde.Мова сайту за замовчуванням
Перша мова в/v1/langs— та, яку власник поставив угору в CRM.Будь-який непорожній переклад
Краще щось, ніж нічого. Якщо й цього немає — порожній рядок.
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.
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 браузера і, нарешті, мову сайту за замовчуванням.
/** Найкращий збіг для заголовка 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 ?? // відвідувач уже обирав її
langFromHeader(accept, langs) ?? // уподобання браузера
langs[0].key; // мова сайту за замовчуванням
redirect('/' + lang);
}Перемикач записує цю cookie щоразу, коли хтось обирає мову вручну. Назви мов надходять просто з 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="Мова">
{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, з’явиться тут автоматично.
<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>// Коди, які власник міг ввести не за 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, а не новим деплоєм.