Refactor language detection and localization handling. Removed i18next-browser-languagedetector dependency, integrated language detection logic directly into the application, and improved user experience with loading indicators. Updated App and routing structure to support language detection more effectively. Cleaned up package dependencies and adjusted image assets for better performance.
@@ -12,7 +12,6 @@
|
||||
"caniuse-lite": "^1.0.30001764",
|
||||
"date-fns": "^2.30.0",
|
||||
"i18next": "^23.8.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"ky": "^1.1.3",
|
||||
"peerjs": "^1.5.4",
|
||||
"react": "^18.2.0",
|
||||
@@ -409,8 +408,6 @@
|
||||
|
||||
"i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="],
|
||||
|
||||
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"caniuse-lite": "^1.0.30001764",
|
||||
"date-fns": "^2.30.0",
|
||||
"i18next": "^23.8.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"ky": "^1.1.3",
|
||||
"peerjs": "^1.5.4",
|
||||
"react": "^18.2.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 418 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 371 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 83 KiB |
@@ -21,7 +21,7 @@ import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { Bounce, ToastContainer, toast } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import InfoIcon from "./components/icons/InfoIcon";
|
||||
import { detectUserRegion, getRegionHeaders } from "./utils/api";
|
||||
import { detectUserRegion, getRegionHeaders, getUserRegion } from "./utils/api";
|
||||
import { handleApiError, isErrorResponse } from "./utils/errorHandler";
|
||||
|
||||
function App() {
|
||||
@@ -104,15 +104,18 @@ function App() {
|
||||
navigate(streamUrl);
|
||||
}, [streamUrl]);
|
||||
|
||||
// Определяем регион пользователя при первой загрузке
|
||||
// Определяем регион пользователя при первой загрузке (пропускаем API, если ?lang= уже задал регион)
|
||||
useEffect(() => {
|
||||
async function initializeRegion() {
|
||||
if (getUserRegion()) {
|
||||
setRegionDetected(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await detectUserRegion();
|
||||
setRegionDetected(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to detect user region:", error);
|
||||
// Даже при ошибке продолжаем работу с дефолтным регионом
|
||||
setRegionDetected(true);
|
||||
}
|
||||
}
|
||||
@@ -305,33 +308,41 @@ function App() {
|
||||
</div> */}
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="group relative lg:h-[508px] sm:h-[284px] h-[264px] col-span-2 bg-gray-700 bg-no-repeat bg-center bg-cover"
|
||||
style={{
|
||||
backgroundImage: `url("/images/cards/shipyard.jpg")`,
|
||||
}}
|
||||
>
|
||||
<div className="h-full transition-opacity duration-300 bg-gradient-card group-hover:opacity-90"></div>
|
||||
<>
|
||||
<div
|
||||
className="group relative lg:h-[508px] sm:h-[284px] h-[264px] lg:col-span-2 bg-gray-700 bg-no-repeat bg-center bg-cover"
|
||||
style={{
|
||||
backgroundImage: `url("/images/cards/upside.jpg")`,
|
||||
}}
|
||||
>
|
||||
<div className="h-full transition-opacity duration-300 bg-gradient-card group-hover:opacity-50"></div>
|
||||
|
||||
<div className="absolute bottom-0 p-6 space-y-6">
|
||||
<div>
|
||||
<p className="text-xl font-semibold xl:text-2xl font-gilroy">
|
||||
IMI Saudi Shipyard
|
||||
</p>
|
||||
<p className="text-xs lg:text-sm">Saudi Arabia</p>
|
||||
<div className="absolute bottom-0 p-6 space-y-6">
|
||||
<div>
|
||||
<p className="text-xl font-semibold xl:text-2xl font-gilroy">
|
||||
<Trans i18nKey={"main.cards.title4"}>
|
||||
Upside Towers
|
||||
</Trans>
|
||||
</p>
|
||||
<p className="text-xs lg:text-sm">
|
||||
<Trans i18nKey={"main.cards.city3"}>
|
||||
Russia, Moscow
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => void startStream("upsideTowersDevEn")}
|
||||
className="flex gap-0 p-2 rounded-full transition-all duration-300 bg-gradient group-hover:gap-1 group-hover:pr-4 group-hover:pl-6"
|
||||
>
|
||||
<span className="font-medium w-0 opacity-0 group-hover:w-[82px] group-hover:opacity-100 overflow-hidden transition-all duration-300">
|
||||
<Trans i18nKey={"main.cards.button"}>Run demo</Trans>
|
||||
</span>
|
||||
<ArrowRightIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => void startStream("ShipyardSaudiDev")}
|
||||
className="flex gap-0 p-2 rounded-full transition-all duration-300 bg-gradient group-hover:gap-1 group-hover:pr-4 group-hover:pl-6"
|
||||
>
|
||||
<span className="font-medium w-0 opacity-0 group-hover:w-[82px] group-hover:opacity-100 overflow-hidden transition-all duration-300">
|
||||
<Trans i18nKey={"main.cards.button"}>Запустить</Trans>
|
||||
</span>
|
||||
<ArrowRightIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { ReactNode } from "react";
|
||||
import { useLanguageDetection } from "../hooks/useLanguageDetection";
|
||||
import LoaderIcon from "./icons/LoaderIcon";
|
||||
|
||||
interface LanguageDetectorProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function LanguageDetector({ children }: LanguageDetectorProps) {
|
||||
useLanguageDetection();
|
||||
const { isReady } = useLanguageDetection();
|
||||
|
||||
if (!isReady) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center min-h-screen bg-[#14161F]">
|
||||
<LoaderIcon className="w-12 h-12 animate-spin opacity-90" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { detectUserRegion } from "../utils/api";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { detectUserRegion, setUserRegion } from "../utils/api";
|
||||
|
||||
const SUPPORTED_LANGS = ["ru", "en"] as const;
|
||||
|
||||
export function useLanguageDetection() {
|
||||
const { i18n } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const userChoseLangFromUrl = useRef(false);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function detectLanguage() {
|
||||
const langParam = searchParams.get("lang")?.toLowerCase();
|
||||
|
||||
// Приоритет 1: query-параметр ?lang=ru или ?lang=en — без запроса в API
|
||||
if (langParam && SUPPORTED_LANGS.includes(langParam as (typeof SUPPORTED_LANGS)[number])) {
|
||||
userChoseLangFromUrl.current = true;
|
||||
setUserRegion(langParam === "ru" ? "RU" : "EN");
|
||||
await i18n.changeLanguage(langParam);
|
||||
setIsReady(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Если пользователь ранее выбрал язык через URL — не переопределять при навигации
|
||||
if (userChoseLangFromUrl.current) {
|
||||
setIsReady(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Приоритет 2: определение по региону
|
||||
try {
|
||||
const countryCode = await detectUserRegion();
|
||||
|
||||
@@ -17,11 +41,15 @@ export function useLanguageDetection() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to get country code:", error);
|
||||
// Fallback to browser language detection (handled by i18next-browser-languagedetector)
|
||||
// Оставляем fallbackLng из i18n (ru)
|
||||
} finally {
|
||||
setIsReady(true);
|
||||
}
|
||||
}
|
||||
|
||||
void detectLanguage();
|
||||
}, [i18n]);
|
||||
}, [i18n, searchParams]);
|
||||
|
||||
return { isReady };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
const resources = {
|
||||
ru: {
|
||||
@@ -468,20 +467,13 @@ const resources = {
|
||||
},
|
||||
};
|
||||
|
||||
void i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: "ru",
|
||||
supportedLngs: ["ru", "en"],
|
||||
detection: {
|
||||
order: ["navigator", "htmlTag"],
|
||||
caches: [], // Отключаем кеширование в localStorage
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
void i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
fallbackLng: "ru",
|
||||
supportedLngs: ["ru", "en"],
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import { createBrowserRouter, Outlet, RouterProvider } from "react-router-dom";
|
||||
import "./index.css";
|
||||
import "./i18n";
|
||||
import App from "./App";
|
||||
@@ -12,26 +12,33 @@ import LanguageDetector from "./components/LanguageDetector";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <App />,
|
||||
// errorElement: <ErrorBoundary />,
|
||||
},
|
||||
{
|
||||
path: "/stream/:id",
|
||||
element: <StreamPage />,
|
||||
},
|
||||
{
|
||||
path: "/history",
|
||||
element: <HistoryPage />,
|
||||
},
|
||||
{
|
||||
path: "/scheduled/:sessionId",
|
||||
element: <ScheduledPage />,
|
||||
element: (
|
||||
<LanguageDetector>
|
||||
<Outlet />
|
||||
</LanguageDetector>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <App />,
|
||||
// errorElement: <ErrorBoundary />,
|
||||
},
|
||||
{
|
||||
path: "/stream/:id",
|
||||
element: <StreamPage />,
|
||||
},
|
||||
{
|
||||
path: "/history",
|
||||
element: <HistoryPage />,
|
||||
},
|
||||
{
|
||||
path: "/scheduled/:sessionId",
|
||||
element: <ScheduledPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<LanguageDetector>
|
||||
<RouterProvider router={router} />
|
||||
</LanguageDetector>
|
||||
<RouterProvider router={router} />
|
||||
);
|
||||
|
||||