Update i18next and improve localization by integrating language detection. Refactor components to utilize translation functions for user-facing text, enhancing multilingual support across the application.

This commit is contained in:
2025-12-09 18:30:26 +05:00
parent 5f8a71e4ea
commit 33e26c3f2c
23 changed files with 396 additions and 119 deletions
+3 -2
View File
@@ -1,5 +1,6 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 0,
"workspaces": { "workspaces": {
"": { "": {
"name": "ps2-react-client", "name": "ps2-react-client",
@@ -9,7 +10,7 @@
"ahooks": "^3.7.10", "ahooks": "^3.7.10",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"i18next": "^23.8.2", "i18next": "^23.8.2",
"i18next-browser-languagedetector": "^7.2.0", "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",
@@ -404,7 +405,7 @@
"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@7.2.2", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ=="], "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=="],
+1 -1
View File
@@ -15,7 +15,7 @@
"ahooks": "^3.7.10", "ahooks": "^3.7.10",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"i18next": "^23.8.2", "i18next": "^23.8.2",
"i18next-browser-languagedetector": "^7.2.0", "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",
+2 -15
View File
@@ -20,7 +20,6 @@ import ky from "ky";
import { useNavigate, useSearchParams } from "react-router-dom"; 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 api from "./utils/api";
import InfoIcon from "./components/icons/InfoIcon"; import InfoIcon from "./components/icons/InfoIcon";
function App() { function App() {
@@ -53,14 +52,6 @@ function App() {
}); });
} }
// async function getLang() {
// const { countryCode, error }: { countryCode: string; error: string } =
// await api.get("getCountryCode").json();
// if (!error && countryCode !== "RU") {
// i18n.changeLanguage("en");
// }
// }
async function startStream(build: string) { async function startStream(build: string) {
// const { countryCode, error }: { countryCode: string; error: string } = // const { countryCode, error }: { countryCode: string; error: string } =
@@ -108,7 +99,7 @@ function App() {
toastError(response.error); toastError(response.error);
// setLoading(false); // setLoading(false);
} else { } else {
toastError("Неизвестная ошибка"); toastError(t("errors.unknownError"));
// setLoading(false); // setLoading(false);
} }
} catch (error) { } catch (error) {
@@ -131,8 +122,6 @@ function App() {
}, [streamUrl]); }, [streamUrl]);
useEffect(() => { useEffect(() => {
// getLang();
if (build) { if (build) {
void startStream(build); void startStream(build);
} }
@@ -146,9 +135,7 @@ function App() {
<> <>
<div className="min-h-screen bg-[#14161F] text-white overflow-hidden"> <div className="min-h-screen bg-[#14161F] text-white overflow-hidden">
<div className="container mx-auto 2xl:px-10 lg:px-8 sm:px-6 px-4 max-w-[1600px]"> <div className="container mx-auto 2xl:px-10 lg:px-8 sm:px-6 px-4 max-w-[1600px]">
<Header <Header />
// handleChangeLang={(lang) => void i18n.changeLanguage(lang)}
/>
<div className="2xl:mt-[72px] lg:mt-16 sm:mt-[88px] mt-14 relative"> <div className="2xl:mt-[72px] lg:mt-16 sm:mt-[88px] mt-14 relative">
<div className="flex absolute -top-8 justify-center items-center w-full blur-sm"> <div className="flex absolute -top-8 justify-center items-center w-full blur-sm">
+17 -7
View File
@@ -2,6 +2,7 @@
import ky from "ky"; import ky from "ky";
import useAuthStore from "./stores/useAuthStore"; import useAuthStore from "./stores/useAuthStore";
import { FormEvent, useRef, useState } from "react"; import { FormEvent, useRef, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
type User = { type User = {
id: string; id: string;
@@ -15,6 +16,7 @@ interface IResult {
} }
function PersonalAreaLoginPage() { function PersonalAreaLoginPage() {
const { t } = useTranslation();
const [setAccessToken, setUser] = useAuthStore((state) => [ const [setAccessToken, setUser] = useAuthStore((state) => [
state.setAccessToken, state.setAccessToken,
state.setUser, state.setUser,
@@ -45,12 +47,12 @@ function PersonalAreaLoginPage() {
passwordRef.current?.focus(); passwordRef.current?.focus();
setPassword(""); setPassword("");
setError("Неверное имя пользователя или пароль"); setError(t("errors.invalidCredentials"));
return; return;
} }
if (!result.accessToken || !result.user) { if (!result.accessToken || !result.user) {
setError("Не удалось получить данные"); setError(t("errors.failedToFetchData"));
return; return;
} }
@@ -61,7 +63,7 @@ function PersonalAreaLoginPage() {
if (error instanceof Error) { if (error instanceof Error) {
if (error.message === "Failed to fetch") { if (error.message === "Failed to fetch") {
setError("Нет соединения с сервером, попробуйте позже"); setError(t("errors.noConnection"));
} else { } else {
setError(error.message); setError(error.message);
} }
@@ -72,11 +74,15 @@ function PersonalAreaLoginPage() {
return ( return (
<div className="p-8 min-h-screen flex flex-col justify-center items-center text-[#F2F2F2]"> <div className="p-8 min-h-screen flex flex-col justify-center items-center text-[#F2F2F2]">
<div className="space-y-12 w-[400px] bg-[#151619] p-8 rounded-lg shadow"> <div className="space-y-12 w-[400px] bg-[#151619] p-8 rounded-lg shadow">
<p className="text-2xl font-gilroy">Вход в личный кабинет</p> <p className="text-2xl font-gilroy">
<Trans i18nKey={"login.title"}>Вход в личный кабинет</Trans>
</p>
<form onSubmit={auth} className="flex flex-col gap-12"> <form onSubmit={auth} className="flex flex-col gap-12">
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-[#C5C7CE] text-sm">Имя пользователя</p> <p className="text-[#C5C7CE] text-sm">
<Trans i18nKey={"login.username"}>Имя пользователя</Trans>
</p>
<input <input
ref={usernameRef} ref={usernameRef}
required required
@@ -87,7 +93,9 @@ function PersonalAreaLoginPage() {
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-[#C5C7CE] text-sm">Пароль</p> <p className="text-[#C5C7CE] text-sm">
<Trans i18nKey={"login.password"}>Пароль</Trans>
</p>
<input <input
ref={passwordRef} ref={passwordRef}
required required
@@ -126,7 +134,9 @@ function PersonalAreaLoginPage() {
></path> ></path>
</svg> </svg>
) : ( ) : (
<span>Войти</span> <span>
<Trans i18nKey={"login.loginButton"}>Войти</Trans>
</span>
)} )}
</button> </button>
</form> </form>
+9 -8
View File
@@ -17,7 +17,7 @@ import { useEffect, useState } from "react";
import ChevronRightIcon from "./icons/ChevronRightIcon"; import ChevronRightIcon from "./icons/ChevronRightIcon";
import ChevronLeftIcon from "./icons/ChevronLeftIcon"; import ChevronLeftIcon from "./icons/ChevronLeftIcon";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import i18n from "../i18n"; import { useTranslation } from "react-i18next";
interface CalendarProps { interface CalendarProps {
schedules: any[]; schedules: any[];
@@ -30,6 +30,7 @@ function classNames(...classes: (string | boolean)[]) {
} }
function Calendar({ schedules, handleSelect, className }: CalendarProps) { function Calendar({ schedules, handleSelect, className }: CalendarProps) {
const { t, i18n } = useTranslation();
const today = startOfToday(); const today = startOfToday();
const [selectedDay, setSelectedDay] = useState<Date | null>(null); const [selectedDay, setSelectedDay] = useState<Date | null>(null);
const [currentMonth, setCurrentMonth] = useState(format(today, "MMM-yyyy")); const [currentMonth, setCurrentMonth] = useState(format(today, "MMM-yyyy"));
@@ -76,13 +77,13 @@ function Calendar({ schedules, handleSelect, className }: CalendarProps) {
</button> </button>
</div> </div>
<div className="sm:mt-8 mt-5 grid grid-cols-7 gap-2 font-gilroy text-sm text-center text-white font-semibold"> <div className="sm:mt-8 mt-5 grid grid-cols-7 gap-2 font-gilroy text-sm text-center text-white font-semibold">
<div>{i18n.language === "ru" ? "пн" : "Mo"}</div> <div>{t("calendar.mon")}</div>
<div>{i18n.language === "ru" ? "вт" : "Tu"}</div> <div>{t("calendar.tue")}</div>
<div>{i18n.language === "ru" ? "ср" : "We"}</div> <div>{t("calendar.wed")}</div>
<div>{i18n.language === "ru" ? "чт" : "Th"}</div> <div>{t("calendar.thu")}</div>
<div>{i18n.language === "ru" ? "пт" : "Fr"}</div> <div>{t("calendar.fri")}</div>
<div>{i18n.language === "ru" ? "сб" : "Sa"}</div> <div>{t("calendar.sat")}</div>
<div>{i18n.language === "ru" ? "вс" : "Su"}</div> <div>{t("calendar.sun")}</div>
</div> </div>
<div className="grid grid-cols-7 gap-2 mt-2 text-sm font-semibold"> <div className="grid grid-cols-7 gap-2 mt-2 text-sm font-semibold">
{days.map((day, dayIdx) => ( {days.map((day, dayIdx) => (
+6 -2
View File
@@ -7,6 +7,7 @@ import "./Chat.css";
import ChevronDownIcon from "./icons/ChevronDownIcon"; import ChevronDownIcon from "./icons/ChevronDownIcon";
import CloseIcon from "./icons/CloseIcon"; import CloseIcon from "./icons/CloseIcon";
import useChatStore from "../stores/useChatStore"; import useChatStore from "../stores/useChatStore";
import { Trans, useTranslation } from "react-i18next";
interface ChatProps { interface ChatProps {
className?: string; className?: string;
@@ -14,6 +15,7 @@ interface ChatProps {
} }
function Chat({ className, handleClose }: ChatProps) { function Chat({ className, handleClose }: ChatProps) {
const { t } = useTranslation();
const socket = useSocketStore((state) => state.socket); const socket = useSocketStore((state) => state.socket);
const [message, setMessage] = useState<string>(""); const [message, setMessage] = useState<string>("");
const [messages, setMessages] = useChatStore((state) => [ const [messages, setMessages] = useChatStore((state) => [
@@ -94,7 +96,9 @@ function Chat({ className, handleClose }: ChatProps) {
].join(" ")} ].join(" ")}
> >
<div className="border-b pb-3 border-y-neutral-800 flex justify-between"> <div className="border-b pb-3 border-y-neutral-800 flex justify-between">
<p className="text-2xl font-gilroy">Чат</p> <p className="text-2xl font-gilroy">
<Trans i18nKey={"chat.title"}>Чат</Trans>
</p>
<button onClick={handleClose}> <button onClick={handleClose}>
<CloseIcon /> <CloseIcon />
</button> </button>
@@ -137,7 +141,7 @@ function Chat({ className, handleClose }: ChatProps) {
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"
placeholder="Написать сообщение..." placeholder={t("chat.placeholder")}
autoFocus autoFocus
autoComplete="off" autoComplete="off"
value={message} value={message}
+1 -1
View File
@@ -50,7 +50,7 @@ function Chat2({ onClose }: Props) {
<div className="p-4 pb-2"> <div className="p-4 pb-2">
<div className="flex items-center justify-between border-b border-[#DAE0E5] pb-4"> <div className="flex items-center justify-between border-b border-[#DAE0E5] pb-4">
<p className="text-sm font-semibold"> <p className="text-sm font-semibold">
<Trans i18nKey={"chat"}>Чат демонстрации</Trans> <Trans i18nKey={"chat.demoChat"}>Чат демонстрации</Trans>
</p> </p>
<Button <Button
variant="tertiary" variant="tertiary"
+6 -2
View File
@@ -5,6 +5,7 @@ import SubtracktIcon from "./icons/SubtracktIcon";
import Button from "./ui/Button"; import Button from "./ui/Button";
import { Socket } from "socket.io-client"; import { Socket } from "socket.io-client";
import { format } from "date-fns"; import { format } from "date-fns";
import { Trans, useTranslation } from "react-i18next";
interface User { interface User {
id: string; id: string;
@@ -27,6 +28,7 @@ interface ChatNewProps {
} }
function ChatNew({ isShow, socket, userId, name, onClose }: ChatNewProps) { function ChatNew({ isShow, socket, userId, name, onClose }: ChatNewProps) {
const { t } = useTranslation();
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [message, setMessage] = useState<string>(""); const [message, setMessage] = useState<string>("");
const messagesRef = useRef<HTMLDivElement>(null); const messagesRef = useRef<HTMLDivElement>(null);
@@ -86,7 +88,9 @@ function ChatNew({ isShow, socket, userId, name, onClose }: ChatNewProps) {
}`} }`}
> >
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<p className="font-semibold text-sm">Чат демонстрации</p> <p className="font-semibold text-sm">
<Trans i18nKey={"chat.demoChat"}>Чат демонстрации</Trans>
</p>
<Button <Button
variant="tertiary" variant="tertiary"
icon={<CloseIcon />} icon={<CloseIcon />}
@@ -168,7 +172,7 @@ function ChatNew({ isShow, socket, userId, name, onClose }: ChatNewProps) {
<input <input
ref={textRef} ref={textRef}
type="text" type="text"
placeholder="Написать сообщение" placeholder={t("chat.placeholder").replace("...", "")}
value={message} value={message}
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
className="bg-white outline-none w-full" className="bg-white outline-none w-full"
+10 -4
View File
@@ -164,18 +164,24 @@ function FeedbackForm() {
{isSend && ( {isSend && (
<div className="absolute top-0 left-0 w-full h-full bg-[#14161F] border border-[#3D425C] p-6 flex flex-col justify-between"> <div className="absolute top-0 left-0 w-full h-full bg-[#14161F] border border-[#3D425C] p-6 flex flex-col justify-between">
<p className="text-gradient text-xl font-gilroy leading-tight font-semibold flex items-center gap-2"> <p className="text-gradient text-xl font-gilroy leading-tight font-semibold flex items-center gap-2">
<span>Заявка отправлена</span> <span>
<Trans i18nKey={"feedbackSuccess.title"}>Заявка отправлена</Trans>
</span>
<CheckGradientIcon className="lg:w-8 lg:h-8 w-6 h-6" /> <CheckGradientIcon className="lg:w-8 lg:h-8 w-6 h-6" />
</p> </p>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<p className="font-gilroy leading-snug lg:text-2xl text-xl font-semibold"> <p className="font-gilroy leading-snug lg:text-2xl text-xl font-semibold">
Спасибо за подачу заявки! <Trans i18nKey={"feedbackSuccess.thankYou"}>
Спасибо за подачу заявки!
</Trans>
</p> </p>
<p className="lg:w-1/2 sm:w-2/3 lg:text-base text-sm"> <p className="lg:w-1/2 sm:w-2/3 lg:text-base text-sm">
Мы ценим ваш интерес к нашей компании и в ближайшее время свяжемся <Trans i18nKey={"feedbackSuccess.message"}>
с вами для уточнения деталей проекта. Мы ценим ваш интерес к нашей компании и в ближайшее время
свяжемся с вами для уточнения деталей проекта.
</Trans>
</p> </p>
</div> </div>
</div> </div>
+12 -13
View File
@@ -1,14 +1,13 @@
import LogoIcon from "./icons/LogoIcon"; import LogoIcon from "./icons/LogoIcon";
import LogoMobileIcon from "./icons/LogoMobileIcon"; import LogoMobileIcon from "./icons/LogoMobileIcon";
// import i18n from "../i18n"; import { useTranslation } from "react-i18next";
// import useSidebarStore from "../stores/useSidebarStore";
// interface HeaderProps {
// handleChangeLang: (lang: string) => void;
// }
function Header() { function Header() {
// const [setIsOpen] = useSidebarStore((state) => [state.setIsOpen]); const { i18n } = useTranslation();
const handleChangeLang = (lang: string) => {
void i18n.changeLanguage(lang);
};
return ( return (
<header className="sm:py-6 py-4 flex justify-between"> <header className="sm:py-6 py-4 flex justify-between">
@@ -18,14 +17,14 @@ function Header() {
<a href="/" className="sm:hidden block"> <a href="/" className="sm:hidden block">
<LogoMobileIcon /> <LogoMobileIcon />
</a> </a>
{/* <div className="flex sm:gap-8 gap-2"> <div className="flex sm:gap-8 gap-2">
<div className="flex gap-1"> <div className="flex gap-1">
<button <button
className={[ className={[
"px-3 py-1.5 border rounded-full", "px-3 py-1.5 border rounded-full transition-colors",
i18n.language === "ru" i18n.language === "ru"
? "border-[#D375FF]" ? "border-[#D375FF]"
: "border-transparent hover:bg-[#3D425C] transition-colors", : "border-transparent hover:bg-[#3D425C]",
].join(" ")} ].join(" ")}
onClick={() => handleChangeLang("ru")} onClick={() => handleChangeLang("ru")}
> >
@@ -33,17 +32,17 @@ function Header() {
</button> </button>
<button <button
className={[ className={[
"px-3 py-1.5 border rounded-full", "px-3 py-1.5 border rounded-full transition-colors",
i18n.language === "en" i18n.language === "en"
? "border-[#D375FF]" ? "border-[#D375FF]"
: "border-transparent hover:bg-[#3D425C] transition-colors", : "border-transparent hover:bg-[#3D425C]",
].join(" ")} ].join(" ")}
onClick={() => handleChangeLang("en")} onClick={() => handleChangeLang("en")}
> >
EN EN
</button> </button>
</div> </div>
</div> */} </div>
</header> </header>
); );
} }
+19 -9
View File
@@ -9,8 +9,10 @@ import api from "../utils/api";
import { ToastContainer, toast } from "react-toastify"; import { ToastContainer, toast } from "react-toastify";
import MailIcon from "./icons/MailIcon"; import MailIcon from "./icons/MailIcon";
import QRCode from "react-qr-code"; import QRCode from "react-qr-code";
import { Trans, useTranslation } from "react-i18next";
function InviteModal() { function InviteModal() {
const { t } = useTranslation();
const { setModal } = useModalStore(); const { setModal } = useModalStore();
const clipboard = useClipboard(); const clipboard = useClipboard();
const [email, setEmail] = useState<string>(""); const [email, setEmail] = useState<string>("");
@@ -26,7 +28,7 @@ function InviteModal() {
.json(); .json();
console.log(result); console.log(result);
toast.success("Приглашение отправлено", { toast.success(t("toasts.invitationSent"), {
position: "top-center", position: "top-center",
autoClose: 2000, autoClose: 2000,
hideProgressBar: true, hideProgressBar: true,
@@ -48,7 +50,7 @@ function InviteModal() {
function handleClickClipboard() { function handleClickClipboard() {
clipboard.copy(); clipboard.copy();
toast.success("Ссылка скопирована в буфер обмена", { toast.success(t("toasts.linkCopied"), {
position: "top-center", position: "top-center",
autoClose: 2000, autoClose: 2000,
hideProgressBar: true, hideProgressBar: true,
@@ -64,7 +66,9 @@ function InviteModal() {
return ( return (
<div className="w-[400px] bg-white rounded-lg"> <div className="w-[400px] bg-white rounded-lg">
<div className="flex justify-between items-center pl-6 pr-2 py-2 border-b border-[#DAE0E5]"> <div className="flex justify-between items-center pl-6 pr-2 py-2 border-b border-[#DAE0E5]">
<p className="text-sm font-semibold">Пригласить</p> <p className="text-sm font-semibold">
<Trans i18nKey={"invite"}>Пригласить</Trans>
</p>
<Button <Button
variant="tertiary" variant="tertiary"
icon={<CloseIcon />} icon={<CloseIcon />}
@@ -76,10 +80,12 @@ function InviteModal() {
<div className="px-6 py-4 border-b border-[#DAE0E5]"> <div className="px-6 py-4 border-b border-[#DAE0E5]">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="font-semibold text-center"> <p className="font-semibold text-center">
Отсканируйте QR-код, <Trans i18nKey={"scanQRCode"}>
<br /> Отсканируйте QR-код,
чтобы присоедениться <br />
<br />к демонстрации чтобы присоедениться
<br />к демонстрации
</Trans>
</p> </p>
<QRCode size={128} value={link} /> <QRCode size={128} value={link} />
</div> </div>
@@ -95,7 +101,9 @@ function InviteModal() {
onChange={handleChangeEmail} onChange={handleChangeEmail}
/> />
<Button type="submit" large> <Button type="submit" large>
<p className="text-xs">Пригласить</p> <p className="text-xs">
<Trans i18nKey={"invite"}>Пригласить</Trans>
</p>
</Button> </Button>
</form> </form>
</div> </div>
@@ -110,7 +118,9 @@ function InviteModal() {
onClick={handleClickClipboard} onClick={handleClickClipboard}
> >
<LinkIcon /> <LinkIcon />
<span className="text-xs">Скопировать ссылку</span> <span className="text-xs">
<Trans i18nKey={"copyLinkToConnect"}>Скопировать ссылку</Trans>
</span>
</button> </button>
</div> </div>
+14
View File
@@ -0,0 +1,14 @@
import { ReactNode } from "react";
import { useLanguageDetection } from "../hooks/useLanguageDetection";
interface LanguageDetectorProps {
children: ReactNode;
}
function LanguageDetector({ children }: LanguageDetectorProps) {
useLanguageDetection();
return <>{children}</>;
}
export default LanguageDetector;
+13 -7
View File
@@ -1,5 +1,5 @@
/* eslint-disable no-irregular-whitespace */ /* eslint-disable no-irregular-whitespace */
import { Trans } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import useSidebarTabStore from "../stores/useSidebarStore"; import useSidebarTabStore from "../stores/useSidebarStore";
import ArrowRightIcon from "./icons/ArrowRightIcon"; import ArrowRightIcon from "./icons/ArrowRightIcon";
import MailGradientIcon from "./icons/MailGradientIcon"; import MailGradientIcon from "./icons/MailGradientIcon";
@@ -10,12 +10,13 @@ import { Bounce, ToastContainer, toast } from "react-toastify";
import InfoIcon from "./icons/InfoBlueIcon"; import InfoIcon from "./icons/InfoBlueIcon";
function SidebarTab5() { function SidebarTab5() {
const { t } = useTranslation();
const { setIsOpen, name, url } = useSidebarTabStore(); const { setIsOpen, name, url } = useSidebarTabStore();
const [, copyToClipboard] = useCopyToClipboard(); const [, copyToClipboard] = useCopyToClipboard();
function handleClickCopy() { function handleClickCopy() {
copyToClipboard(url); copyToClipboard(url);
toast.info("Ссылка скопирована в буфер обмена!", { toast.info(t("toasts.linkCopiedExclamation"), {
icon: <InfoIcon />, icon: <InfoIcon />,
position: "top-center", position: "top-center",
autoClose: 5000, autoClose: 5000,
@@ -53,23 +54,28 @@ function SidebarTab5() {
</p> </p>
<div className="sm:mt-6 mt-4 text-sm"> <div className="sm:mt-6 mt-4 text-sm">
<p className="font-semibold mb-4">Ссылка для подключения</p> <p className="font-semibold mb-4">
<Trans i18nKey={"sidebarTab5.linkLabel"}>
Ссылка для подключения
</Trans>
</p>
<input <input
type="text" type="text"
readOnly readOnly
value={url} value={url}
className="p-4 border border-[#3D425C] bg-transparent outline-none w-full mb-2" className="p-4 border border-[#3D425C] bg-transparent outline-none w-full mb-2"
onClick={() => console.log(1)}
/> />
<p className="text-xs text-[#52587A] mb-4"> <p className="text-xs text-[#52587A] mb-4">
Ссылка, получаемая пользователем на почтовый адрес, для подключения <Trans i18nKey={"sidebarTab5.linkDescription"}>
к демонстрации. Ссылка, получаемая пользователем на почтовый адрес, для
подключения к демонстрации.
</Trans>
</p> </p>
<button <button
className="px-4 py-3.5 border border-[#3D425C] bg-transparent outline-none w-full rounded-full bg-[#3D425C] bg-opacity-20 hover:bg-[#52587A] hover:bg-opacity-20 transition-colors" className="px-4 py-3.5 border border-[#3D425C] bg-transparent outline-none w-full rounded-full bg-[#3D425C] bg-opacity-20 hover:bg-[#52587A] hover:bg-opacity-20 transition-colors"
onClick={handleClickCopy} onClick={handleClickCopy}
> >
Скопировать <Trans i18nKey={"sidebarTab5.copyButton"}>Скопировать</Trans>
</button> </button>
</div> </div>
+5 -3
View File
@@ -13,6 +13,7 @@ import SpinnerIcon from "./icons/SpinnerIcon";
import InternetSpeedHighIcon from "./icons/InternetSpeedHighIcon"; import InternetSpeedHighIcon from "./icons/InternetSpeedHighIcon";
import InternetSpeedMediumIcon from "./icons/InternetSpeedMediumIcon"; import InternetSpeedMediumIcon from "./icons/InternetSpeedMediumIcon";
import InternetSpeedLowIcon from "./icons/InternetSpeedLowIcon"; import InternetSpeedLowIcon from "./icons/InternetSpeedLowIcon";
import { useTranslation } from "react-i18next";
interface Props { interface Props {
me: IUser; me: IUser;
@@ -22,6 +23,7 @@ interface Props {
} }
function User({ me, user, handleTransferControl, handleKick }: Props) { function User({ me, user, handleTransferControl, handleKick }: Props) {
const { t } = useTranslation();
const [showMore, setShowMore] = useState(false); const [showMore, setShowMore] = useState(false);
const ref = useClickAway<HTMLDivElement>(() => { const ref = useClickAway<HTMLDivElement>(() => {
@@ -65,7 +67,7 @@ function User({ me, user, handleTransferControl, handleKick }: Props) {
onlyIcon onlyIcon
onClick={() => setShowMore(true)} onClick={() => setShowMore(true)}
/> />
<Tooltip text={"Действия"} /> <Tooltip text={t("tooltips.actions")} />
{showMore && ( {showMore && (
<div className="absolute py-2 mt-4 space-y-1 -translate-x-[calc(100%-32px)] bg-white rounded-lg shadow w-60 z-10"> <div className="absolute py-2 mt-4 space-y-1 -translate-x-[calc(100%-32px)] bg-white rounded-lg shadow w-60 z-10">
@@ -85,7 +87,7 @@ function User({ me, user, handleTransferControl, handleKick }: Props) {
<span className="text-[#77828C]"> <span className="text-[#77828C]">
<HandOnIcon /> <HandOnIcon />
</span> </span>
<span>Передать управление</span> <span>{t("userActions.transferControl")}</span>
</button> </button>
<button <button
className="flex items-center w-full gap-2 px-4 py-1 text-sm hover:bg-[#E6ECF2] transition-colors" className="flex items-center w-full gap-2 px-4 py-1 text-sm hover:bg-[#E6ECF2] transition-colors"
@@ -97,7 +99,7 @@ function User({ me, user, handleTransferControl, handleKick }: Props) {
<span className="text-[#EB5757]"> <span className="text-[#EB5757]">
<CloseIcon /> <CloseIcon />
</span> </span>
<span>Исключить</span> <span>{t("userActions.kick")}</span>
</button> </button>
</div> </div>
)} )}
+1 -1
View File
@@ -21,7 +21,7 @@ function Users({ onClose, transferControl, kick }: Props) {
<div className="p-4 pb-2"> <div className="p-4 pb-2">
<div className="flex items-center justify-between border-b border-[#DAE0E5] pb-4"> <div className="flex items-center justify-between border-b border-[#DAE0E5] pb-4">
<p className="text-sm font-semibold"> <p className="text-sm font-semibold">
<Trans i18nKey={"chat"}>Участники</Trans> <Trans i18nKey={"members"}>Участники</Trans>
</p> </p>
<Button <Button
variant="tertiary" variant="tertiary"
+3 -2
View File
@@ -5,9 +5,10 @@ import "react-toastify/dist/ReactToastify.css";
import useModalStore from "../../stores/useModalStore"; import useModalStore from "../../stores/useModalStore";
import CloseIcon from "../icons/CloseIcon"; import CloseIcon from "../icons/CloseIcon";
import AlertIcon from "../icons/AlertIcon"; import AlertIcon from "../icons/AlertIcon";
import { Trans } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
function ShareModal() { function ShareModal() {
const { t } = useTranslation();
const [setModal] = useModalStore((state) => [state.setModal]); const [setModal] = useModalStore((state) => [state.setModal]);
const clipboard = useClipboard(); const clipboard = useClipboard();
@@ -27,7 +28,7 @@ function ShareModal() {
function handleClickCopy() { function handleClickCopy() {
clipboard.copy(); clipboard.copy();
toastInfo("Ссылка скопирована в буфер обмена"); toastInfo(t("toasts.linkCopied"));
setModal(null); setModal(null);
} }
+3 -2
View File
@@ -1,4 +1,4 @@
import { Trans } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import QRCode from "react-qr-code"; import QRCode from "react-qr-code";
import CloseIcon from "../../icons/CloseIcon"; import CloseIcon from "../../icons/CloseIcon";
import LinkIcon from "../../icons/LinkIcon"; import LinkIcon from "../../icons/LinkIcon";
@@ -9,6 +9,7 @@ import { toast, Bounce, ToastContainer } from "react-toastify";
import InfoIcon from "../../icons/InfoBlueIcon"; import InfoIcon from "../../icons/InfoBlueIcon";
function InviteModal() { function InviteModal() {
const { t } = useTranslation();
const { setModal } = useModalStore(); const { setModal } = useModalStore();
const clipboard = useClipboard(); const clipboard = useClipboard();
const link = window.location.origin + window.location.pathname; const link = window.location.origin + window.location.pathname;
@@ -16,7 +17,7 @@ function InviteModal() {
function handleClickClipboard() { function handleClickClipboard() {
clipboard.copy(); clipboard.copy();
toast.info("Ссылка скопирована в буфер обмена", { toast.info(t("toasts.linkCopied"), {
icon: <InfoIcon className="text-blue-500" />, icon: <InfoIcon className="text-blue-500" />,
position: "top-center", position: "top-center",
autoClose: 3000, autoClose: 3000,
+17 -6
View File
@@ -3,6 +3,7 @@ import Input from "../../ui/Input";
import useStreamStore from "../../../stores/useStreamStore"; import useStreamStore from "../../../stores/useStreamStore";
import Button from "../../ui/Button"; import Button from "../../ui/Button";
import useModalStore from "../../../stores/useModalStore"; import useModalStore from "../../../stores/useModalStore";
import { Trans } from "react-i18next";
interface Props { interface Props {
onAction: () => void; onAction: () => void;
@@ -31,16 +32,26 @@ function SetNameModal({ onAction }: Props) {
return ( return (
<div className="flex items-center justify-center w-full h-full bg-opacity-50 backdrop-blur-2xl"> <div className="flex items-center justify-center w-full h-full bg-opacity-50 backdrop-blur-2xl">
<div className="bg-white sm:p-12 p-6 sm:rounded-lg space-y-6 sm:h-auto h-full sm:w-auto w-full flex flex-col max-sm:justify-center"> <div className="bg-white sm:p-12 p-6 sm:rounded-lg space-y-6 sm:h-auto h-full sm:w-auto w-full flex flex-col max-sm:justify-center">
<p className="text-2xl font-semibold">Здравствуйте!</p> <p className="text-2xl font-semibold">
<Trans i18nKey={"setName.hello"}>Здравствуйте!</Trans>
</p>
<div className="space-y-2"> <div className="space-y-2">
<p className="font-semibold">Представьтесь, пожалуйста</p> <p className="font-semibold">
<Trans i18nKey={"setName.introduceYourself"}>
Представьтесь, пожалуйста
</Trans>
</p>
<p className="text-sm text-[#77828C]"> <p className="text-sm text-[#77828C]">
Так мы будем знать, как к вам обратиться <Trans i18nKey={"setName.howToAddress"}>
Так мы будем знать, как к вам обратиться
</Trans>
</p> </p>
</div> </div>
<form onSubmit={handleSubmit} className="space-y-10"> <form onSubmit={handleSubmit} className="space-y-10">
<div className="space-y-1 text-xs"> <div className="space-y-1 text-xs">
<p className="text-[#77828C]">Имя</p> <p className="text-[#77828C]">
<Trans i18nKey={"setName.name"}>Имя</Trans>
</p>
<Input <Input
value={name} value={name}
onChange={handleChangeName} onChange={handleChangeName}
@@ -57,10 +68,10 @@ function SetNameModal({ onAction }: Props) {
onClick={handleClickNoName} onClick={handleClickNoName}
className="max-sm:w-full" className="max-sm:w-full"
> >
Не указывать <Trans i18nKey={"setName.skip"}>Не указывать</Trans>
</Button> </Button>
<Button type="submit" large className="max-sm:w-full"> <Button type="submit" large className="max-sm:w-full">
Продолжить <Trans i18nKey={"setName.continue"}>Продолжить</Trans>
</Button> </Button>
</div> </div>
</form> </form>
@@ -4,12 +4,14 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useSpeedtest } from "../../../hooks/useSpeedtest"; import { useSpeedtest } from "../../../hooks/useSpeedtest";
import { useCountdown } from "usehooks-ts"; import { useCountdown } from "usehooks-ts";
import { Trans, useTranslation } from "react-i18next";
interface Props { interface Props {
onSuccess: (downloadSpeed: number) => void; onSuccess: (downloadSpeed: number) => void;
} }
function SpeedtestModal({ onSuccess }: Props) { function SpeedtestModal({ onSuccess }: Props) {
const { t } = useTranslation();
const [downloadSpeed] = useSpeedtest(); const [downloadSpeed] = useSpeedtest();
const downloadSpeedRef = useRef<number>(); const downloadSpeedRef = useRef<number>();
downloadSpeedRef.current = downloadSpeed; downloadSpeedRef.current = downloadSpeed;
@@ -30,18 +32,26 @@ function SpeedtestModal({ onSuccess }: Props) {
<div className="p-12 bg-white rounded-lg w-[494px] shadow"> <div className="p-12 bg-white rounded-lg w-[494px] shadow">
<div className="space-y-4 pb-6 border-b border-[#DAE0E5]"> <div className="space-y-4 pb-6 border-b border-[#DAE0E5]">
<p className="text-2xl font-semibold leading-[24px]"> <p className="text-2xl font-semibold leading-[24px]">
Пожалуйста, подождите <Trans i18nKey={"speedtest.pleaseWait"}>
Пожалуйста, подождите
</Trans>
</p> </p>
<p className="text-sm text-[#77828C]"> <p className="text-sm text-[#77828C]">
Проверяем качество вашего <Trans i18nKey={"speedtest.checkingConnection"}>
<br /> Проверяем качество вашего
интернет-соединения <br />
интернет-соединения
</Trans>
</p> </p>
</div> </div>
<div className="pt-6 space-y-3"> <div className="pt-6 space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p>Проверка</p> <p>
<p className="text-[#49A1F5]">Осталось {counter} секунд</p> <Trans i18nKey={"speedtest.checking"}>Проверка</Trans>
</p>
<p className="text-[#49A1F5]">
{t("speedtest.secondsLeft", { count: counter })}
</p>
</div> </div>
<div className="h-1.5 bg-[#F0F1F2] rounded-lg"> <div className="h-1.5 bg-[#F0F1F2] rounded-lg">
<div className="h-full bg-[#49A1F5] rounded-lg animate-progress"></div> <div className="h-full bg-[#49A1F5] rounded-lg animate-progress"></div>
+28
View File
@@ -0,0 +1,28 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import api from "../utils/api";
export function useLanguageDetection() {
const { i18n } = useTranslation();
useEffect(() => {
async function detectLanguage() {
try {
const { countryCode, error }: { countryCode: string; error: string } =
await api.get("getCountryCode").json();
if (!error && countryCode && countryCode !== "RU") {
await i18n.changeLanguage("en");
} else if (!error && countryCode === "RU") {
await i18n.changeLanguage("ru");
}
} catch (error) {
console.error("Failed to get country code:", error);
// Fallback to browser language detection (handled by i18next-browser-languagedetector)
}
}
void detectLanguage();
}, [i18n]);
}
+184 -8
View File
@@ -1,5 +1,6 @@
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: {
@@ -118,6 +119,90 @@ const resources = {
getAccess: "Получите доступ у администратора трансляции!", getAccess: "Получите доступ у администратора трансляции!",
controlReceived: "Управление получено!", controlReceived: "Управление получено!",
}, },
feedbackSuccess: {
title: "Заявка отправлена",
thankYou: "Спасибо за подачу заявки!",
message:
"Мы ценим ваш интерес к нашей компании и в ближайшее время свяжемся с вами для уточнения деталей проекта.",
},
sidebarTab5: {
linkLabel: "Ссылка для подключения",
linkDescription:
"Ссылка, получаемая пользователем на почтовый адрес, для подключения к демонстрации.",
copyButton: "Скопировать",
},
tooltips: {
returnControl: "Вернуть управление",
requestControl: "Запросить управление",
turnOffMic: "Выключить микрофон",
turnOnMic: "Включить микрофон",
turnOffCamera: "Выключить камеру",
turnOnCamera: "Включить камеру",
hideParticipants: "Скрыть участников",
showParticipants: "Показать участников",
hideChat: "Скрыть чат",
showChat: "Показать чат",
share: "Поделиться",
windowedMode: "Оконный режим",
fullscreenMode: "Полноэкранный режим",
actions: "Действия",
},
toasts: {
requestPermission: "запрашивает разрешение на управление",
receivedPermission: "Вы получили разрешение на управление",
linkCopied: "Ссылка скопирована в буфер обмена",
linkCopiedExclamation: "Ссылка скопирована в буфер обмена!",
invitationSent: "Приглашение отправлено",
needPermission: "Необходимо запросить разрешение на управление",
},
calendar: {
mon: "пн",
tue: "вт",
wed: "ср",
thu: "чт",
fri: "пт",
sat: "сб",
sun: "вс",
},
errors: {
unknownError: "Неизвестная ошибка",
unknownCountryError: "Неизвестная ошибка при получении кода страны",
invalidCredentials: "Неверное имя пользователя или пароль",
failedToFetchData: "Не удалось получить данные",
noConnection: "Нет соединения с сервером, попробуйте позже",
},
userActions: {
transferControl: "Передать управление",
kick: "Исключить",
},
speedtest: {
pleaseWait: "Пожалуйста, подождите",
checkingConnection: "Проверяем качество вашего<br />интернет-соединения",
checking: "Проверка",
secondsLeft: "Осталось {{count}} секунд",
},
setName: {
hello: "Здравствуйте!",
introduceYourself: "Представьтесь, пожалуйста",
howToAddress: "Так мы будем знать, как к вам обратиться",
name: "Имя",
skip: "Не указывать",
continue: "Продолжить",
},
chat: {
placeholder: "Написать сообщение...",
title: "Чат",
demoChat: "Чат демонстрации",
},
login: {
title: "Вход в личный кабинет",
username: "Имя пользователя",
password: "Пароль",
loginButton: "Войти",
},
stream: {
rotateDevice: "Поверните устройство",
},
}, },
}, },
en: { en: {
@@ -140,7 +225,6 @@ const resources = {
allow: "Allow", // Разрешить allow: "Allow", // Разрешить
members: "Members", // Участники members: "Members", // Участники
invite: "Invite", // Пригласить invite: "Invite", // Пригласить
chat: "Chat", // Чат
scanQRCode: "Scan the QR code<br />to join the demonstration", // Отсканируйте QR-код, чтобы присоединиться к демонстрации scanQRCode: "Scan the QR code<br />to join the demonstration", // Отсканируйте QR-код, чтобы присоединиться к демонстрации
copyLinkToConnect: "Copy link to connect", // Скопировать ссылку для подключения copyLinkToConnect: "Copy link to connect", // Скопировать ссылку для подключения
loading: "Loading", loading: "Loading",
@@ -252,16 +336,108 @@ const resources = {
getAccess: "Get access from the stream administrator!", getAccess: "Get access from the stream administrator!",
controlReceived: "Control received!", controlReceived: "Control received!",
}, },
feedbackSuccess: {
title: "Request sent",
thankYou: "Thank you for submitting your request!",
message:
"We appreciate your interest in our company and will contact you shortly to discuss project details.",
},
sidebarTab5: {
linkLabel: "Connection link",
linkDescription:
"Link sent to the user's email address to connect to the demonstration.",
copyButton: "Copy",
},
tooltips: {
returnControl: "Return control",
requestControl: "Request control",
turnOffMic: "Turn off microphone",
turnOnMic: "Turn on microphone",
turnOffCamera: "Turn off camera",
turnOnCamera: "Turn on camera",
hideParticipants: "Hide participants",
showParticipants: "Show participants",
hideChat: "Hide chat",
showChat: "Show chat",
share: "Share",
windowedMode: "Windowed mode",
fullscreenMode: "Fullscreen mode",
actions: "Actions",
},
toasts: {
requestPermission: "requests permission to control",
receivedPermission: "You have received permission to control",
linkCopied: "Link copied to clipboard",
linkCopiedExclamation: "Link copied to clipboard!",
invitationSent: "Invitation sent",
needPermission: "You need to request permission to control",
},
calendar: {
mon: "Mo",
tue: "Tu",
wed: "We",
thu: "Th",
fri: "Fr",
sat: "Sa",
sun: "Su",
},
errors: {
unknownError: "Unknown error",
unknownCountryError: "Unknown error while getting country code",
invalidCredentials: "Invalid username or password",
failedToFetchData: "Failed to fetch data",
noConnection: "No connection to server, please try again later",
},
userActions: {
transferControl: "Transfer control",
kick: "Kick",
},
speedtest: {
pleaseWait: "Please wait",
checkingConnection: "Checking your<br />internet connection quality",
checking: "Checking",
secondsLeft: "{{count}} seconds left",
},
setName: {
hello: "Hello!",
introduceYourself: "Please introduce yourself",
howToAddress: "This way we will know how to address you",
name: "Name",
skip: "Skip",
continue: "Continue",
},
chat: {
placeholder: "Write a message...",
title: "Chat",
demoChat: "Demonstration chat",
},
login: {
title: "Login to personal account",
username: "Username",
password: "Password",
loginButton: "Login",
},
stream: {
rotateDevice: "Rotate device",
},
}, },
}, },
}; };
void i18n.use(initReactI18next).init({ void i18n
resources, .use(LanguageDetector)
fallbackLng: "ru", .use(initReactI18next)
interpolation: { .init({
escapeValue: false, resources,
}, fallbackLng: "ru",
}); supportedLngs: ["ru", "en"],
detection: {
order: ["navigator", "htmlTag"],
caches: [], // Отключаем кеширование в localStorage
},
interpolation: {
escapeValue: false,
},
});
export default i18n; export default i18n;
+4 -1
View File
@@ -8,6 +8,7 @@ import App from "./App";
import HistoryPage from "./HistoryPage"; import HistoryPage from "./HistoryPage";
import ScheduledPage from "./ScheduledPage"; import ScheduledPage from "./ScheduledPage";
import StreamPage from "./pages/StreamPage"; import StreamPage from "./pages/StreamPage";
import LanguageDetector from "./components/LanguageDetector";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -30,5 +31,7 @@ const router = createBrowserRouter([
]); ]);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<RouterProvider router={router} /> <LanguageDetector>
<RouterProvider router={router} />
</LanguageDetector>
); );
+22 -19
View File
@@ -24,7 +24,7 @@ import MicroOnIcon from "../components/icons/MicroOnIcon";
import MicroOffIcon from "../components/icons/MicroOffIcon"; import MicroOffIcon from "../components/icons/MicroOffIcon";
import CameraOnIcon from "../components/icons/CameraOnIcon"; import CameraOnIcon from "../components/icons/CameraOnIcon";
import CameraOffIcon from "../components/icons/CameraOffIcon"; import CameraOffIcon from "../components/icons/CameraOffIcon";
import { Trans } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { isIOS, isMobile, useMobileOrientation } from "react-device-detect"; import { isIOS, isMobile, useMobileOrientation } from "react-device-detect";
import WindowIcon from "../components/icons/WindowIcon"; import WindowIcon from "../components/icons/WindowIcon";
import FullscreenIcon from "../components/icons/FullscreenIcon"; import FullscreenIcon from "../components/icons/FullscreenIcon";
@@ -56,6 +56,7 @@ import InternetSpeedMediumIcon from "../components/icons/InternetSpeedMediumIcon
const userId = uuidv4(); const userId = uuidv4();
function StreamPage() { function StreamPage() {
const { t } = useTranslation();
const params = useParams(); const params = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [WSUrl, setWSUrl] = useState<string>(""); const [WSUrl, setWSUrl] = useState<string>("");
@@ -223,13 +224,13 @@ function StreamPage() {
if (user?.id === me?.id || !me?.isAdmin) return; if (user?.id === me?.id || !me?.isAdmin) return;
toast.info(`${user?.name} запрашивает разрешение на управление`); toast.info(`${user?.name} ${t("toasts.requestPermission")}`);
}); });
socket.on("transfer-control", (userId) => { socket.on("transfer-control", (userId) => {
if (me?.id !== userId) return; if (me?.id !== userId) return;
toast.info(`Вы получили разрешение на управление`); toast.info(t("toasts.receivedPermission"));
}); });
socket.on("kick", (userId) => { socket.on("kick", (userId) => {
@@ -481,11 +482,11 @@ function StreamPage() {
} }
/> />
{me?.isAdmin && !me.isControlAllowed && ( {me?.isAdmin && !me.isControlAllowed && (
<Tooltip text={"Вернуть управление"} /> <Tooltip text={t("tooltips.returnControl")} />
)} )}
{!me?.isAdmin && !me?.isControlAllowed && ( {!me?.isAdmin && !me?.isControlAllowed && (
<Tooltip text={"Запросить управление"} /> <Tooltip text={t("tooltips.requestControl")} />
)} )}
</div> </div>
@@ -503,8 +504,8 @@ function StreamPage() {
<Tooltip <Tooltip
text={ text={
isMicEnabled isMicEnabled
? "Выключить микрофон" ? t("tooltips.turnOffMic")
: "Включить микрофон" : t("tooltips.turnOnMic")
} }
/> />
</div> </div>
@@ -524,8 +525,8 @@ function StreamPage() {
<Tooltip <Tooltip
text={ text={
isCameraEnabled isCameraEnabled
? "Выключить камеру" ? t("tooltips.turnOffCamera")
: "Включить камеру" : t("tooltips.turnOnCamera")
} }
/> />
</div> </div>
@@ -546,8 +547,8 @@ function StreamPage() {
<Tooltip <Tooltip
text={ text={
isShowUsers isShowUsers
? "Скрыть участников" ? t("tooltips.hideParticipants")
: "Показать участников" : t("tooltips.showParticipants")
} }
/> />
{users.filter((user) => user.id !== userId).length > {users.filter((user) => user.id !== userId).length >
@@ -587,7 +588,9 @@ function StreamPage() {
)} )}
/> />
<Tooltip <Tooltip
text={isShowChat ? "Скрыть чат" : "Показать чат"} text={
isShowChat ? t("tooltips.hideChat") : t("tooltips.showChat")
}
/> />
</div> </div>
<div className="w-px h-4 bg-[#DAE0E5] max-lg:hidden"></div> <div className="w-px h-4 bg-[#DAE0E5] max-lg:hidden"></div>
@@ -599,7 +602,7 @@ function StreamPage() {
onlyIcon onlyIcon
onClick={() => setModal(<InviteModal />)} onClick={() => setModal(<InviteModal />)}
/> />
<Tooltip text={"Поделиться"} /> <Tooltip text={t("tooltips.share")} />
</div> </div>
{!isIOS && ( {!isIOS && (
<div className="relative group"> <div className="relative group">
@@ -614,8 +617,8 @@ function StreamPage() {
<Tooltip <Tooltip
text={ text={
isFullscreen isFullscreen
? "Оконный режим" ? t("tooltips.windowedMode")
: "Полноэкранный режим" : t("tooltips.fullscreenMode")
} }
/> />
</div> </div>
@@ -644,9 +647,7 @@ function StreamPage() {
!users.find((user) => user.id === userId)?.isControlAllowed && ( !users.find((user) => user.id === userId)?.isControlAllowed && (
<div <div
className="absolute top-0 left-0 w-full h-full" className="absolute top-0 left-0 w-full h-full"
onClick={() => onClick={() => toast.warn(t("toasts.needPermission"))}
toast.warn("Необходимо запросить разрешение на управление")
}
></div> ></div>
)} )}
@@ -704,7 +705,9 @@ function StreamPage() {
{isPortrait && ( {isPortrait && (
<div className="flex absolute top-0 left-0 flex-col gap-2 justify-center items-center w-full h-full bg-white"> <div className="flex absolute top-0 left-0 flex-col gap-2 justify-center items-center w-full h-full bg-white">
<Rotate64Icon /> <Rotate64Icon />
<p className="font-semibold">Поверните устройство</p> <p className="font-semibold">
<Trans i18nKey={"stream.rotateDevice"}>Поверните устройство</Trans>
</p>
</div> </div>
)} )}