🔹 1. Использовать объект с переводами прямо в коде
Самый простой вариант — держать переводы в объекте (JS/TS), а не в JSON.
const translations = {
en: {
hello: "Hello",
welcome: (name) => `Welcome, ${name}!`,
},
ru: {
hello: "Привет",
welcome: (name) => `Добро пожаловать, ${name}!`,
},
};
const useLang =
(lang = "en") =>
(key, ...args) =>
typeof translations[lang][key] === "function"
? translations[lang][key](...args)
: translations[lang][key];
export default function App() {
const t = useLang("ru");
return (
<div>
<h1>{t("hello")}</h1>
<p>{t("welcome", "Алексей")}</p>
</div>
);
}
📌 Плюсы: не нужен JSON, можно использовать функции для динамических фраз.
📌 Минусы: при росте проекта такой объект будет пухнуть.
🔹 2. Использовать JSX-компоненты как словарь
Можно вообще уйти от строк и использовать компоненты:
const dictionary = {
en: {
greeting: (
<>
Hello <strong>World</strong>!
</>
),
},
ru: {
greeting: (
<>
Привет, <strong>Мир</strong>!
</>
),
},
};
export function Text({ lang, id }) {
return dictionary[lang][id] || id;
}
export default function App() {
return <Text lang="ru" id="greeting" />;
}
📌 Плюсы: легко вставлять HTML/React-компоненты в переводы.
📌 Минусы: сложнее отдавать на перевод вне проекта.
🔹 3. Использовать функциональные словари
Вместо строк — сразу функции:
const dict = {
en: {
items: (n) => `${n} item${n > 1 ? "s" : ""}`,
},
ru: {
items: (n) => `${n} ${n === 1 ? "элемент" : "элемента"}`,
},
};
function App() {
const lang = "ru";
return <div>{dict[lang].items(3)}</div>;
}
📌 Плюсы: удобно для числительных и падежей.
📌 Минусы: всё равно приходится писать руками для каждого языка.
🔹 4. Использовать React Context
Можно сделать свой минималистичный “i18n движок”:
import { createContext, useContext } from "react";
const LangContext = createContext("en");
const translations = {
en: { hello: "Hello" },
ru: { hello: "Привет" },
};
export const useT = () => {
const lang = useContext(LangContext);
return (key) => translations[lang][key] || key;
};
export default function App() {
const t = useT();
return (
<LangContext.Provider value="ru">
<h1>{t("hello")}</h1>
</LangContext.Provider>
);
}
📌 Плюсы: гибкость, легко расширить.
📌 Минусы: нужно поддерживать структуру словаря.
🔹 5. Использовать AI/онлайн-переводы на лету
Если цель — вообще уйти от JSON и статических словарей, можно подключить API (например, Google Translate API или OpenAI) и переводить строки на лету. Но это дороже и может лагать.
👉 Если тебе нужен минимализм — я бы выбрал вариант №1 или №3 (объект в коде + функции).
А если масштабируемость и удобство — лучше сразу взять i18next, но хранить переводы не в JSON, а в .js/.ts
файлах.
Хочешь, я накидаю супер-минималистичный хук useI18n
для React без JSON, который можно переиспользовать?
Не-не, это не идиотизм 🙂
Идея с отдельным компонентом под каждый язык — рабочая, но у неё есть нюансы.
🔥 Почему это может быть ок
- Очень маленький проект — если у тебя лендинг на 2–3 страницы, то реально проще сделать
<HeaderRu />
,<HeaderEn />
и всё, чем тащить словари и провайдеры. - Разные тексты/верстка — иногда тексты на другом языке не просто перевод, а реально другой контент (например, в RU показываешь «Оплатить картой Мир», в EN это не нужно). В таком случае отдельные компоненты удобнее.
- Нет переводчиков — проще дать верстальщику или копирайтеру отдельный JSX с текстами, чем объяснять JSON/объекты.
🔥 Почему это боль в будущем
- Дублирование верстки — если завтра меняешь структуру блока, надо править сразу в двух/трёх компонентах.
- Сложнее масштабировать — добавишь третий язык → уже три копии кода.
- Трудно автоматизировать — нельзя выгрузить словарь для переводчиков, всё завязано на коде.
🚀 Альтернатива — хук useI18n
(без JSON, чисто в коде)
import { createContext, useContext, useState } from "react";
const translations = {
en: {
hello: "Hello",
welcome: (name) => `Welcome, ${name}!`,
},
ru: {
hello: "Привет",
welcome: (name) => `Добро пожаловать, ${name}!`,
},
};
const LangContext = createContext();
export const LangProvider = ({ children, defaultLang = "en" }) => {
const [lang, setLang] = useState(defaultLang);
return (
<LangContext.Provider value={{ lang, setLang }}>
{children}
</LangContext.Provider>
);
};
export const useI18n = () => {
const { lang } = useContext(LangContext);
return (key, ...args) => {
const value = translations[lang][key];
return typeof value === "function" ? value(...args) : value;
};
};
// --- пример использования ---
export default function App() {
const t = useI18n();
return (
<LangProvider defaultLang="ru">
<h1>{t("hello")}</h1>
<p>{t("welcome", "Алексей")}</p>
</LangProvider>
);
}
📌 Всё хранится прямо в коде, JSON не нужен.
📌 Поддерживает динамические строки (welcome(name)
).
📌 Можно легко переключать язык через setLang
.
Хочешь, я покажу, как этот хук можно подружить с роутами (например, mysite.com/ru/...
и mysite.com/en/...
), чтобы мультияз автоматически подхватывался?
🔥 Отлично, тогда можно сделать мультияз прямо через React Router.
Трюк в том, чтобы язык был частью URL (/en/...
, /ru/...
), а дальше — хук useParams
подтягивает язык и подсовывает его в LangProvider
.
⚡ Пример: мультияз через react-router-dom
import {
BrowserRouter as Router,
Routes,
Route,
useParams,
Link,
Navigate,
} from "react-router-dom";
import { createContext, useContext } from "react";
// --- словарь переводов ---
const translations = {
en: {
home: "Home",
about: "About",
welcome: (name) => `Welcome, ${name}!`,
},
ru: {
home: "Главная",
about: "О нас",
welcome: (name) => `Добро пожаловать, ${name}!`,
},
};
// --- контекст языка ---
const LangContext = createContext();
const LangProvider = ({ children }) => {
const { lang } = useParams();
return (
<LangContext.Provider value={lang || "en"}>{children}</LangContext.Provider>
);
};
// --- хук для переводов ---
const useI18n = () => {
const lang = useContext(LangContext);
return (key, ...args) => {
const value = translations[lang][key];
return typeof value === "function" ? value(...args) : value;
};
};
// --- компоненты страниц ---
function Home() {
const t = useI18n();
return (
<div>
<h1>{t("home")}</h1>
<p>{t("welcome", "Alexey")}</p>
</div>
);
}
function About() {
const t = useI18n();
return <h1>{t("about")}</h1>;
}
// --- меню переключения языков ---
function LangSwitcher() {
const { lang } = useParams();
return (
<nav>
<Link to={`/en/home`}>EN</Link> | <Link to={`/ru/home`}>RU</Link>
<p>Текущий язык: {lang}</p>
</nav>
);
}
// --- маршруты ---
export default function App() {
return (
<Router>
<Routes>
{/* редирект с корня */}
<Route path="/" element={<Navigate to="/en/home" />} />
{/* язык как параметр */}
<Route
path=":lang/*"
element={
<LangProvider>
<LangSwitcher />
<Routes>
<Route path="home" element={<Home />} />
<Route path="about" element={<About />} />
</Routes>
</LangProvider>
}
/>
</Routes>
</Router>
);
}
🚀 Как это работает
- Все пути включают язык (
/:lang/...
).
Пример:/ru/home
→ русский,/en/home
→ английский. LangProvider
беретlang
из URL и кладет вContext
.- Хук
useI18n
достает язык и возвращает перевод из объекта. - Можно легко переключать язык через ссылки (
<Link to="/ru/home">RU</Link>
).
📌 Плюсы такого подхода:
- SEO-friendly: разные URL для разных языков.
- Не нужен отдельный state для языка, он всегда берется из URL.
- Легко добавить новый язык (просто расширяешь словарь и роуты).
Хочешь, я допилю пример так, чтобы react-router
сам редиректил на правильный язык по настройкам браузера (например, если у юзера navigator.language = "ru"
, то сразу /ru/home
)?
О да, это можно 💯
i18next очень гибкий, и никто не запрещает тебе смешивать разные подходы в одном проекте. Более того, i18next сам по себе не навязывает формат хранения переводов — JSON это просто стандартный пример.
🔥 Как можно совместить твой текущий i18next с другими методами
1. Использовать JS/TS вместо JSON
i18next поддерживает не только JSON, а и «plain object». То есть ты можешь держать словари в .js/.ts
файлах, а не в JSON:
// locales/ru.js
export default {
translation: {
hello: "Привет",
welcome: (name) => `Добро пожаловать, ${name}!`,
},
};
// i18n.js
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import ru from "./locales/ru";
import en from "./locales/en";
i18n.use(initReactI18next).init({
resources: {
en: { translation: en },
ru: { translation: ru },
},
lng: "ru",
fallbackLng: "en",
});
export default i18n;
📌 Теперь у тебя переводы не в JSON, а в коде (можно хранить функции, условия, JSX).
2. Подмешать свой кастомный словарь
Например, для каких-то «нестандартных» текстов можно держать свой объект и вызывать его напрямую через хук useI18n
(типа как я показывал выше), а для остального использовать i18next.
const extraTranslations = {
ru: { special: "⚡ Специальный текст" },
en: { special: "⚡ Special text" },
};
function SpecialText() {
const { i18n } = useTranslation();
const lang = i18n.language;
return <span>{extraTranslations[lang].special}</span>;
}
3. Компоненты для уникального контента
Если у тебя есть блоки, где тексты сильно разные между языками (не просто перевод, а разные фразы/разметка), там лучше сделать по компоненту на язык. i18next легко позволяет встроить кастомную логику:
function PromoBlock() {
const { i18n } = useTranslation();
return i18n.language === "ru" ? (
<div>🎉 Акция только для России!</div>
) : (
<div>🎉 Global sale is here!</div>
);
}
4. React Router + i18next
Ты можешь объединить мою схему с /:lang/...
и i18next: при смене URL менять i18n.changeLanguage(lang)
.
import { useParams } from "react-router-dom";
import { useEffect } from "react";
import i18n from "./i18n";
function LangWatcher() {
const { lang } = useParams();
useEffect(() => {
i18n.changeLanguage(lang);
}, [lang]);
return null;
}
📌 Теперь i18next всегда будет подстраиваться под роут.
⚡ Итого
Ты можешь:
- оставить i18next для основных переводов (чтобы было масштабируемо и привычно для переводчиков);
- а для гибких кейсов использовать:
- словари прямо в коде (с функциями, JSX и условиями);
- отдельные компоненты для уникального контента;
- Router для SEO-friendly URL с мультиязом.
Хочешь, я соберу для тебя гибридный пример, где i18next работает вместе с react-router
и кастомным словарём/компонентами?
Окей, сделаем гибридный пример 🧩:
- i18next → для основной массы переводов.
- React Router → язык в URL (
/ru/...
,/en/...
). - кастомные словари/компоненты → для «особых кейсов».
⚡ Полный рабочий пример
// i18n.js
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
// словари можно хранить прямо в JS
const resources = {
en: {
translation: {
home: "Home",
about: "About",
hello: "Hello, {{name}}!",
},
},
ru: {
translation: {
home: "Главная",
about: "О нас",
hello: "Привет, {{name}}!",
},
},
};
i18n.use(initReactI18next).init({
resources,
lng: "en", // дефолт
fallbackLng: "en",
interpolation: {
escapeValue: false,
},
});
export default i18n;
// App.jsx
import "./i18n";
import {
BrowserRouter as Router,
Routes,
Route,
Navigate,
useParams,
Link,
} from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useEffect } from "react";
import i18n from "./i18n";
// --- LangWatcher: следит за URL и меняет язык в i18next
function LangWatcher() {
const { lang } = useParams();
useEffect(() => {
if (lang && i18n.language !== lang) {
i18n.changeLanguage(lang);
}
}, [lang]);
return null;
}
// --- Кастомный словарь для "особых кейсов"
const extraTranslations = {
en: { special: "⚡ Exclusive for global users" },
ru: { special: "⚡ Специальное предложение для России" },
};
function SpecialText() {
const { i18n } = useTranslation();
const lang = i18n.language;
return <p>{extraTranslations[lang].special}</p>;
}
// --- Компоненты страниц
function Home() {
const { t } = useTranslation();
return (
<div>
<h1>{t("home")}</h1>
<p>{t("hello", { name: "Alexey" })}</p>
<SpecialText />
</div>
);
}
function About() {
const { t, i18n } = useTranslation();
return (
<div>
<h1>{t("about")}</h1>
{i18n.language === "ru" ? (
<p>🇷🇺 Здесь мы рассказываем о компании по-русски.</p>
) : (
<p>🌍 Here we explain about the company in English.</p>
)}
</div>
);
}
// --- Переключатель языков
function LangSwitcher() {
const { lang } = useParams();
return (
<nav style={{ marginBottom: "20px" }}>
<Link to={`/en/home`}>EN</Link> | <Link to={`/ru/home`}>RU</Link>
<p>Current lang: {lang}</p>
</nav>
);
}
// --- Маршруты
export default function App() {
return (
<Router>
<Routes>
{/* редирект с корня */}
<Route path="/" element={<Navigate to="/en/home" />} />
{/* язык в URL */}
<Route
path=":lang/*"
element={
<>
<LangWatcher />
<LangSwitcher />
<Routes>
<Route path="home" element={<Home />} />
<Route path="about" element={<About />} />
</Routes>
</>
}
/>
</Routes>
</Router>
);
}
🚀 Что умеет этот гибрид
- i18next → стандартные переводы (
home
,about
,hello
). - extraTranslations → кастомный словарь JS для спец. текстов.
- условный JSX → для случаев, где тексты/разметка реально разные.
- React Router → язык в URL (
/ru/...
,/en/...
). - LangWatcher → автоматически синхронизирует i18next с роутером.
👉 Такой сетап хорош, если проект растёт:
- простые тексты → i18next;
- особые блоки → функции/JSX/компоненты;
- SEO и роутинг → язык в URL.
Хочешь, я могу ещё показать, как к этому сетапу прикрутить автоматический выбор языка по navigator.language
(например, если у юзера браузер на русском, его сразу кинет на /ru/home
)?
Отлично 🚀
Давай добавим автоопределение языка по navigator.language
.
Логика простая:
- если юзер заходит на
/
, мы редиректим его на/<его язык>/home
; - если язык браузера не поддержан → кидаем на дефолт (
/en/home
).
⚡ Обновлённый пример с автоопределением языка
// App.jsx
import "./i18n";
import {
BrowserRouter as Router,
Routes,
Route,
Navigate,
useParams,
Link,
useNavigate,
} from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useEffect } from "react";
import i18n from "./i18n";
// --- список поддерживаемых языков
const supportedLangs = ["en", "ru"];
// --- компонент автоопределения языка
function AutoRedirect() {
const navigate = useNavigate();
useEffect(() => {
const browserLang = navigator.language.slice(0, 2); // "ru", "en", "fr"
const lang = supportedLangs.includes(browserLang) ? browserLang : "en";
navigate(`/${lang}/home`, { replace: true });
}, [navigate]);
return null; // ничего не рендерим, только редиректим
}
// --- LangWatcher: следит за URL и меняет язык в i18next
function LangWatcher() {
const { lang } = useParams();
useEffect(() => {
if (lang && i18n.language !== lang) {
i18n.changeLanguage(lang);
}
}, [lang]);
return null;
}
// --- кастомный словарь для особых текстов
const extraTranslations = {
en: { special: "⚡ Exclusive for global users" },
ru: { special: "⚡ Специальное предложение для России" },
};
function SpecialText() {
const { i18n } = useTranslation();
const lang = i18n.language;
return <p>{extraTranslations[lang].special}</p>;
}
// --- страницы
function Home() {
const { t } = useTranslation();
return (
<div>
<h1>{t("home")}</h1>
<p>{t("hello", { name: "Alexey" })}</p>
<SpecialText />
</div>
);
}
function About() {
const { t, i18n } = useTranslation();
return (
<div>
<h1>{t("about")}</h1>
{i18n.language === "ru" ? (
<p>🇷🇺 Здесь мы рассказываем о компании по-русски.</p>
) : (
<p>🌍 Here we explain about the company in English.</p>
)}
</div>
);
}
// --- переключатель языков
function LangSwitcher() {
const { lang } = useParams();
return (
<nav style={{ marginBottom: "20px" }}>
<Link to={`/en/home`}>EN</Link> | <Link to={`/ru/home`}>RU</Link>
<p>Current lang: {lang}</p>
</nav>
);
}
// --- маршруты
export default function App() {
return (
<Router>
<Routes>
{/* автоопределение с корня */}
<Route path="/" element={<AutoRedirect />} />
{/* язык в URL */}
<Route
path=":lang/*"
element={
<>
<LangWatcher />
<LangSwitcher />
<Routes>
<Route path="home" element={<Home />} />
<Route path="about" element={<About />} />
</Routes>
</>
}
/>
</Routes>
</Router>
);
}
🚀 Как это работает
-
При заходе на
/
→ компонентAutoRedirect
берётnavigator.language
.- Например:
ru-RU
→ берёмru
. - Если поддержан (
ru
илиen
) → редиректим на/<lang>/home
. - Если нет (например,
fr
) → редиректим на/en/home
.
- Например:
-
LangWatcher
всегда синхронизируетi18next
с URL. -
Юзер может вручную переключить язык через ссылки.
👉 Таким образом, у тебя:
- SEO-дружественные URL (
/en/...
,/ru/...
); - автоопределение по браузеру;
- i18next + кастомные словари/JSX в одном проекте.
Хочешь, я ещё добавлю, чтобы язык сохранялся в localStorage
, и при следующем визите он подгружался не из navigator.language
, а из «последнего выбора» пользователя?
Окей 💡 сделаем структуру для портфолио с мультиязом, чтобы всё было гибко:
- i18next → кнопки/навигация.
- JS словари → проекты (заголовки + описания).
- условные компоненты → About (разный текст и разметка).
- React Router → язык в URL (
/ru/...
,/en/...
).
⚡ Полный пример
// i18n.js
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
const resources = {
en: {
translation: {
nav: {
home: "Home",
projects: "Projects",
about: "About",
},
},
},
ru: {
translation: {
nav: {
home: "Главная",
projects: "Проекты",
about: "Обо мне",
},
},
},
};
i18n.use(initReactI18next).init({
resources,
lng: "en",
fallbackLng: "en",
interpolation: {
escapeValue: false,
},
});
export default i18n;
// projectsData.js
export const projects = {
en: [
{
title: "Portfolio Website",
desc: "Responsive React + Tailwind portfolio site.",
},
{
title: "E-commerce Store",
desc: "Fullstack shop built with Node.js and MongoDB.",
},
],
ru: [
{
title: "Сайт-портфолио",
desc: "Адаптивный сайт на React + Tailwind.",
},
{
title: "Интернет-магазин",
desc: "Фуллстек-магазин на Node.js и MongoDB.",
},
],
};
// App.jsx
import "./i18n";
import {
BrowserRouter as Router,
Routes,
Route,
Navigate,
useParams,
Link,
useNavigate,
} from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useEffect } from "react";
import i18n from "./i18n";
import { projects } from "./projectsData";
// --- поддерживаемые языки
const supportedLangs = ["en", "ru"];
// --- автоопределение языка при заходе на "/"
function AutoRedirect() {
const navigate = useNavigate();
useEffect(() => {
const browserLang = navigator.language.slice(0, 2);
const lang = supportedLangs.includes(browserLang) ? browserLang : "en";
navigate(`/${lang}/home`, { replace: true });
}, [navigate]);
return null;
}
// --- синхронизация i18next с URL
function LangWatcher() {
const { lang } = useParams();
useEffect(() => {
if (lang && i18n.language !== lang) {
i18n.changeLanguage(lang);
}
}, [lang]);
return null;
}
// --- Навигация
function Navbar() {
const { t } = useTranslation();
const { lang } = useParams();
return (
<nav style={{ marginBottom: "20px" }}>
<Link to={`/${lang}/home`}>{t("nav.home")}</Link> |{" "}
<Link to={`/${lang}/projects`}>{t("nav.projects")}</Link> |{" "}
<Link to={`/${lang}/about`}>{t("nav.about")}</Link>
<div style={{ marginTop: "10px" }}>
🌍 <Link to="/en/home">EN</Link> | <Link to="/ru/home">RU</Link>
</div>
</nav>
);
}
// --- Главная
function Home() {
return <h1>👋 Welcome to my portfolio!</h1>;
}
// --- Проекты
function Projects() {
const { i18n } = useTranslation();
const lang = i18n.language;
return (
<div>
<h1>📂 Projects</h1>
{projects[lang].map((p) => (
<div key={p.title} style={{ marginBottom: "15px" }}>
<h3>{p.title}</h3>
<p>{p.desc}</p>
</div>
))}
</div>
);
}
// --- About (разные тексты и разметка)
function About() {
const { i18n } = useTranslation();
return i18n.language === "ru" ? (
<>
<h1>Обо мне</h1>
<p>
Я фронтенд-разработчик, увлечённый созданием удобных и быстрых
интерфейсов.
</p>
<p>Опыт в React, Node.js, и UI/UX дизайне.</p>
</>
) : (
<>
<h1>About Me</h1>
<p>
I'm a frontend developer passionate about building fast and
user-friendly interfaces.
</p>
<p>Experience with React, Node.js, and UI/UX design.</p>
</>
);
}
// --- Маршруты
export default function App() {
return (
<Router>
<Routes>
<Route path="/" element={<AutoRedirect />} />
<Route
path=":lang/*"
element={
<>
<LangWatcher />
<Navbar />
<Routes>
<Route path="home" element={<Home />} />
<Route path="projects" element={<Projects />} />
<Route path="about" element={<About />} />
</Routes>
</>
}
/>
</Routes>
</Router>
);
}
🚀 Что получилось
- Навигация и UI → через i18next.
- Проекты → отдельный JS-словарь (легко добавлять новые).
- About → разные тексты/разметка в зависимости от языка.
- Автоопределение языка по
navigator.language
. - SEO-friendly URL:
/ru/projects
,/en/about
.
👉 Хочешь, я ещё покажу, как вынести описания проектов в Markdown и подгружать их, чтобы можно было писать большие тексты «по-человечески» (как статьи)?
🔥 Отлично, тогда покажу, как перевести описания проектов в Markdown — это супер удобно для портфолио, потому что:
- редактировать тексты проще (без кавычек, без экранирования, обычный Markdown),
- можно вставлять списки, ссылки, картинки,
- переводить тексты можно держать в отдельных
.md
файлах для каждого языка.
⚡ Шаг 1. Установка зависимости
Тебе нужен пакет react-markdown
:
npm install react-markdown
⚡ Шаг 2. Создаём markdown-файлы
Например, так:
content/projects.en.md
# Portfolio Website
Responsive React + Tailwind portfolio site.
## Features
- Responsive design
- Deployed on Netlify
- Uses Tailwind CSS
content/projects.ru.md
# Сайт-портфолио
Адаптивный сайт на React + Tailwind.
## Фичи
- Адаптивная верстка
- Деплой на Netlify
- Использует Tailwind CSS
⚡ Шаг 3. Подгружаем Markdown в компонент
React позволяет импортировать .md
как текст (через Vite/Webpack).
Пример:
// ProjectDetails.jsx
import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import ReactMarkdown from "react-markdown";
// импортируем markdown-файлы
import enContent from "./content/projects.en.md?raw";
import ruContent from "./content/projects.ru.md?raw";
const contentMap = {
en: enContent,
ru: ruContent,
};
export default function ProjectDetails() {
const { i18n } = useTranslation();
const lang = i18n.language;
return (
<div className="prose">
<ReactMarkdown>{contentMap[lang]}</ReactMarkdown>
</div>
);
}
⚡ ?raw
— это Vite/webpack-лоадер, который импортирует файл как строку.
⚡ Шаг 4. Подключаем к маршрутам
// App.jsx (добавляем страницу проекта)
<Routes>
<Route path="home" element={<Home />} />
<Route path="projects" element={<Projects />} />
<Route path="projects/:id" element={<ProjectDetails />} />
<Route path="about" element={<About />} />
</Routes>
Теперь если зайти на /ru/projects/1
, то подтянется описание на русском, /en/projects/1
→ на английском.