diff --git a/lib/components/QuizAnswerer.tsx b/lib/components/QuizAnswerer.tsx
index 451a1b3..c733d39 100644
--- a/lib/components/QuizAnswerer.tsx
+++ b/lib/components/QuizAnswerer.tsx
@@ -77,12 +77,12 @@ function QuizAnswererInner({
if (error) return ;
// if (!data) return ;
quizSettings ??= data;
- if (!quizSettings) return ;
+ if (!quizSettings) return ;
if (quizSettings.questions.length === 1 && quizSettings?.settings.cfg.noStartPage)
- return ;
- // if (quizSettings.questions.length === 1) return ;
- if (!quizId) return ;
+ return ;
+ // if (quizSettings.questions.length === 1) return ;
+ if (!quizId) return ;
const quizContainer = (
;
export const ApologyPage = ({ error }: Props) => {
- let message = "Что-то пошло не так";
-
- if (error.response?.data === "quiz is inactive") message = "Квиз не активирован";
- if (error.message === "No questions found") message = "Нет созданных вопросов";
- if (error.message === "Quiz is empty") message = "Квиз пуст";
- if (error.message === "Quiz already completed") message = "Вы уже прошли этот опрос";
- if (error.message === "No quiz id") message = "Отсутствует id квиза";
- if (error.message === "Quiz data is null") message = "Не были переданы параметры квиза";
- if (error.response?.data === "Invalid request data") message = "Такого квиза не существует";
+ let message = error.message || error.response?.data;
+ const { t } = useTranslation();
return (
{
color: "text.primary",
}}
>
- {message}
+ {t(message.toLowerCase())}
);
diff --git a/lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx b/lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx
index c57d4c9..73149a9 100644
--- a/lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx
+++ b/lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx
@@ -25,6 +25,7 @@ import type { FormContactFieldData, FormContactFieldName } from "@model/settings
import type { QuizQuestionResult } from "@model/questionTypes/result";
import type { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { isProduction } from "@/utils/defineDomain";
+import { useTranslation } from "react-i18next";
type Props = {
currentQuestion: AnyTypedQuizQuestion;
@@ -49,6 +50,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
const [fire, setFire] = useState(false);
const isMobile = useRootContainerSize() < 850;
const isTablet = useRootContainerSize() < 1000;
+ const { t } = useTranslation();
const vkMetrics = useVkMetricsGoals(settings.cfg.vkMetricsNumber);
const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber);
@@ -85,7 +87,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
if (email.length > 0) body.email = email;
if (phone.length > 0) body.phone = phone;
if (adress.length > 0) body.address = adress;
- if (text.length > 0) body.customs = { [FC.text.text || "Фамилия"]: text };
+ if (text.length > 0) body.customs = { [FC.text.text || t("Last name")]: text };
if (Object.keys(body).length > 0) {
try {
@@ -99,7 +101,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
localStorage.setItem("sessions", JSON.stringify({ ...sessions, [quizId]: new Date().getTime() }));
} catch (e) {
- enqueueSnackbar("ответ не был засчитан");
+ enqueueSnackbar(t("The answer was not counted"));
}
}
};
@@ -119,12 +121,12 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
const FC = settings.cfg.formContact.fields;
if (!isDisableEmail && FC["email"].used !== EMAIL_REGEXP.test(email)) {
- return enqueueSnackbar("введена некорректная почта");
+ return enqueueSnackbar("Incorrect email entered");
}
if (fireOnce.current) {
if (name.length === 0 && email.length === 0 && phone.length === 0 && text.length === 0 && adress.length === 0)
- return enqueueSnackbar("Пожалуйста, заполните поля");
+ return enqueueSnackbar(t("Please fill in the fields"));
//почта валидна, хоть одно поле заполнено
setFire(true);
@@ -159,7 +161,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
yandexMetrics.contactsFormField("address");
}
} catch (e) {
- enqueueSnackbar("повторите попытку позже");
+ enqueueSnackbar(t("Please try again later"));
}
if (settings.cfg.resultInfo.showResultForm === "after") {
onShowResult();
@@ -286,17 +288,17 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
href={"https://shub.pena.digital/ppdd"}
target="_blank"
>
- Положением об обработке персональных данных{" "}
+ {`${t("Regulation on the processing of personal data")} `}
- и
+ {t("and")}
{" "}
- Политикой конфиденциальности{" "}
+ {`${t("Privacy Policy")} `}
- ознакомлен
+ {t("familiarized")}
@@ -315,7 +317,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
},
}}
>
- {settings.cfg.formContact?.button || "Получить результаты"}
+ {settings.cfg.formContact?.button || t("Get results")}
{show_badge && (
diff --git a/lib/components/ViewPublicationPage/ContactForm/ContactTextBlock/index.tsx b/lib/components/ViewPublicationPage/ContactForm/ContactTextBlock/index.tsx
index f250b1a..1240c71 100644
--- a/lib/components/ViewPublicationPage/ContactForm/ContactTextBlock/index.tsx
+++ b/lib/components/ViewPublicationPage/ContactForm/ContactTextBlock/index.tsx
@@ -2,6 +2,7 @@ import { Box, Typography, useTheme } from "@mui/material";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext.ts";
import { QuizSettingsConfig } from "@model/settingsData.ts";
import { FC } from "react";
+import { useTranslation } from "react-i18next";
type ContactTextBlockProps = {
settings: QuizSettingsConfig;
@@ -11,6 +12,7 @@ export const ContactTextBlock: FC = ({ settings }) => {
const theme = useTheme();
const isMobile = useRootContainerSize() < 850;
const isTablet = useRootContainerSize() < 1000;
+ const { t } = useTranslation();
return (
= ({ settings }) => {
wordBreak: "break-word",
}}
>
- {settings.cfg.formContact.title || "Заполните форму, чтобы получить результаты теста"}
+ {settings.cfg.formContact.title || t("Fill out the form to receive your test results")}
{settings.cfg.formContact.desc && (
{
const { settings } = useQuizSettings();
+ const { t } = useTranslation();
const FC = settings.cfg.formContact.fields;
if (!FC) return null;
@@ -45,8 +47,8 @@ export const Inputs = ({
setName(target.value)}
id={name}
- title={FC["name"].innerText || "Введите имя"}
- desc={FC["name"].text || "Имя"}
+ title={FC["name"].innerText || `${t("Enter")} ${t("Name").toLowerCase()}`}
+ desc={FC["name"].text || t("Name")}
Icon={NameIcon}
/>
);
@@ -56,7 +58,7 @@ export const Inputs = ({
setEmail(target.value.replaceAll(/\s/g, ""));
}}
id={email}
- title={FC["email"].innerText || "Введите Email"}
+ title={FC["email"].innerText || `${t("Enter")} Email`}
desc={FC["email"].text || "Email"}
Icon={EmailIcon}
type="email"
@@ -70,8 +72,8 @@ export const Inputs = ({
}}
value={phone}
id={phone}
- title={FC["phone"].innerText || "Введите номер телефона"}
- desc={FC["phone"].text || "Номер телефона"}
+ title={FC["phone"].innerText || `${t("Enter")} ${t("Phone number").toLowerCase()}`}
+ desc={FC["phone"].text || t("Phone number")}
Icon={PhoneIcon}
isPhone={true}
/>
@@ -80,8 +82,8 @@ export const Inputs = ({
setText(target.value)}
id={text}
- title={FC["text"].text || "Введите фамилию"}
- desc={FC["text"].innerText || "Фамилия"}
+ title={FC["text"].text || `${t("Enter")} ${t("Last name").toLowerCase()}`}
+ desc={FC["text"].innerText || t("Last name")}
Icon={TextIcon}
/>
);
@@ -89,8 +91,8 @@ export const Inputs = ({
setAdress(target.value)}
id={adress}
- title={FC["address"].innerText || "Введите адрес"}
- desc={FC["address"].text || "Адрес"}
+ title={FC["address"].innerText || `${t("Enter")} ${t("Address").toLowerCase()}`}
+ desc={FC["address"].text || t("Address")}
Icon={AddressIcon}
/>
);
diff --git a/lib/components/ViewPublicationPage/Footer.tsx b/lib/components/ViewPublicationPage/Footer.tsx
index f9c41e3..0673610 100644
--- a/lib/components/ViewPublicationPage/Footer.tsx
+++ b/lib/components/ViewPublicationPage/Footer.tsx
@@ -4,6 +4,7 @@ import { Box, Typography, useTheme } from "@mui/material";
import { useQuizSettings } from "@contexts/QuizDataContext";
import Stepper from "@ui_kit/Stepper";
+import { useTranslation } from "react-i18next";
type FooterProps = {
stepNumber: number | null;
@@ -15,6 +16,7 @@ export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
const theme = useTheme();
const { questions, settings } = useQuizSettings();
const questionsAmount = questions.filter(({ type }) => type !== "result").length;
+ const { t } = useTranslation();
return (
{
{stepNumber !== null && (
- Вопрос {stepNumber} из {questionsAmount}
+ {t("Step")} {stepNumber} {t("of")} {questionsAmount}
{
const theme = useTheme();
const { questions } = useQuizSettings();
const answers = useQuizViewStore((state) => state.answers);
+ const { t } = useTranslation();
const questionsWothoutResult = questions.filter(
(q: AnyTypedQuizQuestion): q is QuizQuestionVariant => q.type === "variant"
@@ -58,7 +60,7 @@ export const PointSystemResultList = () => {
color: theme.palette.text.primary,
}}
>
- {currentQuestion.title || "Вопрос без названия"}
+ {currentQuestion.title || t("Question without a title")}
{
color: theme.palette.grey[500],
}}
>
- Ваш ответ:
+ {t("Your answer")}:
{
setQuestion(target.value);
}}
diff --git a/lib/components/ViewPublicationPage/ResultForm.tsx b/lib/components/ViewPublicationPage/ResultForm.tsx
index 2a55429..5289eac 100644
--- a/lib/components/ViewPublicationPage/ResultForm.tsx
+++ b/lib/components/ViewPublicationPage/ResultForm.tsx
@@ -19,6 +19,7 @@ import { PointSystemResultList } from "./PointSystemResultList";
import { enqueueSnackbar } from "notistack";
import { sendFC, sendResult } from "@/api/quizRelase";
import { isProduction } from "@/utils/defineDomain";
+import { useTranslation } from "react-i18next";
type ResultFormProps = {
resultQuestion: QuizQuestionResult;
@@ -35,6 +36,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
const spec = settings.cfg.spec;
const vkMetrics = useVkMetricsGoals(settings.cfg.vkMetricsNumber);
const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber);
+ const { t } = useTranslation();
useEffect(() => {
vkMetrics.resultIdShown(resultQuestion.id);
@@ -55,7 +57,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
localStorage.setItem("sessions", JSON.stringify({ ...sessions, [quizId]: new Date().getTime() }));
} catch (e) {
- enqueueSnackbar("Заявка не может быть отправлена");
+ enqueueSnackbar(t("The request could not be sent"));
}
}
if (Boolean(settings.cfg.score)) {
@@ -70,7 +72,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
localStorage.setItem("sessions", JSON.stringify({ ...sessions, [quizId]: new Date().getTime() }));
} catch (e) {
- enqueueSnackbar("Количество баллов не может быть отправлено");
+ enqueueSnackbar(t("Количество баллов не может быть отправлено"));
}
}
})();
@@ -154,7 +156,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
wordBreak: "break-word",
}}
>
- Ваш результат:
+ {t("Your result")}:
{
fontWeight: 600,
}}
>
- Ваши баллы
+ {t("Your points")}
{
fontWeight: 600,
}}
>
- {pointsSum} из {questions.filter((e) => e.type != "result").length}
+ {pointsSum} {t("of")} {questions.filter((e) => e.type != "result").length}
{
},
}}
>
- Посмотреть ответы
+ {t("View answers")}
}
sx={{
@@ -339,7 +341,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
height: "50px",
}}
>
- {resultQuestion.content.hint.text || "Узнать подробнее"}
+ {resultQuestion.content.hint.text || t("Find out more")}
)}
{settings.cfg.resultInfo.showResultForm === "after" && resultQuestion.content.redirect && (
@@ -361,7 +363,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
width: "auto",
}}
>
- {resultQuestion.content.hint.text || "Перейти на сайт"}
+ {resultQuestion.content.hint.text || t("Go to website")}
)}
diff --git a/lib/components/ViewPublicationPage/StartPageViewPublication/index.tsx b/lib/components/ViewPublicationPage/StartPageViewPublication/index.tsx
index c3ad205..dd8db91 100644
--- a/lib/components/ViewPublicationPage/StartPageViewPublication/index.tsx
+++ b/lib/components/ViewPublicationPage/StartPageViewPublication/index.tsx
@@ -164,20 +164,6 @@ export const StartPageViewPublication = () => {
: "#FFFFFF",
}}
/>
- {/**/}
- {/* Сделано на PenaQuiz*/}
- {/**/}
);
diff --git a/lib/components/ViewPublicationPage/questions/Date/DateRange.tsx b/lib/components/ViewPublicationPage/questions/Date/DateRange.tsx
index ff2462b..6e3e30a 100644
--- a/lib/components/ViewPublicationPage/questions/Date/DateRange.tsx
+++ b/lib/components/ViewPublicationPage/questions/Date/DateRange.tsx
@@ -7,6 +7,7 @@ import type { Moment } from "moment";
import moment from "moment";
import { Box, Paper, TextField, useTheme } from "@mui/material";
import { useRootContainerSize } from "@/contexts/RootContainerWidthContext";
+import { useTranslation } from "react-i18next";
type DateProps = {
currentQuestion: QuizQuestionDate;
@@ -18,6 +19,7 @@ export default ({ currentQuestion }: DateProps) => {
const isMobile = useRootContainerSize() < 690;
const { settings } = useQuizSettings();
const { updateAnswer } = useQuizViewStore((state) => state);
+ const { t } = useTranslation();
const answers = useQuizViewStore((state) => state.answers);
const answer = (answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string) || ["0", "0"];
@@ -51,7 +53,7 @@ export default ({ currentQuestion }: DateProps) => {
}}
>
- От
+ {t("From")}
{
/>
- До
+ {t("До")}
state.answers);
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const theme = useTheme();
+ const { t } = useTranslation();
const onVariantClick = async (event: MouseEvent) => {
event.preventDefault();
@@ -181,7 +183,7 @@ export const EmojiVariant = ({
pl: "15px",
}}
>
- Введите свой ответ
+ {t("Enter your answer")}
)}
(false);
const theme = useTheme();
+ const { t } = useTranslation();
const answers = useQuizViewStore((state) => state.answers);
const { updateAnswer } = useQuizViewStore((state) => state);
const isMobile = useRootContainerSize() < 500;
@@ -68,7 +70,7 @@ export const UploadFile = ({ currentQuestion, setModalWarningType, isSending, se
updateAnswer(currentQuestion.id, `${file.name}|${URL.createObjectURL(file)}`, 0);
} catch (error) {
console.error(error);
- enqueueSnackbar("ответ не был засчитан");
+ enqueueSnackbar(t("The answer was not counted"));
}
setIsSending(false);
diff --git a/lib/components/ViewPublicationPage/questions/File/UploadedFile.tsx b/lib/components/ViewPublicationPage/questions/File/UploadedFile.tsx
index ca6b63a..3c08d31 100644
--- a/lib/components/ViewPublicationPage/questions/File/UploadedFile.tsx
+++ b/lib/components/ViewPublicationPage/questions/File/UploadedFile.tsx
@@ -7,6 +7,7 @@ import { useQuizViewStore } from "@stores/quizView";
import CloseBold from "@icons/CloseBold";
import type { QuizQuestionFile } from "@model/questionTypes/file";
+import { useTranslation } from "react-i18next";
type UploadedFileProps = {
currentQuestion: QuizQuestionFile;
@@ -18,6 +19,7 @@ export const UploadedFile = ({ currentQuestion, setIsSending }: UploadedFileProp
const answers = useQuizViewStore((state) => state.answers);
const { updateAnswer } = useQuizViewStore((state) => state);
const theme = useTheme();
+ const { t } = useTranslation();
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
@@ -39,7 +41,7 @@ export const UploadedFile = ({ currentQuestion, setIsSending }: UploadedFileProp
return (
- Вы загрузили:
+ {t("You have uploaded")}:
{
};
const CurrentModal = ({ status }: { status: ModalWarningType }) => {
+ const { t } = useTranslation();
switch (status) {
case null:
return null;
case "errorType":
- return Выбран некорректный тип файла;
+ return {t("Incorrect file type selected")};
case "errorSize":
- return Файл слишком большой. Максимальный размер 50 МБ;
+ return {t("File is too big. Maximum size is 50 MB")};
default:
return (
<>
- Допустимые расширения файлов:
+ {t("Acceptable file extensions")}:
{ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}
>
);
diff --git a/lib/components/ViewPublicationPage/questions/Images/ImageVariant.tsx b/lib/components/ViewPublicationPage/questions/Images/ImageVariant.tsx
index 57b5f03..b25540e 100644
--- a/lib/components/ViewPublicationPage/questions/Images/ImageVariant.tsx
+++ b/lib/components/ViewPublicationPage/questions/Images/ImageVariant.tsx
@@ -8,6 +8,7 @@ import RadioIcon from "@ui_kit/RadioIcon";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useMemo, type MouseEvent, useRef, useEffect } from "react";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
+import { useTranslation } from "react-i18next";
type ImagesProps = {
questionId: string;
@@ -95,6 +96,7 @@ export const ImageVariant = ({
const { settings } = useQuizSettings();
const { deleteAnswer, updateAnswer } = useQuizViewStore((state) => state);
const theme = useTheme();
+ const { t } = useTranslation();
const answers = useQuizViewStore((state) => state.answers);
const isMobile = useRootContainerSize() < 450;
const isTablet = useRootContainerSize() < 850;
@@ -204,7 +206,7 @@ export const ImageVariant = ({
pl: "15px",
}}
>
- Введите свой ответ
+ {t("Enter your answer")}
)}
;
@@ -28,6 +29,7 @@ interface OwnInputProps {
}
const OwnInput = ({ questionId, variant, largeCheck, ownPlaceholder }: OwnInputProps) => {
const theme = useTheme();
+
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const { updateOwnVariant } = useQuizViewStore((state) => state);
@@ -102,6 +104,7 @@ export const VariantItem = ({
const { settings } = useQuizSettings();
const theme = useTheme();
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
+ const { t } = useTranslation();
const sendVariant = async (event: MouseEvent) => {
event.preventDefault();
@@ -195,7 +198,7 @@ export const VariantItem = ({
top: "-23px",
}}
>
- Введите свой ответ
+ {t("Enter your answer")}
{
const theme = useTheme();
-
+ const { t } = useTranslation();
const { settings } = useQuizSettings();
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
@@ -127,7 +128,7 @@ export const VarimgVariant = ({
pl: "15px",
}}
>
- Введите свой ответ
+ {t("Enter your answer")}
{
const answers = useQuizViewStore((state) => state.answers);
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const updateOwnVariant = useQuizViewStore((state) => state.updateOwnVariant);
+ const { t } = useTranslation();
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
@@ -156,9 +158,9 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
) : currentQuestion.content.replText !== " " && currentQuestion.content.replText.length > 0 ? (
currentQuestion.content.replText
) : variant?.extendedText || isMobile ? (
- "Выберите вариант ответа ниже"
+ t("Select an answer option below")
) : (
- "Выберите вариант ответа слева"
+ t("Select an answer option on the left")
)}
diff --git a/lib/components/ViewPublicationPage/tools/NextButton.tsx b/lib/components/ViewPublicationPage/tools/NextButton.tsx
index 006c68d..f109ec0 100644
--- a/lib/components/ViewPublicationPage/tools/NextButton.tsx
+++ b/lib/components/ViewPublicationPage/tools/NextButton.tsx
@@ -1,6 +1,7 @@
import { useQuizSettings } from "@contexts/QuizDataContext";
import { Button } from "@mui/material";
import { quizThemes } from "@utils/themes/Publication/themePublication";
+import { useTranslation } from "react-i18next";
interface Props {
isNextButtonEnabled: boolean;
@@ -9,6 +10,7 @@ interface Props {
export default function NextButton({ isNextButtonEnabled, moveToNextQuestion }: Props) {
const { settings } = useQuizSettings();
+ const { t } = useTranslation();
return (
);
}
diff --git a/lib/components/ViewPublicationPage/tools/PrevButton.tsx b/lib/components/ViewPublicationPage/tools/PrevButton.tsx
index 80621e8..1f77569 100644
--- a/lib/components/ViewPublicationPage/tools/PrevButton.tsx
+++ b/lib/components/ViewPublicationPage/tools/PrevButton.tsx
@@ -2,6 +2,7 @@ import { Button, useTheme } from "@mui/material";
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useQuizSettings } from "@contexts/QuizDataContext";
+import { useTranslation } from "react-i18next";
interface Props {
isPreviousButtonEnabled: boolean;
@@ -12,6 +13,7 @@ export default function PrevButton({ isPreviousButtonEnabled, moveToPrevQuestion
const theme = useTheme();
const { settings } = useQuizSettings();
const isMobileMini = useRootContainerSize() < 382;
+ const { t } = useTranslation();
return (
);
}
diff --git a/lib/components/ViewPublicationPage/tools/fileUpload.ts b/lib/components/ViewPublicationPage/tools/fileUpload.ts
index af24ccb..a96ac01 100644
--- a/lib/components/ViewPublicationPage/tools/fileUpload.ts
+++ b/lib/components/ViewPublicationPage/tools/fileUpload.ts
@@ -4,15 +4,15 @@ export const MAX_FILE_SIZE = 419430400;
export const UPLOAD_FILE_DESCRIPTIONS_MAP = {
picture: {
- title: "Добавить изображение",
- description: "Принимает изображения",
+ title: "Add image",
+ description: "Accepts images",
},
video: {
- title: "Добавить видео",
- description: "Принимает .mp4 и .mov формат — максимум 50мб",
+ title: "Add video",
+ description: "Accepts .mp4 and .mov format - maximum 50mb",
},
- audio: { title: "Добавить аудиофайл", description: "Принимает аудиофайлы" },
- document: { title: "Добавить документ", description: "Принимает документы" },
+ audio: { title: "Add audio file", description: "Accepts audio files" },
+ document: { title: "Add document", description: "Accepts documents" },
} as const satisfies Record;
export const ACCEPT_SEND_FILE_TYPES_MAP = {
diff --git a/package.json b/package.json
index 3a18579..8d60a60 100755
--- a/package.json
+++ b/package.json
@@ -94,9 +94,13 @@
"country-flag-emoji-polyfill": "^0.1.8",
"current-device": "^0.10.2",
"hex-rgb": "^5.0.0",
+ "i18next": "^25.0.0",
+ "i18next-browser-languagedetector": "^8.0.5",
+ "i18next-http-backend": "^3.0.2",
"mobile-detect": "^1.4.5",
"mui-tel-input": "^5.1.2",
"react-helmet-async": "^2.0.5",
+ "react-i18next": "^15.4.1",
"react-imask": "^7.6.0",
"react-phone-number-input": "^3.4.1"
},
diff --git a/public/locales/en.json b/public/locales/en.json
new file mode 100644
index 0000000..9c2c720
--- /dev/null
+++ b/public/locales/en.json
@@ -0,0 +1,55 @@
+{
+ "quiz is inactive": "Quiz is inactive",
+ "no questions found": "No questions found",
+ "quiz is empty": "Quiz is empty",
+ "quiz already completed": "You've already completed this quiz",
+ "no quiz id": "Missing quiz ID",
+ "quiz data is null": "No quiz parameters were provided",
+ "invalid request data": "This quiz doesn't exist",
+ "default message": "Something went wrong",
+ "The request could not be sent": "Request could not be sent",
+ "The number of points could not be sent": "Points could not be submitted",
+ "Your result": "Your result",
+ "Your points": "Your points",
+ "of": "of",
+ "View answers": "View answers",
+ "Find out more": "Learn more",
+ "Go to website": "Go to website",
+ "Question title": "Question title",
+ "Question without a title": "Untitled question",
+ "Your answer": "Your answer",
+ "Add image": "Add image",
+ "Accepts images": "Accepts images",
+ "Add video": "Add video",
+ "Accepts .mp4 and .mov format - maximum 50mb": "Accepts .mp4 and .mov format - maximum 50MB",
+ "Add audio file": "Add audio file",
+ "Accepts audio files": "Accepts audio files",
+ "Add document": "Add document",
+ "Accepts documents": "Accepts documents",
+ "Next": "Next",
+ "Prev": "Previous",
+ "From": "From",
+ "До": "To",
+ "Enter your answer": "Enter your answer",
+ "Incorrect file type selected": "Invalid file type selected",
+ "File is too big. Maximum size is 50 MB": "File is too large. Maximum size is 50 MB",
+ "Acceptable file extensions": "Allowed file extensions",
+ "You have uploaded": "You've uploaded",
+ "The answer was not counted": "Answer wasn't counted",
+ "Select an answer option below": "Select an answer option below",
+ "Select an answer option on the left": "Select an answer option on the left",
+ "Fill out the form to receive your test results": "Fill out the form to receive your test results",
+ "Enter": "Enter",
+ "Name": "Name",
+ "Phone number": "Phone number",
+ "Last name": "Last name",
+ "Address": "Address",
+ "Incorrect email entered": "Invalid email entered",
+ "Please fill in the fields": "Please fill in the fields",
+ "Please try again later": "Please try again later",
+ "Regulation on the processing of personal data": "Personal Data Processing Regulation",
+ "Privacy Policy": "Privacy Policy",
+ "familiarized": "acknowledged",
+ "and": "and",
+ "Get results": "Get results"
+}
diff --git a/public/locales/ru.json b/public/locales/ru.json
new file mode 100644
index 0000000..0ca5e4c
--- /dev/null
+++ b/public/locales/ru.json
@@ -0,0 +1,55 @@
+{
+ "quiz is inactive": "Квиз не активирован",
+ "no questions found": "Нет созданных вопросов",
+ "quiz is empty": "Квиз пуст",
+ "quiz already completed": "Вы уже прошли этот опрос",
+ "no quiz id": "Отсутствует id квиза",
+ "quiz data is null": "Не были переданы параметры квиза",
+ "invalid request data": "Такого квиза не существует",
+ "default message": "Что-то пошло не так",
+ "The request could not be sent": "Заявка не может быть отправлена",
+ "The number of points could not be sent": "Количество баллов не может быть отправлено",
+ "Your result": "Ваш результат",
+ "Your points": "Ваши баллы",
+ "of": "из",
+ "View answers": "Посмотреть ответы",
+ "Find out more": "Узнать подробнее",
+ "Go to website": "Перейти на сайт",
+ "Question title": "Заголовок вопроса",
+ "Question without a title": "Вопрос без названия",
+ "Your answer": "Ваш ответ",
+ "Add image": "Добавить изображение",
+ "Accepts images": "Принимает изображения",
+ "Add video": "Добавить видео",
+ "Accepts .mp4 and .mov format - maximum 50mb": "Принимает .mp4 и .mov формат — максимум 50мб",
+ "Add audio file": "Добавить аудиофайл",
+ "Accepts audio files": "Принимает аудиофайлы",
+ "Add document": "Добавить документ",
+ "Accepts documents": "Принимает документы",
+ "Next": "Далее",
+ "Prev": "Назад",
+ "From": "От",
+ "До": "До",
+ "Enter your answer": "Введите свой ответ",
+ "Incorrect file type selected": "Выбран некорректный тип файла",
+ "File is too big. Maximum size is 50 MB": "Файл слишком большой. Максимальный размер 50 МБ",
+ "Acceptable file extensions": "Допустимые расширения файлов",
+ "You have uploaded": "Вы загрузили",
+ "The answer was not counted": "Ответ не был засчитан",
+ "Select an answer option below": "Выберите вариант ответа ниже",
+ "Select an answer option on the left": "Выберите вариант ответа слева",
+ "Fill out the form to receive your test results": "Заполните форму, чтобы получить результаты теста",
+ "Enter": "Введите",
+ "Name": "Имя",
+ "Phone number": "Номер телефона",
+ "Last name": "Фамилия",
+ "Address": "Адрес",
+ "Incorrect email entered": "Введена некорректная почта",
+ "Please fill in the fields": "Пожалуйста, заполните поля",
+ "Please try again later": "повторите попытку позже",
+ "Regulation on the processing of personal data": "Положением об обработке персональных данных",
+ "Privacy Policy": "Политикой конфиденциальности",
+ "familiarized": "ознакомлен",
+ "and": "и",
+ "Get results": "Получить результаты"
+}
diff --git a/public/locales/uz.json b/public/locales/uz.json
new file mode 100644
index 0000000..2b41905
--- /dev/null
+++ b/public/locales/uz.json
@@ -0,0 +1,55 @@
+{
+ "quiz is inactive": "Test faol emas",
+ "no questions found": "Savollar topilmadi",
+ "quiz is empty": "Test boʻsh",
+ "quiz already completed": "Siz bu testni allaqachon topshirgansiz",
+ "no quiz id": "Test IDsi yoʻq",
+ "quiz data is null": "Test parametrlari yuborilmagan",
+ "invalid request data": "Bunday test mavjud emas",
+ "default message": "Xatolik yuz berdi",
+ "The request could not be sent": "Soʻrov yuborib boʻlmadi",
+ "The number of points could not be sent": "Ballar yuborib boʻlmadi",
+ "Your result": "Sizning natijangiz",
+ "Your points": "Sizning ballaringiz",
+ "of": "/",
+ "View answers": "Javoblarni koʻrish",
+ "Find out more": "Batafsil maʼlumot",
+ "Go to website": "Veb-saytga oʻtish",
+ "Question title": "Savol sarlavhasi",
+ "Question without a title": "Sarlavhasiz savol",
+ "Your answer": "Sizning javobingiz",
+ "Add image": "Rasm qoʻshish",
+ "Accepts images": "Rasmlarni qabul qiladi",
+ "Add video": "Video qoʻshish",
+ "Accepts .mp4 and .mov format - maximum 50mb": ".mp4 va .mov formatlarini qabul qiladi - maksimal 50MB",
+ "Add audio file": "Audio fayl qoʻshish",
+ "Accepts audio files": "Audio fayllarni qabul qiladi",
+ "Add document": "Hujjat qoʻshish",
+ "Accepts documents": "Hujjatlarni qabul qiladi",
+ "Next": "Keyingi",
+ "Prev": "Oldingi",
+ "From": "Dan",
+ "До": "Gacha",
+ "Enter your answer": "Javobingizni kiriting",
+ "Incorrect file type selected": "Notoʻgʻri fayl turi tanlandi",
+ "File is too big. Maximum size is 50 MB": "Fayl juda katta. Maksimal hajmi 50 MB",
+ "Acceptable file extensions": "Qabul qilinadigan fayl kengaytmalari",
+ "You have uploaded": "Siz yuklagansiz",
+ "The answer was not counted": "Javob hisobga olinmadi",
+ "Select an answer option below": "Quyidagi javob variantlaridan birini tanlang",
+ "Select an answer option on the left": "Chapdagi javob variantlaridan birini tanlang",
+ "Fill out the form to receive your test results": "Test natijalaringizni olish uchun shaklni toʻldiring",
+ "Enter": "Kiriting",
+ "Name": "Ism",
+ "Phone number": "Telefon raqami",
+ "Last name": "Familiya",
+ "Address": "Manzil",
+ "Incorrect email entered": "Notoʻgʻri elektron pochta kiritildi",
+ "Please fill in the fields": "Iltimos, maydonlarni toʻldiring",
+ "Please try again later": "Iltimos, keyinroq urinib koʻring",
+ "Regulation on the processing of personal data": "Shaxsiy maʼlumotlarni qayta ishlash qoidalari",
+ "Privacy Policy": "Maxfiylik siyosati",
+ "familiarized": "tanishdim",
+ "and": "va",
+ "Get results": "Natijalarni olish"
+}
diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts
new file mode 100644
index 0000000..d757bcf
--- /dev/null
+++ b/src/i18n/i18n.ts
@@ -0,0 +1,63 @@
+import i18n from "i18next";
+import { initReactI18next } from "react-i18next";
+import Backend from "i18next-http-backend";
+
+// 1. Функция для принудительного определения языка из URL
+const getLanguageFromURL = (): string => {
+ const path = window.location.pathname;
+
+ // Регулярка для /en/ /ru/ /uz/ в начале пути
+ const langMatch = path.match(/^\/(en|ru|uz)(\/|$)/i);
+
+ if (langMatch) {
+ console.log("Язык из URL:", langMatch[1]);
+ return langMatch[1].toLowerCase();
+ }
+
+ console.log('Язык не указан в URL, используем "ru"');
+ return "ru"; // Жёсткий фолбэк
+};
+
+// 2. Конфиг с полной отладкой
+i18n
+ .use(Backend)
+ .use(initReactI18next)
+ .init({
+ lng: getLanguageFromURL(), // Принудительно из URL
+ fallbackLng: "ru",
+ supportedLngs: ["en", "ru", "uz"],
+ debug: true,
+ interpolation: {
+ escapeValue: false,
+ },
+ backend: {
+ loadPath: "/locales/{{lng}}.json",
+ allowMultiLoading: false,
+ },
+ react: {
+ useSuspense: false, // Отключаем для совместимости с React 18
+ },
+ detection: {
+ order: ["path"], // Только из URL
+ lookupFromPathIndex: 0,
+ caches: [], // Не использовать localStorage
+ },
+ })
+ .then(() => {
+ console.log("i18n инициализирован! Текущий язык:", i18n.language);
+ console.log("Загруженные переводы:", i18n.store.data);
+ })
+ .catch((err) => {
+ console.error("Ошибка i18n:", err);
+ });
+
+// 3. Логирование всех событий
+i18n.on("languageChanged", (lng) => {
+ console.log("Язык изменён на:", lng);
+});
+
+i18n.on("failedLoading", (lng, ns, msg) => {
+ console.error(`Ошибка загрузки ${lng}.json:`, msg);
+});
+
+export default i18n;
diff --git a/src/main.tsx b/src/main.tsx
index 197859c..ea1343f 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,7 +1,8 @@
import { createRoot } from "react-dom/client";
-import { RouteObject, RouterProvider, createBrowserRouter } from "react-router-dom";
+import { RouteObject, RouterProvider, createBrowserRouter, useParams } from "react-router-dom";
import App from "./App";
-import { StrictMode, lazy } from "react";
+import { StrictMode, lazy, useEffect } from "react";
+import "./i18n/i18n";
const routes: RouteObject[] = [
{
diff --git a/tsconfig.json b/tsconfig.json
index 4f90474..0748996 100755
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -23,7 +23,8 @@
"@api/*": ["./lib/api/*"],
"@model/*": ["./lib/model/*"],
"@utils/*": ["./lib/utils/*"],
- "@contexts/*": ["./lib/contexts/*"]
+ "@contexts/*": ["./lib/contexts/*"],
+ "@locales/*": ["./lib/locales/*"]
}
},
"include": ["lib", "src"],
diff --git a/vite.config.ts b/vite.config.ts
index ee76c62..7ea4b17 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -11,6 +11,7 @@ export const alias = {
"@model": resolve(__dirname, "./lib/model"),
"@utils": resolve(__dirname, "./lib/utils"),
"@contexts": resolve(__dirname, "./lib/contexts"),
+ "@locales": resolve(__dirname, "./lib/locales"),
};
// https://vitejs.dev/config/
diff --git a/yarn.lock b/yarn.lock
index 10fe9b2..78c6749 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -190,6 +190,13 @@
dependencies:
regenerator-runtime "^0.14.0"
+"@babel/runtime@^7.25.0", "@babel/runtime@^7.26.10":
+ version "7.27.0"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762"
+ integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==
+ dependencies:
+ regenerator-runtime "^0.14.0"
+
"@babel/template@^7.22.15", "@babel/template@^7.24.0":
version "7.24.0"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz"
@@ -1666,6 +1673,13 @@ country-flag-icons@^1.5.11:
resolved "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.11.tgz"
integrity sha512-B+mvFywunkRJs270k7kCBjhogvIA0uNn6GAXv6m2cPn3rrwqZzZVr2gBWcz+Cz7OGVWlcbERlYRIX0S6OGr8Bw==
+cross-fetch@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983"
+ integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==
+ dependencies:
+ node-fetch "^2.6.12"
+
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
@@ -2435,6 +2449,13 @@ hoist-non-react-statics@^3.3.1:
dependencies:
react-is "^16.7.0"
+html-parse-stringify@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
+ integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==
+ dependencies:
+ void-elements "3.1.0"
+
http-signature@~1.3.6:
version "1.3.6"
resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz"
@@ -2459,6 +2480,27 @@ husky@^9.0.11:
resolved "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz"
integrity sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==
+i18next-browser-languagedetector@^8.0.5:
+ version "8.0.5"
+ resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.5.tgz#6cfdc72820457ce95e69a2788a4f837d1d8f4e9d"
+ integrity sha512-OstebRKqKiQw8xEvQF5aRyUujsCatanj7Q9eo5iiH2gJpoXGZ7483ol3sVBwfqbobTQPNH1J+NAyJ1aCQoEC+w==
+ dependencies:
+ "@babel/runtime" "^7.23.2"
+
+i18next-http-backend@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz#7c8daa31aa69679e155ec1f96a37846225bdf907"
+ integrity sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==
+ dependencies:
+ cross-fetch "4.0.0"
+
+i18next@^25.0.0:
+ version "25.0.0"
+ resolved "https://registry.yarnpkg.com/i18next/-/i18next-25.0.0.tgz#ae6b44dea48597e556a126c6f4f10d9c18cc62e2"
+ integrity sha512-POPvwjOPR1GQvRnbikTMPEhQD+ekd186MHE6NtVxl3Lby+gPp0iq60eCqGrY6wfRnp1lejjFNu0EKs1afA322w==
+ dependencies:
+ "@babel/runtime" "^7.26.10"
+
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
@@ -3020,6 +3062,13 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
+node-fetch@^2.6.12:
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
+ integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
+ dependencies:
+ whatwg-url "^5.0.0"
+
node-releases@^2.0.14:
version "2.0.14"
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz"
@@ -3310,6 +3359,14 @@ react-helmet-async@^2.0.5:
react-fast-compare "^3.2.2"
shallowequal "^1.1.0"
+react-i18next@^15.4.1:
+ version "15.4.1"
+ resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.4.1.tgz#33f3e89c2f6c68e2bfcbf9aa59986ad42fe78758"
+ integrity sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==
+ dependencies:
+ "@babel/runtime" "^7.25.0"
+ html-parse-stringify "^3.0.1"
+
react-imask@^7.6.0:
version "7.6.0"
resolved "https://registry.npmjs.org/react-imask/-/react-imask-7.6.0.tgz"
@@ -3777,6 +3834,11 @@ tough-cookie@^4.1.3:
universalify "^0.2.0"
url-parse "^1.5.3"
+tr46@~0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+ integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+
ts-api-utils@^1.0.1:
version "1.3.0"
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz"
@@ -3932,6 +3994,11 @@ vite@^5.0.8:
optionalDependencies:
fsevents "~2.3.3"
+void-elements@3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
+ integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
+
vue-template-compiler@^2.7.14:
version "2.7.16"
resolved "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz"
@@ -3949,6 +4016,19 @@ vue-tsc@^1.8.27:
"@vue/language-core" "1.8.27"
semver "^7.5.4"
+webidl-conversions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+ integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
+
+whatwg-url@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
+ integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
+ dependencies:
+ tr46 "~0.0.3"
+ webidl-conversions "^3.0.0"
+
which@^2.0.1:
version "2.0.2"
resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"