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.

This commit is contained in:
2026-03-17 17:55:32 +05:00
parent c2fc1624a4
commit 3c0f556503
12 changed files with 115 additions and 71 deletions
-3
View File
@@ -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=="],
-1
View File
@@ -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",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 83 KiB

+38 -27
View File
@@ -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>
+11 -1
View File
@@ -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}</>;
} }
+32 -4
View File
@@ -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 };
} }
+8 -16
View File
@@ -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;
+26 -19
View File
@@ -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>
); );