diff --git a/bun.lock b/bun.lock index 33695e4..2985a67 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index 790b8af..2741bc3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/images/cards/aivaz.jpg b/public/images/cards/aivaz.jpg index c0c5295..c490a49 100644 Binary files a/public/images/cards/aivaz.jpg and b/public/images/cards/aivaz.jpg differ diff --git a/public/images/cards/liferes.jpg b/public/images/cards/liferes.jpg index d2f434f..4b9db2b 100644 Binary files a/public/images/cards/liferes.jpg and b/public/images/cards/liferes.jpg differ diff --git a/public/images/cards/nks.jpg b/public/images/cards/nks.jpg index cbf428e..5da79d1 100644 Binary files a/public/images/cards/nks.jpg and b/public/images/cards/nks.jpg differ diff --git a/public/images/cards/shipyard.jpg b/public/images/cards/shipyard.jpg index 285c2f5..fe0b80f 100644 Binary files a/public/images/cards/shipyard.jpg and b/public/images/cards/shipyard.jpg differ diff --git a/public/images/cards/upside.jpg b/public/images/cards/upside.jpg index db13d61..656d0ae 100644 Binary files a/public/images/cards/upside.jpg and b/public/images/cards/upside.jpg differ diff --git a/src/App.tsx b/src/App.tsx index d4ac8e9..24e4cbd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { */} ) : ( -
-
+ <> +
+
-
-
-

- IMI Saudi Shipyard -

-

Saudi Arabia

+
+
+

+ + Upside Towers + +

+

+ + Russia, Moscow + +

+
+ +
- -
-
+ )}
diff --git a/src/components/LanguageDetector.tsx b/src/components/LanguageDetector.tsx index e7d00e0..2859ba1 100644 --- a/src/components/LanguageDetector.tsx +++ b/src/components/LanguageDetector.tsx @@ -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 ( +
+ +
+ ); + } + return <>{children}; } diff --git a/src/hooks/useLanguageDetection.ts b/src/hooks/useLanguageDetection.ts index fe8ad00..6c63ab2 100644 --- a/src/hooks/useLanguageDetection.ts +++ b/src/hooks/useLanguageDetection.ts @@ -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 }; } diff --git a/src/i18n.ts b/src/i18n.ts index 2043aec..0962e02 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -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; diff --git a/src/main.tsx b/src/main.tsx index 727db09..ec53af0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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: , - // errorElement: , - }, - { - path: "/stream/:id", - element: , - }, - { - path: "/history", - element: , - }, - { - path: "/scheduled/:sessionId", - element: , + element: ( + + + + ), + children: [ + { + path: "/", + element: , + // errorElement: , + }, + { + path: "/stream/:id", + element: , + }, + { + path: "/history", + element: , + }, + { + path: "/scheduled/:sessionId", + element: , + }, + ], }, ]); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - + );