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