i18n
This commit is contained in:
parent
066e622420
commit
7243ae77f1
@ -77,12 +77,12 @@ function QuizAnswererInner({
|
||||
if (error) return <ApologyPage error={error} />;
|
||||
// if (!data) return <LoadingSkeleton />;
|
||||
quizSettings ??= data;
|
||||
if (!quizSettings) return <ApologyPage error={new Error("Quiz data is null")} />;
|
||||
if (!quizSettings) return <ApologyPage error={new Error("quiz data is null")} />;
|
||||
|
||||
if (quizSettings.questions.length === 1 && quizSettings?.settings.cfg.noStartPage)
|
||||
return <ApologyPage error={new Error("Quiz is empty")} />;
|
||||
// if (quizSettings.questions.length === 1) return <ApologyPage error={new Error("No questions found")} />;
|
||||
if (!quizId) return <ApologyPage error={new Error("No quiz id")} />;
|
||||
return <ApologyPage error={new Error("quiz is empty")} />;
|
||||
// if (quizSettings.questions.length === 1) return <ApologyPage error={new Error("no questions found")} />;
|
||||
if (!quizId) return <ApologyPage error={new Error("no quiz id")} />;
|
||||
|
||||
const quizContainer = (
|
||||
<Box
|
||||
|
@ -1,18 +1,12 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { FallbackProps } from "react-error-boundary";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = Partial<FallbackProps>;
|
||||
|
||||
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 (
|
||||
<Box
|
||||
@ -30,7 +24,7 @@ export const ApologyPage = ({ error }: Props) => {
|
||||
color: "text.primary",
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
{t(message.toLowerCase())}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
@ -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")} `}
|
||||
</Link>
|
||||
 и 
|
||||
 {t("and")} 
|
||||
<Link
|
||||
href={"https://shub.pena.digital/docs/privacy"}
|
||||
target="_blank"
|
||||
>
|
||||
{" "}
|
||||
Политикой конфиденциальности{" "}
|
||||
{`${t("Privacy Policy")} `}
|
||||
</Link>
|
||||
 ознакомлен
|
||||
 {t("familiarized")}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@ -315,7 +317,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{settings.cfg.formContact?.button || "Получить результаты"}
|
||||
{settings.cfg.formContact?.button || t("Get results")}
|
||||
</Button>
|
||||
</Box>
|
||||
{show_badge && (
|
||||
|
@ -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<ContactTextBlockProps> = ({ settings }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 850;
|
||||
const isTablet = useRootContainerSize() < 1000;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -45,7 +47,7 @@ export const ContactTextBlock: FC<ContactTextBlockProps> = ({ settings }) => {
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{settings.cfg.formContact.title || "Заполните форму, чтобы получить результаты теста"}
|
||||
{settings.cfg.formContact.title || t("Fill out the form to receive your test results")}
|
||||
</Typography>
|
||||
{settings.cfg.formContact.desc && (
|
||||
<Typography
|
||||
|
@ -7,6 +7,7 @@ import { Dispatch, SetStateAction } from "react";
|
||||
import { CustomInput } from "@/components/ViewPublicationPage/ContactForm/CustomInput/CustomInput.tsx";
|
||||
import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon.tsx";
|
||||
import PhoneInput from "react-phone-number-input";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type InputsProps = {
|
||||
name: string;
|
||||
@ -38,6 +39,7 @@ export const Inputs = ({
|
||||
crutch,
|
||||
}: InputsProps) => {
|
||||
const { settings } = useQuizSettings();
|
||||
const { t } = useTranslation();
|
||||
const FC = settings.cfg.formContact.fields;
|
||||
|
||||
if (!FC) return null;
|
||||
@ -45,8 +47,8 @@ export const Inputs = ({
|
||||
<CustomInput
|
||||
onChange={({ target }) => 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 = ({
|
||||
<CustomInput
|
||||
onChange={({ target }) => 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 = ({
|
||||
<CustomInput
|
||||
onChange={({ target }) => 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}
|
||||
/>
|
||||
);
|
||||
|
@ -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 (
|
||||
<Box
|
||||
@ -41,7 +43,7 @@ export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
|
||||
{stepNumber !== null && (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Вопрос {stepNumber} из {questionsAmount}
|
||||
{t("Step")} {stepNumber} {t("of")} {questionsAmount}
|
||||
</Typography>
|
||||
<Stepper
|
||||
activeStep={stepNumber}
|
||||
|
@ -4,11 +4,13 @@ import { Box, Typography, useTheme } from "@mui/material";
|
||||
import { useQuizSettings } from "@/contexts/QuizDataContext";
|
||||
import { useQuizViewStore } from "@/stores/quizView";
|
||||
import { AnyTypedQuizQuestion, QuizQuestionVariant } from "@/index";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const PointSystemResultList = () => {
|
||||
const theme = useTheme();
|
||||
const { questions } = useQuizSettings();
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const questionsWothoutResult = questions.filter<QuizQuestionVariant>(
|
||||
(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")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
@ -81,7 +83,7 @@ export const PointSystemResultList = () => {
|
||||
color: theme.palette.grey[500],
|
||||
}}
|
||||
>
|
||||
Ваш ответ:
|
||||
{t("Your answer")}:
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useQuizSettings } from "@/contexts/QuizDataContext";
|
||||
import { AnyTypedQuizQuestion } from "@/model/questionTypes/shared";
|
||||
import { Box, FormControl, MenuItem, Select as MuiSelect, useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
selectedQuestion: AnyTypedQuizQuestion;
|
||||
@ -10,6 +11,7 @@ interface Props {
|
||||
export default function QuestionSelect({ selectedQuestion, setQuestion }: Props) {
|
||||
const theme = useTheme();
|
||||
const { questions, preview } = useQuizSettings();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!preview) return null;
|
||||
|
||||
@ -35,7 +37,7 @@ export default function QuestionSelect({ selectedQuestion, setQuestion }: Props)
|
||||
id="category-select"
|
||||
variant="outlined"
|
||||
value={selectedQuestion.id}
|
||||
placeholder="Заголовок вопроса"
|
||||
placeholder={t("Question title")}
|
||||
onChange={({ target }) => {
|
||||
setQuestion(target.value);
|
||||
}}
|
||||
|
@ -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")}:
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
@ -248,7 +250,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Ваши баллы
|
||||
{t("Your points")}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
@ -257,7 +259,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{pointsSum} из {questions.filter((e) => e.type != "result").length}
|
||||
{pointsSum} {t("of")} {questions.filter((e) => e.type != "result").length}
|
||||
</Typography>
|
||||
<TextAccordion
|
||||
headerText={
|
||||
@ -269,7 +271,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
Посмотреть ответы
|
||||
{t("View answers")}
|
||||
</Typography>
|
||||
}
|
||||
sx={{
|
||||
@ -339,7 +341,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
|
||||
height: "50px",
|
||||
}}
|
||||
>
|
||||
{resultQuestion.content.hint.text || "Узнать подробнее"}
|
||||
{resultQuestion.content.hint.text || t("Find out more")}
|
||||
</Button>
|
||||
)}
|
||||
{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")}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
@ -164,20 +164,6 @@ export const StartPageViewPublication = () => {
|
||||
: "#FFFFFF",
|
||||
}}
|
||||
/>
|
||||
{/*<Typography*/}
|
||||
{/* sx={{*/}
|
||||
{/* fontSize: "13px",*/}
|
||||
{/* color:*/}
|
||||
{/* settings.cfg.startpageType === "expanded"*/}
|
||||
{/* ? "#F5F7FF"*/}
|
||||
{/* : quizThemes[settings.cfg.theme].isLight*/}
|
||||
{/* ? "#4D4D4D"*/}
|
||||
{/* : "#F5F7FF",*/}
|
||||
{/* whiteSpace: "nowrap",*/}
|
||||
{/* }}*/}
|
||||
{/*>*/}
|
||||
{/* Сделано на PenaQuiz*/}
|
||||
{/*</Typography>*/}
|
||||
</Box>
|
||||
);
|
||||
|
||||
|
@ -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) => {
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<span style={{ marginLeft: "25px", color: theme.palette.text.primary }}>От</span>
|
||||
<span style={{ marginLeft: "25px", color: theme.palette.text.primary }}>{t("From")}</span>
|
||||
<DateCalendar
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
@ -74,7 +76,7 @@ export default ({ currentQuestion }: DateProps) => {
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<span style={{ marginLeft: "25px", color: theme.palette.text.primary }}>До</span>
|
||||
<span style={{ marginLeft: "25px", color: theme.palette.text.primary }}>{t("До")}</span>
|
||||
<DateCalendar
|
||||
minDate={today}
|
||||
sx={{
|
||||
|
@ -17,6 +17,7 @@ import RadioIcon from "@ui_kit/RadioIcon";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
|
||||
import type { MouseEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
polyfillCountryFlagEmojis();
|
||||
|
||||
@ -108,6 +109,7 @@ export const EmojiVariant = ({
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
@ -181,7 +183,7 @@ export const EmojiVariant = ({
|
||||
pl: "15px",
|
||||
}}
|
||||
>
|
||||
Введите свой ответ
|
||||
{t("Enter your answer")}
|
||||
</Typography>
|
||||
)}
|
||||
<FormControlLabel
|
||||
|
@ -18,6 +18,7 @@ import UploadIcon from "@icons/UploadIcon";
|
||||
|
||||
import type { QuizQuestionFile } from "@model/questionTypes/file";
|
||||
import type { ModalWarningType } from "./index";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type UploadFileProps = {
|
||||
currentQuestion: QuizQuestionFile;
|
||||
@ -30,6 +31,7 @@ export const UploadFile = ({ currentQuestion, setModalWarningType, isSending, se
|
||||
const { quizId, preview } = useQuizSettings();
|
||||
const [isDropzoneHighlighted, setIsDropzoneHighlighted] = useState<boolean>(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);
|
||||
|
@ -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 (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
|
||||
<Typography color={theme.palette.text.primary}>Вы загрузили:</Typography>
|
||||
<Typography color={theme.palette.text.primary}>{t("You have uploaded")}:</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
padding: "5px 5px 5px 16px",
|
||||
|
@ -10,6 +10,7 @@ import { useQuizViewStore } from "@stores/quizView";
|
||||
import { ACCEPT_SEND_FILE_TYPES_MAP } from "@/components/ViewPublicationPage/tools/fileUpload";
|
||||
|
||||
import type { QuizQuestionFile } from "@model/questionTypes/file";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type ModalWarningType = "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null;
|
||||
|
||||
@ -101,18 +102,19 @@ export const File = ({ currentQuestion }: FileProps) => {
|
||||
};
|
||||
|
||||
const CurrentModal = ({ status }: { status: ModalWarningType }) => {
|
||||
const { t } = useTranslation();
|
||||
switch (status) {
|
||||
case null:
|
||||
return null;
|
||||
case "errorType":
|
||||
return <Typography>Выбран некорректный тип файла</Typography>;
|
||||
return <Typography>{t("Incorrect file type selected")}</Typography>;
|
||||
case "errorSize":
|
||||
return <Typography>Файл слишком большой. Максимальный размер 50 МБ</Typography>;
|
||||
return <Typography>{t("File is too big. Maximum size is 50 MB")}</Typography>;
|
||||
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<Typography>Допустимые расширения файлов:</Typography>
|
||||
<Typography>{t("Acceptable file extensions")}:</Typography>
|
||||
<Typography>{ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}</Typography>
|
||||
</>
|
||||
);
|
||||
|
@ -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")}
|
||||
</Typography>
|
||||
)}
|
||||
<FormControlLabel
|
||||
|
@ -17,6 +17,7 @@ import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import type { FC, MouseEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
|
||||
|
||||
@ -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<HTMLLabelElement>) => {
|
||||
event.preventDefault();
|
||||
@ -195,7 +198,7 @@ export const VariantItem = ({
|
||||
top: "-23px",
|
||||
}}
|
||||
>
|
||||
Введите свой ответ
|
||||
{t("Enter your answer")}
|
||||
</Typography>
|
||||
<OwnInput
|
||||
questionId={questionId}
|
||||
|
@ -16,6 +16,7 @@ import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { type MouseEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
type VarimgVariantProps = {
|
||||
@ -103,7 +104,7 @@ export const VarimgVariant = ({
|
||||
answer,
|
||||
}: VarimgVariantProps) => {
|
||||
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")}
|
||||
</Typography>
|
||||
|
||||
<FormControlLabel
|
||||
|
@ -10,6 +10,7 @@ import BlankImage from "@icons/BlankImage";
|
||||
|
||||
import type { QuizQuestionVarImg } from "@model/questionTypes/varimg";
|
||||
import moment from "moment";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type VarimgProps = {
|
||||
currentQuestion: QuizQuestionVarImg;
|
||||
@ -20,6 +21,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
|
||||
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")
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -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 (
|
||||
<Button
|
||||
@ -23,7 +25,7 @@ export default function NextButton({ isNextButtonEnabled, moveToNextQuestion }:
|
||||
}}
|
||||
onClick={moveToNextQuestion}
|
||||
>
|
||||
Далее →
|
||||
{t("Next")} →
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@ -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 (
|
||||
<Button
|
||||
disabled={!isPreviousButtonEnabled}
|
||||
@ -35,7 +37,7 @@ export default function PrevButton({ isPreviousButtonEnabled, moveToPrevQuestion
|
||||
}}
|
||||
onClick={moveToPrevQuestion}
|
||||
>
|
||||
{isMobileMini ? "←" : "← Назад"}
|
||||
{isMobileMini ? "←" : `← ${t("Prev")}`}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@ -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<UploadFileType, { title: string; description: string }>;
|
||||
|
||||
export const ACCEPT_SEND_FILE_TYPES_MAP = {
|
||||
|
@ -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"
|
||||
},
|
||||
|
55
public/locales/en.json
Normal file
55
public/locales/en.json
Normal file
@ -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"
|
||||
}
|
55
public/locales/ru.json
Normal file
55
public/locales/ru.json
Normal file
@ -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": "Получить результаты"
|
||||
}
|
55
public/locales/uz.json
Normal file
55
public/locales/uz.json
Normal file
@ -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"
|
||||
}
|
63
src/i18n/i18n.ts
Normal file
63
src/i18n/i18n.ts
Normal file
@ -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;
|
@ -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[] = [
|
||||
{
|
||||
|
@ -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"],
|
||||
|
@ -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/
|
||||
|
80
yarn.lock
80
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"
|
||||
|
Loading…
Reference in New Issue
Block a user