Категории

Музльтияз в React

🔹 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>
  );
}

🚀 Как это работает

  1. Все пути включают язык (/:lang/...).
    Пример: /ru/home → русский, /en/home → английский.
  2. LangProvider берет lang из URL и кладет в Context.
  3. Хук useI18n достает язык и возвращает перевод из объекта.
  4. Можно легко переключать язык через ссылки (<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>
  );
}

🚀 Как это работает

  1. При заходе на / → компонент AutoRedirect берёт navigator.language.

    • Например: ru-RU → берём ru.
    • Если поддержан (ru или en) → редиректим на /<lang>/home.
    • Если нет (например, fr) → редиректим на /en/home.
  2. LangWatcher всегда синхронизирует i18next с URL.

  3. Юзер может вручную переключить язык через ссылки.


👉 Таким образом, у тебя:

  • 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 → на английском.