← Назад к вопросам
Как реализуешь приложение с мультиязычностью для динамических страниц создаваемых через админку?
1.8 Middle🔥 123 комментариев
#JavaScript Core
Комментарии (3)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Мультиязычность для динамических страниц из админки
Реализация i18n для динамического контента, создаваемого через админку - одна из самых сложных задач в веб-разработке. Подход зависит от архитектуры приложения, но основная идея - разделить структуру контента и переводы.
Подходы к реализации
1. Переводы хранятся в БД (Database-first approach) Это самый гибкий подход для динамического контента:
// Структура в базе данных
// Table: pages
{
id: "UUID",
slug: "about-us",
template: "standard", // какой шаблон использовать
created_at: "timestamp"
}
// Table: page_translations
{
id: "UUID",
page_id: "UUID",
language: "en",
title: "About Us",
content: "<h1>About Us</h1>...",
meta_description: "...",
slug: "about-us",
created_at: "timestamp"
}
// Frontend - компонент для отображения динамической страницы
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
function DynamicPage() {
const { locale } = useRouter(); // "en", "ru", "de"
const { slug } = useRouter().query;
const [page, setPage] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!slug) return;
// Запрашиваем перевод на нужном языке
fetch(`/api/pages/${slug}?lang=${locale}`)
.then(res => res.json())
.then(data => {
setPage(data);
setLoading(false);
})
.catch(err => {
console.error("Failed to fetch page:", err);
setLoading(false);
});
}, [slug, locale]);
if (loading) return <div>Loading...</div>;
if (!page) return <div>Page not found</div>;
return (
<article>
<h1>{page.title}</h1>
<div dangerouslySetInnerHTML={{ __html: sanitizeHtml(page.content) }} />
</article>
);
}
export default DynamicPage;
2. Структура API для админки
// API endpoint для получения страницы
// GET /api/pages/{slug}?lang=en
Response:
{
id: "uuid",
slug: "about-us",
template: "standard",
translations: {
en: {
title: "About Us",
content: "...",
meta_description: "...",
slug: "about-us"
},
ru: {
title: "О нас",
content: "...",
meta_description: "...",
slug: "o-nas"
}
}
}
// API endpoint для создания/обновления страницы
// POST/PUT /api/pages
{
slug: "about-us",
template: "standard",
translations: {
en: { title: "About Us", content: "...", ... },
ru: { title: "О нас", content: "...", ... }
}
}
Реализация с next-i18next
// next-i18next конфиг для работы с админ-контентом
// next-i18next.config.js
module.exports = {
i18n: {
defaultLocale: "en",
locales: ["en", "ru", "de"],
},
localePath: "./public/locales", // для статических переводов
ns: ["common", "pages"], // пространства имён
};
// Комбинированный подход: статические + динамические переводы
import { useTranslation } from "next-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
function PageComponent() {
const { t } = useTranslation("common");
const [dynamicContent, setDynamicContent] = useState(null);
// Статический перевод меню, кнопок
const navigationLabel = t("pages.navigation.home");
// Динамический контент из БД
useEffect(() => {
const locale = useRouter().locale;
fetch(`/api/pages/${slug}?lang=${locale}`)
.then(res => res.json())
.then(data => setDynamicContent(data));
}, [slug]);
return (
<div>
<nav>{navigationLabel}</nav>
{dynamicContent && (
<h1>{dynamicContent.title}</h1>
)}
</div>
);
}
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ["common", "pages"])),
},
};
}
export default PageComponent;
Продвинутый подход: Contentful + i18n
// Если используется Contentful (CMS as a Service)
import { createClient } from "contentful";
const client = createClient({
space: process.env.CONTENTFUL_SPACE,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
});
async function getPageFromContentful(locale) {
const entries = await client.getEntries({
content_type: "page",
"fields.slug": slug,
locale: locale, // Contentful автоматически возвращает нужный язык
});
return entries.items[0]?.fields || null;
}
// Page component
function DynamicPage() {
const [page, setPage] = useState(null);
const { locale } = useRouter();
useEffect(() => {
getPageFromContentful(locale).then(setPage);
}, [locale]);
return (
<article>
{page && (
<>
<h1>{page.title}</h1>
<RichTextRenderer document={page.content} />
</>
)}
</article>
);
}
Обработка URL с разными слагами по языкам
// next.config.js
module.exports = {
i18n: {
locales: ["en", "ru"],
defaultLocale: "en",
},
rewrites: async () => {
return {
beforeFiles: [
// Переписываем URL /o-nas на /pages/about-us?lang=ru
{
source: "/o-nas",
destination: "/pages/about-us?lang=ru",
locale: false,
},
{
source: "/about-us",
destination: "/pages/about-us?lang=en",
locale: false,
},
],
};
},
};
// Или используем динамическую маршрутизацию
// pages/[...slug].js
import { useRouter } from "next/router";
function DynamicPage() {
const { slug } = useRouter().query;
const { locale } = useRouter();
// Определяем slug на основе текущего языка
const normalizedSlug = normalizeSlug(slug, locale);
// Запрашиваем страницу
const page = fetchPage(normalizedSlug, locale);
return <PageRenderer page={page} />;
}
function normalizeSlug(slug, locale) {
// Если пользователь на /ru/o-nas, преобразуем в about-us
const slugMap = {
ru: {
"o-nas": "about-us",
"kontakty": "contacts",
},
en: {
"about-us": "about-us",
"contacts": "contacts",
},
};
return slugMap[locale]?.[slug] || slug;
}
Кэширование переводов
// Используем Redis для кэширования часто запрашиваемых страниц
import redis from "./redis-client";
async function getPage(slug, language) {
const cacheKey = `page:${slug}:${language}`;
// Проверяем кэш
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Если нет в кэше - берём из БД
const page = await db.query(
"SELECT * FROM page_translations WHERE slug = ? AND language = ?",
[slug, language]
);
// Кэшируем на 1 час
await redis.set(cacheKey, JSON.stringify(page), "EX", 3600);
return page;
}
Админка для управления переводами
// React компонент для админки (используя React Hook Form)
import { useForm, useFieldArray } from "react-hook-form";
import { useRouter } from "next/router";
const LANGUAGES = ["en", "ru", "de"];
function PageEditForm({ pageId }) {
const { control, register, handleSubmit } = useForm({
defaultValues: async () => {
const page = await fetch(`/api/pages/${pageId}`).then(r => r.json());
return page;
},
});
const { fields: translations } = useFieldArray({
control,
name: "translations",
});
const onSubmit = async (data) => {
const response = await fetch(`/api/pages/${pageId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (response.ok) {
alert("Page updated successfully");
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{LANGUAGES.map((lang) => (
<fieldset key={lang}>
<legend>{lang.toUpperCase()}</legend>
<label>
Title ({lang})
<input
{...register(`translations.${lang}.title`)}
placeholder={`Page title in ${lang}`}
/>
</label>
<label>
Slug ({lang})
<input
{...register(`translations.${lang}.slug`)}
placeholder={`URL slug in ${lang}`}
/>
</label>
<label>
Content ({lang})
<RichTextEditor
control={control}
name={`translations.${lang}.content`}
/>
</label>
<label>
Meta Description
<textarea
{...register(`translations.${lang}.meta_description`)}
rows={2}
/>
</label>
</fieldset>
))}
<button type="submit">Save Page</button>
</form>
);
}
export default PageEditForm;
SEO-оптимизация для мультиязычности
// Генерируем hreflang ссылки для поисковиков
function generateHrefLang(slug, translations) {
return (
<>
{Object.entries(translations).map(([lang, data]) => (
<link
key={lang}
rel="alternate"
hrefLang={lang}
href={`${process.env.SITE_URL}/${lang}/${data.slug}`}
/>
))}
<link
rel="alternate"
hrefLang="x-default"
href={`${process.env.SITE_URL}/${translations.en.slug}`}
/>
</>
);
}
// Используем в Next.js Head
import Head from "next/head";
function DynamicPage({ page }) {
return (
<Head>
<title>{page.title}</title>
<meta name="description" content={page.meta_description} />
{generateHrefLang(page.slug, page.translations)}
</Head>
);
}
Лучшие практики
- Разделяй интерфейс и контент - UI переводы в next-i18next, контент в БД
- Кэшируй агрессивно - используй Redis для часто запрашиваемых страниц
- Валидируй переводы - проверяй наличие перевода на всех языках перед публикацией
- Версионируй контент - сохраняй историю изменений для отката
- SEO-friendly - используй правильные hreflang теги и уникальные URL по языкам
- Документируй - объясни админам правила создания переводов
Этот подход масштабируется от небольших сайтов до крупных мультиязычных платформ.