Любой текст в 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"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 }
]Как выглядят карты переводов
Везде, где текст можно перевести, приходит объект с кодами языков в ключах. Текстовые и 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 срабатывает при любом порядке.
Ошибки проверки
К lang API относится строго — и это нарочно: опечатка должна громко падать при разработке, а не тихо отдавать пустую страницу на боевом сайте. Все три ошибки — 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 всё равно, как вы выбираете язык, — ему нужен только правильный код. Вот схема, в которой легко разобраться, которую любят поисковики и которая не раздражает посетителей.
Главный — адрес страницы
Вынесите язык в путь: /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 напрямую не отдаём: /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, а не поводом для выкладки.