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:
@@ -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
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user