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",
|
"caniuse-lite": "^1.0.30001764",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"i18next": "^23.8.2",
|
"i18next": "^23.8.2",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
|
||||||
"ky": "^1.1.3",
|
"ky": "^1.1.3",
|
||||||
"peerjs": "^1.5.4",
|
"peerjs": "^1.5.4",
|
||||||
"react": "^18.2.0",
|
"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": ["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=="],
|
"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=="],
|
"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",
|
"caniuse-lite": "^1.0.30001764",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"i18next": "^23.8.2",
|
"i18next": "^23.8.2",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
|
||||||
"ky": "^1.1.3",
|
"ky": "^1.1.3",
|
||||||
"peerjs": "^1.5.4",
|
"peerjs": "^1.5.4",
|
||||||
"react": "^18.2.0",
|
"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 { Bounce, ToastContainer, toast } from "react-toastify";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import InfoIcon from "./components/icons/InfoIcon";
|
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";
|
import { handleApiError, isErrorResponse } from "./utils/errorHandler";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -104,15 +104,18 @@ function App() {
|
|||||||
navigate(streamUrl);
|
navigate(streamUrl);
|
||||||
}, [streamUrl]);
|
}, [streamUrl]);
|
||||||
|
|
||||||
// Определяем регион пользователя при первой загрузке
|
// Определяем регион пользователя при первой загрузке (пропускаем API, если ?lang= уже задал регион)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function initializeRegion() {
|
async function initializeRegion() {
|
||||||
|
if (getUserRegion()) {
|
||||||
|
setRegionDetected(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await detectUserRegion();
|
await detectUserRegion();
|
||||||
setRegionDetected(true);
|
setRegionDetected(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to detect user region:", error);
|
console.error("Failed to detect user region:", error);
|
||||||
// Даже при ошибке продолжаем работу с дефолтным регионом
|
|
||||||
setRegionDetected(true);
|
setRegionDetected(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,33 +308,41 @@ function App() {
|
|||||||
</div> */}
|
</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"
|
<div
|
||||||
style={{
|
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"
|
||||||
backgroundImage: `url("/images/cards/shipyard.jpg")`,
|
style={{
|
||||||
}}
|
backgroundImage: `url("/images/cards/upside.jpg")`,
|
||||||
>
|
}}
|
||||||
<div className="h-full transition-opacity duration-300 bg-gradient-card group-hover:opacity-90"></div>
|
>
|
||||||
|
<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 className="absolute bottom-0 p-6 space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xl font-semibold xl:text-2xl font-gilroy">
|
<p className="text-xl font-semibold xl:text-2xl font-gilroy">
|
||||||
IMI Saudi Shipyard
|
<Trans i18nKey={"main.cards.title4"}>
|
||||||
</p>
|
Upside Towers
|
||||||
<p className="text-xs lg:text-sm">Saudi Arabia</p>
|
</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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { useLanguageDetection } from "../hooks/useLanguageDetection";
|
import { useLanguageDetection } from "../hooks/useLanguageDetection";
|
||||||
|
import LoaderIcon from "./icons/LoaderIcon";
|
||||||
|
|
||||||
interface LanguageDetectorProps {
|
interface LanguageDetectorProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function LanguageDetector({ children }: LanguageDetectorProps) {
|
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}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,36 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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() {
|
export function useLanguageDetection() {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const userChoseLangFromUrl = useRef(false);
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function detectLanguage() {
|
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 {
|
try {
|
||||||
const countryCode = await detectUserRegion();
|
const countryCode = await detectUserRegion();
|
||||||
|
|
||||||
@@ -17,11 +41,15 @@ export function useLanguageDetection() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get country code:", 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();
|
void detectLanguage();
|
||||||
}, [i18n]);
|
}, [i18n, searchParams]);
|
||||||
|
|
||||||
|
return { isReady };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
ru: {
|
ru: {
|
||||||
@@ -468,20 +467,13 @@ const resources = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
void i18n
|
void i18n.use(initReactI18next).init({
|
||||||
.use(LanguageDetector)
|
resources,
|
||||||
.use(initReactI18next)
|
fallbackLng: "ru",
|
||||||
.init({
|
supportedLngs: ["ru", "en"],
|
||||||
resources,
|
interpolation: {
|
||||||
fallbackLng: "ru",
|
escapeValue: false,
|
||||||
supportedLngs: ["ru", "en"],
|
},
|
||||||
detection: {
|
});
|
||||||
order: ["navigator", "htmlTag"],
|
|
||||||
caches: [], // Отключаем кеширование в localStorage
|
|
||||||
},
|
|
||||||
interpolation: {
|
|
||||||
escapeValue: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// import React from "react";
|
// import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
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 "./index.css";
|
||||||
import "./i18n";
|
import "./i18n";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
@@ -12,26 +12,33 @@ import LanguageDetector from "./components/LanguageDetector";
|
|||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
element: (
|
||||||
element: <App />,
|
<LanguageDetector>
|
||||||
// errorElement: <ErrorBoundary />,
|
<Outlet />
|
||||||
},
|
</LanguageDetector>
|
||||||
{
|
),
|
||||||
path: "/stream/:id",
|
children: [
|
||||||
element: <StreamPage />,
|
{
|
||||||
},
|
path: "/",
|
||||||
{
|
element: <App />,
|
||||||
path: "/history",
|
// errorElement: <ErrorBoundary />,
|
||||||
element: <HistoryPage />,
|
},
|
||||||
},
|
{
|
||||||
{
|
path: "/stream/:id",
|
||||||
path: "/scheduled/:sessionId",
|
element: <StreamPage />,
|
||||||
element: <ScheduledPage />,
|
},
|
||||||
|
{
|
||||||
|
path: "/history",
|
||||||
|
element: <HistoryPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/scheduled/:sessionId",
|
||||||
|
element: <ScheduledPage />,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<LanguageDetector>
|
<RouterProvider router={router} />
|
||||||
<RouterProvider router={router} />
|
|
||||||
</LanguageDetector>
|
|
||||||
);
|
);
|
||||||
|
|||||||