diff --git a/bun.lock b/bun.lock index 33cc3ec..c5dfbf9 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "ps2-react-client", @@ -9,7 +10,7 @@ "ahooks": "^3.7.10", "date-fns": "^2.30.0", "i18next": "^23.8.2", - "i18next-browser-languagedetector": "^7.2.0", + "i18next-browser-languagedetector": "^8.2.0", "ky": "^1.1.3", "peerjs": "^1.5.4", "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-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=="], diff --git a/package.json b/package.json index 406446a..530f2a3 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "ahooks": "^3.7.10", "date-fns": "^2.30.0", "i18next": "^23.8.2", - "i18next-browser-languagedetector": "^7.2.0", + "i18next-browser-languagedetector": "^8.2.0", "ky": "^1.1.3", "peerjs": "^1.5.4", "react": "^18.2.0", diff --git a/src/App.tsx b/src/App.tsx index f274b42..0d48b47 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,7 +20,6 @@ import ky from "ky"; import { useNavigate, useSearchParams } from "react-router-dom"; import { Bounce, ToastContainer, toast } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; -// import api from "./utils/api"; import InfoIcon from "./components/icons/InfoIcon"; 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) { // const { countryCode, error }: { countryCode: string; error: string } = @@ -108,7 +99,7 @@ function App() { toastError(response.error); // setLoading(false); } else { - toastError("Неизвестная ошибка"); + toastError(t("errors.unknownError")); // setLoading(false); } } catch (error) { @@ -131,8 +122,6 @@ function App() { }, [streamUrl]); useEffect(() => { - // getLang(); - if (build) { void startStream(build); } @@ -146,9 +135,7 @@ function App() { <>
-
void i18n.changeLanguage(lang)} - /> +
diff --git a/src/PersonalAreaLoginPage.tsx b/src/PersonalAreaLoginPage.tsx index 6369052..09c3dae 100644 --- a/src/PersonalAreaLoginPage.tsx +++ b/src/PersonalAreaLoginPage.tsx @@ -2,6 +2,7 @@ import ky from "ky"; import useAuthStore from "./stores/useAuthStore"; import { FormEvent, useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; type User = { id: string; @@ -15,6 +16,7 @@ interface IResult { } function PersonalAreaLoginPage() { + const { t } = useTranslation(); const [setAccessToken, setUser] = useAuthStore((state) => [ state.setAccessToken, state.setUser, @@ -45,12 +47,12 @@ function PersonalAreaLoginPage() { passwordRef.current?.focus(); setPassword(""); - setError("Неверное имя пользователя или пароль"); + setError(t("errors.invalidCredentials")); return; } if (!result.accessToken || !result.user) { - setError("Не удалось получить данные"); + setError(t("errors.failedToFetchData")); return; } @@ -61,7 +63,7 @@ function PersonalAreaLoginPage() { if (error instanceof Error) { if (error.message === "Failed to fetch") { - setError("Нет соединения с сервером, попробуйте позже"); + setError(t("errors.noConnection")); } else { setError(error.message); } @@ -72,11 +74,15 @@ function PersonalAreaLoginPage() { return (
-

Вход в личный кабинет

+

+ Вход в личный кабинет +

-

Имя пользователя

+

+ Имя пользователя +

-

Пароль

+

+ Пароль +

) : ( - Войти + + Войти + )} diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx index 87b86b5..00caac9 100644 --- a/src/components/Calendar.tsx +++ b/src/components/Calendar.tsx @@ -17,7 +17,7 @@ import { useEffect, useState } from "react"; import ChevronRightIcon from "./icons/ChevronRightIcon"; import ChevronLeftIcon from "./icons/ChevronLeftIcon"; import { Trans } from "react-i18next"; -import i18n from "../i18n"; +import { useTranslation } from "react-i18next"; interface CalendarProps { schedules: any[]; @@ -30,6 +30,7 @@ function classNames(...classes: (string | boolean)[]) { } function Calendar({ schedules, handleSelect, className }: CalendarProps) { + const { t, i18n } = useTranslation(); const today = startOfToday(); const [selectedDay, setSelectedDay] = useState(null); const [currentMonth, setCurrentMonth] = useState(format(today, "MMM-yyyy")); @@ -76,13 +77,13 @@ function Calendar({ schedules, handleSelect, className }: CalendarProps) {
-
{i18n.language === "ru" ? "пн" : "Mo"}
-
{i18n.language === "ru" ? "вт" : "Tu"}
-
{i18n.language === "ru" ? "ср" : "We"}
-
{i18n.language === "ru" ? "чт" : "Th"}
-
{i18n.language === "ru" ? "пт" : "Fr"}
-
{i18n.language === "ru" ? "сб" : "Sa"}
-
{i18n.language === "ru" ? "вс" : "Su"}
+
{t("calendar.mon")}
+
{t("calendar.tue")}
+
{t("calendar.wed")}
+
{t("calendar.thu")}
+
{t("calendar.fri")}
+
{t("calendar.sat")}
+
{t("calendar.sun")}
{days.map((day, dayIdx) => ( diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 995a1fc..4768470 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -7,6 +7,7 @@ import "./Chat.css"; import ChevronDownIcon from "./icons/ChevronDownIcon"; import CloseIcon from "./icons/CloseIcon"; import useChatStore from "../stores/useChatStore"; +import { Trans, useTranslation } from "react-i18next"; interface ChatProps { className?: string; @@ -14,6 +15,7 @@ interface ChatProps { } function Chat({ className, handleClose }: ChatProps) { + const { t } = useTranslation(); const socket = useSocketStore((state) => state.socket); const [message, setMessage] = useState(""); const [messages, setMessages] = useChatStore((state) => [ @@ -94,7 +96,9 @@ function Chat({ className, handleClose }: ChatProps) { ].join(" ")} >
-

Чат

+

+ Чат +

@@ -137,7 +141,7 @@ function Chat({ className, handleClose }: ChatProps) {

- Чат демонстрации + Чат демонстрации

-
*/} +
); } diff --git a/src/components/InviteModal.tsx b/src/components/InviteModal.tsx index b09c70e..0163c85 100644 --- a/src/components/InviteModal.tsx +++ b/src/components/InviteModal.tsx @@ -9,8 +9,10 @@ import api from "../utils/api"; import { ToastContainer, toast } from "react-toastify"; import MailIcon from "./icons/MailIcon"; import QRCode from "react-qr-code"; +import { Trans, useTranslation } from "react-i18next"; function InviteModal() { + const { t } = useTranslation(); const { setModal } = useModalStore(); const clipboard = useClipboard(); const [email, setEmail] = useState(""); @@ -26,7 +28,7 @@ function InviteModal() { .json(); console.log(result); - toast.success("Приглашение отправлено", { + toast.success(t("toasts.invitationSent"), { position: "top-center", autoClose: 2000, hideProgressBar: true, @@ -48,7 +50,7 @@ function InviteModal() { function handleClickClipboard() { clipboard.copy(); - toast.success("Ссылка скопирована в буфер обмена", { + toast.success(t("toasts.linkCopied"), { position: "top-center", autoClose: 2000, hideProgressBar: true, @@ -64,7 +66,9 @@ function InviteModal() { return (
-

Пригласить

+

+ Пригласить +

@@ -110,7 +118,9 @@ function InviteModal() { onClick={handleClickClipboard} > - Скопировать ссылку + + Скопировать ссылку +
diff --git a/src/components/LanguageDetector.tsx b/src/components/LanguageDetector.tsx new file mode 100644 index 0000000..e7d00e0 --- /dev/null +++ b/src/components/LanguageDetector.tsx @@ -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; + diff --git a/src/components/SidebarTab5.tsx b/src/components/SidebarTab5.tsx index d145825..2e924be 100644 --- a/src/components/SidebarTab5.tsx +++ b/src/components/SidebarTab5.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-irregular-whitespace */ -import { Trans } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import useSidebarTabStore from "../stores/useSidebarStore"; import ArrowRightIcon from "./icons/ArrowRightIcon"; import MailGradientIcon from "./icons/MailGradientIcon"; @@ -10,12 +10,13 @@ import { Bounce, ToastContainer, toast } from "react-toastify"; import InfoIcon from "./icons/InfoBlueIcon"; function SidebarTab5() { + const { t } = useTranslation(); const { setIsOpen, name, url } = useSidebarTabStore(); const [, copyToClipboard] = useCopyToClipboard(); function handleClickCopy() { copyToClipboard(url); - toast.info("Ссылка скопирована в буфер обмена!", { + toast.info(t("toasts.linkCopiedExclamation"), { icon: , position: "top-center", autoClose: 5000, @@ -53,23 +54,28 @@ function SidebarTab5() {

-

Ссылка для подключения

+

+ + Ссылка для подключения + +

console.log(1)} />

- Ссылка, получаемая пользователем на почтовый адрес, для подключения - к демонстрации. + + Ссылка, получаемая пользователем на почтовый адрес, для + подключения к демонстрации. +

diff --git a/src/components/User.tsx b/src/components/User.tsx index 82b32d9..21d126e 100644 --- a/src/components/User.tsx +++ b/src/components/User.tsx @@ -13,6 +13,7 @@ import SpinnerIcon from "./icons/SpinnerIcon"; import InternetSpeedHighIcon from "./icons/InternetSpeedHighIcon"; import InternetSpeedMediumIcon from "./icons/InternetSpeedMediumIcon"; import InternetSpeedLowIcon from "./icons/InternetSpeedLowIcon"; +import { useTranslation } from "react-i18next"; interface Props { me: IUser; @@ -22,6 +23,7 @@ interface Props { } function User({ me, user, handleTransferControl, handleKick }: Props) { + const { t } = useTranslation(); const [showMore, setShowMore] = useState(false); const ref = useClickAway(() => { @@ -65,7 +67,7 @@ function User({ me, user, handleTransferControl, handleKick }: Props) { onlyIcon onClick={() => setShowMore(true)} /> - + {showMore && (
@@ -85,7 +87,7 @@ function User({ me, user, handleTransferControl, handleKick }: Props) { - Передать управление + {t("userActions.transferControl")}
)} diff --git a/src/components/Users.tsx b/src/components/Users.tsx index 120aefc..38f00dc 100644 --- a/src/components/Users.tsx +++ b/src/components/Users.tsx @@ -21,7 +21,7 @@ function Users({ onClose, transferControl, kick }: Props) {

- Участники + Участники

diff --git a/src/components/modals/stream/SpeedtestModal.tsx b/src/components/modals/stream/SpeedtestModal.tsx index 8d2b09c..046cd19 100644 --- a/src/components/modals/stream/SpeedtestModal.tsx +++ b/src/components/modals/stream/SpeedtestModal.tsx @@ -4,12 +4,14 @@ import { useEffect, useRef } from "react"; import { useSpeedtest } from "../../../hooks/useSpeedtest"; import { useCountdown } from "usehooks-ts"; +import { Trans, useTranslation } from "react-i18next"; interface Props { onSuccess: (downloadSpeed: number) => void; } function SpeedtestModal({ onSuccess }: Props) { + const { t } = useTranslation(); const [downloadSpeed] = useSpeedtest(); const downloadSpeedRef = useRef(); downloadSpeedRef.current = downloadSpeed; @@ -30,18 +32,26 @@ function SpeedtestModal({ onSuccess }: Props) {

- Пожалуйста, подождите + + Пожалуйста, подождите +

- Проверяем качество вашего -
- интернет-соединения + + Проверяем качество вашего +
+ интернет-соединения +

-

Проверка

-

Осталось {counter} секунд

+

+ Проверка +

+

+ {t("speedtest.secondsLeft", { count: counter })} +

diff --git a/src/hooks/useLanguageDetection.ts b/src/hooks/useLanguageDetection.ts new file mode 100644 index 0000000..74d6bed --- /dev/null +++ b/src/hooks/useLanguageDetection.ts @@ -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]); +} + diff --git a/src/i18n.ts b/src/i18n.ts index 6c6d48d..553df44 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,5 +1,6 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; const resources = { ru: { @@ -118,6 +119,90 @@ const resources = { getAccess: "Получите доступ у администратора трансляции!", 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: "Проверяем качество вашего
интернет-соединения", + checking: "Проверка", + secondsLeft: "Осталось {{count}} секунд", + }, + setName: { + hello: "Здравствуйте!", + introduceYourself: "Представьтесь, пожалуйста", + howToAddress: "Так мы будем знать, как к вам обратиться", + name: "Имя", + skip: "Не указывать", + continue: "Продолжить", + }, + chat: { + placeholder: "Написать сообщение...", + title: "Чат", + demoChat: "Чат демонстрации", + }, + login: { + title: "Вход в личный кабинет", + username: "Имя пользователя", + password: "Пароль", + loginButton: "Войти", + }, + stream: { + rotateDevice: "Поверните устройство", + }, }, }, en: { @@ -140,7 +225,6 @@ const resources = { allow: "Allow", // Разрешить members: "Members", // Участники invite: "Invite", // Пригласить - chat: "Chat", // Чат scanQRCode: "Scan the QR code
to join the demonstration", // Отсканируйте QR-код, чтобы присоединиться к демонстрации copyLinkToConnect: "Copy link to connect", // Скопировать ссылку для подключения loading: "Loading", @@ -252,16 +336,108 @@ const resources = { getAccess: "Get access from the stream administrator!", 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
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({ - resources, - fallbackLng: "ru", - interpolation: { - escapeValue: false, - }, -}); +void i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources, + fallbackLng: "ru", + supportedLngs: ["ru", "en"], + detection: { + order: ["navigator", "htmlTag"], + caches: [], // Отключаем кеширование в localStorage + }, + interpolation: { + escapeValue: false, + }, + }); export default i18n; diff --git a/src/main.tsx b/src/main.tsx index f60c714..727db09 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -8,6 +8,7 @@ import App from "./App"; import HistoryPage from "./HistoryPage"; import ScheduledPage from "./ScheduledPage"; import StreamPage from "./pages/StreamPage"; +import LanguageDetector from "./components/LanguageDetector"; const router = createBrowserRouter([ { @@ -30,5 +31,7 @@ const router = createBrowserRouter([ ]); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + + + ); diff --git a/src/pages/StreamPage.tsx b/src/pages/StreamPage.tsx index fddc43c..2e8425b 100644 --- a/src/pages/StreamPage.tsx +++ b/src/pages/StreamPage.tsx @@ -24,7 +24,7 @@ import MicroOnIcon from "../components/icons/MicroOnIcon"; import MicroOffIcon from "../components/icons/MicroOffIcon"; import CameraOnIcon from "../components/icons/CameraOnIcon"; 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 WindowIcon from "../components/icons/WindowIcon"; import FullscreenIcon from "../components/icons/FullscreenIcon"; @@ -56,6 +56,7 @@ import InternetSpeedMediumIcon from "../components/icons/InternetSpeedMediumIcon const userId = uuidv4(); function StreamPage() { + const { t } = useTranslation(); const params = useParams(); const [searchParams] = useSearchParams(); const [WSUrl, setWSUrl] = useState(""); @@ -223,13 +224,13 @@ function StreamPage() { if (user?.id === me?.id || !me?.isAdmin) return; - toast.info(`${user?.name} запрашивает разрешение на управление`); + toast.info(`${user?.name} ${t("toasts.requestPermission")}`); }); socket.on("transfer-control", (userId) => { if (me?.id !== userId) return; - toast.info(`Вы получили разрешение на управление`); + toast.info(t("toasts.receivedPermission")); }); socket.on("kick", (userId) => { @@ -481,11 +482,11 @@ function StreamPage() { } /> {me?.isAdmin && !me.isControlAllowed && ( - + )} {!me?.isAdmin && !me?.isControlAllowed && ( - + )}
@@ -503,8 +504,8 @@ function StreamPage() {
@@ -524,8 +525,8 @@ function StreamPage() {
@@ -546,8 +547,8 @@ function StreamPage() { {users.filter((user) => user.id !== userId).length > @@ -587,7 +588,9 @@ function StreamPage() { )} />
@@ -599,7 +602,7 @@ function StreamPage() { onlyIcon onClick={() => setModal()} /> - +
{!isIOS && (
@@ -614,8 +617,8 @@ function StreamPage() {
@@ -644,9 +647,7 @@ function StreamPage() { !users.find((user) => user.id === userId)?.isControlAllowed && (
- toast.warn("Необходимо запросить разрешение на управление") - } + onClick={() => toast.warn(t("toasts.needPermission"))} >
)} @@ -704,7 +705,9 @@ function StreamPage() { {isPortrait && (
-

Поверните устройство

+

+ Поверните устройство +

)}