добавление переводов в зависимости юзеров
This commit is contained in:
parent
b09e587220
commit
ad8c0a86a7
@ -1,16 +1,10 @@
|
||||
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 = error.message || error.response?.data;
|
||||
console.log("message");
|
||||
console.log(message.toLowerCase());
|
||||
const { t } = useTranslation();
|
||||
console.log("t");
|
||||
console.log(t(message.toLowerCase()));
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -28,7 +22,7 @@ export const ApologyPage = ({ error }: Props) => {
|
||||
color: "text.primary",
|
||||
}}
|
||||
>
|
||||
{t(message.toLowerCase())}
|
||||
ккккккккккккккк
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
@ -2,7 +2,7 @@ import { ReactNode } from "react";
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
|
||||
import Stepper from "@ui_kit/Stepper";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslationSafe } from "../../../src/components/SafeTranslationProvider";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
|
||||
type FooterProps = {
|
||||
@ -15,7 +15,7 @@ export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
|
||||
const theme = useTheme();
|
||||
const { questions, settings } = useQuizStore();
|
||||
const questionsAmount = questions.filter(({ type }) => type !== "result").length;
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslationSafe();
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { Button, Skeleton } from "@mui/material";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslationSafe } from "../../../../src/components/SafeTranslationProvider";
|
||||
|
||||
interface Props {
|
||||
isNextButtonEnabled: boolean;
|
||||
@ -10,7 +10,7 @@ interface Props {
|
||||
|
||||
export default function NextButton({ isNextButtonEnabled, moveToNextQuestion }: Props) {
|
||||
const { settings, nextLoading } = useQuizStore();
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslationSafe();
|
||||
|
||||
return nextLoading ? (
|
||||
<Skeleton
|
||||
|
@ -87,7 +87,11 @@
|
||||
"react-router-dom": "^6.21.3",
|
||||
"swr": "^2.2.4",
|
||||
"use-debounce": "^9.0.4",
|
||||
"zustand": "^4.3.8"
|
||||
"zustand": "^4.3.8",
|
||||
"i18next": "^25.0.0",
|
||||
"i18next-browser-languagedetector": "^8.0.5",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"react-i18next": "^15.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bowser": "1.9.4",
|
||||
@ -97,10 +101,10 @@
|
||||
"i18next": "^25.0.0",
|
||||
"i18next-browser-languagedetector": "^8.0.5",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"react-i18next": "^15.4.1",
|
||||
"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"
|
||||
},
|
||||
|
59
src/components/SafeTranslationProvider.tsx
Normal file
59
src/components/SafeTranslationProvider.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import i18n, { globalTranslate } from "../i18n/i18n";
|
||||
|
||||
interface SafeTranslationProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SafeTranslationProvider: React.FC<SafeTranslationProviderProps> = ({ children }) => {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkReady = () => {
|
||||
if (i18n.isInitialized) {
|
||||
setIsReady(true);
|
||||
} else {
|
||||
setTimeout(checkReady, 50);
|
||||
}
|
||||
};
|
||||
checkReady();
|
||||
}, []);
|
||||
|
||||
if (!isReady) {
|
||||
return <div style={{ padding: "20px", textAlign: "center" }}>Загрузка...</div>;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
// Хук для безопасного использования переводов
|
||||
export const useSafeTranslation = () => {
|
||||
const translation = useTranslation();
|
||||
|
||||
// Проверяем, что t функция существует
|
||||
if (!translation.t) {
|
||||
console.warn("useTranslation вернул undefined, используем globalTranslate");
|
||||
return {
|
||||
t: globalTranslate,
|
||||
i18n: translation.i18n,
|
||||
ready: translation.ready,
|
||||
};
|
||||
}
|
||||
|
||||
return translation;
|
||||
};
|
||||
|
||||
// Глобальный хук для замены useTranslation во всех компонентах
|
||||
export const useTranslationSafe = () => {
|
||||
try {
|
||||
const { t } = useTranslation();
|
||||
if (t) {
|
||||
return { t };
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("useTranslation failed, using fallback:", error);
|
||||
}
|
||||
|
||||
return { t: globalTranslate };
|
||||
};
|
299
src/i18n/i18n.ts
299
src/i18n/i18n.ts
@ -2,11 +2,18 @@ import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import Backend from "i18next-http-backend";
|
||||
|
||||
// Расширяем типы для window
|
||||
declare global {
|
||||
interface Window {
|
||||
QuizAnswererWidget?: any;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Функция для принудительного определения языка из URL
|
||||
const getLanguageFromURL = (): string => {
|
||||
const path = window.location.pathname;
|
||||
|
||||
// Регулярка для /en/ /ru/ /uz/ в начале пути
|
||||
// Регулярка для /en/ /ru/ /uz в начале пути
|
||||
const langMatch = path.match(/^\/(en|ru|uz)(\/|$)/i);
|
||||
|
||||
if (langMatch) {
|
||||
@ -18,58 +25,268 @@ const getLanguageFromURL = (): string => {
|
||||
return "ru"; // Жёсткий фолбэк
|
||||
};
|
||||
|
||||
// 2. Конфиг с полной отладкой
|
||||
// 2. Проверяем, работаем ли мы в виджете
|
||||
const isWidget =
|
||||
typeof window !== "undefined" &&
|
||||
(window.location.pathname.includes("widget") ||
|
||||
window.location.pathname.includes("export") ||
|
||||
window.location.href.includes("pub.js") ||
|
||||
// Проверяем, есть ли элемент с id, который обычно используется для виджета
|
||||
document.querySelector('[id*="quiz"], [id*="widget"]') !== null ||
|
||||
// Проверяем, загружен ли виджет через скрипт
|
||||
typeof window.QuizAnswererWidget !== "undefined");
|
||||
|
||||
// 3. Встроенные переводы для виджета
|
||||
const builtInTranslations = {
|
||||
ru: {
|
||||
Next: "Далее",
|
||||
Prev: "Назад",
|
||||
Step: "Шаг",
|
||||
of: "из",
|
||||
"Enter your answer": "Введите ваш ответ",
|
||||
"Fill out the form to receive your test results": "Заполните форму для получения результатов теста",
|
||||
Name: "Имя",
|
||||
"Phone number": "Номер телефона",
|
||||
"Get results": "Получить результаты",
|
||||
"Data sent successfully": "Данные отправлены успешно",
|
||||
"Please fill in the fields": "Пожалуйста, заполните поля",
|
||||
"Incorrect email entered": "Введен некорректный email",
|
||||
"quiz is inactive": "Квиз неактивен",
|
||||
"no questions found": "Вопросы не найдены",
|
||||
"quiz is empty": "Квиз пуст",
|
||||
"quiz already completed": "Квиз уже пройден",
|
||||
"no quiz id": "ID квиза не указан",
|
||||
"quiz data is null": "Данные квиза не предоставлены",
|
||||
"default message": "Что-то пошло не так",
|
||||
Enter: "Введите",
|
||||
Email: "Email",
|
||||
"Last name": "Фамилия",
|
||||
Address: "Адрес",
|
||||
"Your points": "Ваши баллы",
|
||||
"View answers": "Посмотреть ответы",
|
||||
"Incorrect file type selected": "Выбран неправильный тип файла",
|
||||
"File is too big. Maximum size is 50 MB": "Файл слишком большой. Максимальный размер 50 МБ",
|
||||
"Acceptable file extensions": "Допустимые расширения файлов",
|
||||
"Add your image": "Добавить изображение",
|
||||
"Add image": "Добавить изображение",
|
||||
"Accepts images": "Принимает изображения",
|
||||
"Question title": "Название вопроса",
|
||||
"Question without a title": "Вопрос без названия",
|
||||
"Your answer": "Ваш ответ",
|
||||
"select emoji": "выберите эмодзи",
|
||||
"file is too big": "файл слишком большой",
|
||||
"file type is not supported": "тип файла не поддерживается",
|
||||
"The answer was not counted": "Ответ не был засчитан",
|
||||
"Select an answer option below": "Выберите вариант ответа ниже",
|
||||
"Select an answer option on the left": "Выберите вариант ответа слева",
|
||||
"You have uploaded": "Вы загрузили",
|
||||
From: "От",
|
||||
До: "До",
|
||||
"Your result": "Ваш результат",
|
||||
"Find out more": "Узнать больше",
|
||||
"Go to website": "Перейти на сайт",
|
||||
"The request could not be sent": "Запрос не удалось отправить",
|
||||
"Количество баллов не может быть отправлено": "Количество баллов не может быть отправлено",
|
||||
"Regulation on the processing of personal data": "Положение об обработке персональных данных",
|
||||
"Please try again later": "Пожалуйста, попробуйте позже",
|
||||
},
|
||||
en: {
|
||||
Next: "Next",
|
||||
Prev: "Previous",
|
||||
Step: "Step",
|
||||
of: "of",
|
||||
"Enter your answer": "Enter your answer",
|
||||
"Fill out the form to receive your test results": "Fill out the form to receive your test results",
|
||||
Name: "Name",
|
||||
"Phone number": "Phone number",
|
||||
"Get results": "Get results",
|
||||
"Data sent successfully": "Data sent successfully",
|
||||
"Please fill in the fields": "Please fill in the fields",
|
||||
"Incorrect email entered": "Incorrect email entered",
|
||||
"quiz is inactive": "Quiz is inactive",
|
||||
"no questions found": "No questions found",
|
||||
"quiz is empty": "Quiz is empty",
|
||||
"quiz already completed": "Quiz already completed",
|
||||
"no quiz id": "No quiz id",
|
||||
"quiz data is null": "Quiz data is null",
|
||||
"default message": "Something went wrong",
|
||||
Enter: "Enter",
|
||||
Email: "Email",
|
||||
"Last name": "Last name",
|
||||
Address: "Address",
|
||||
"Your points": "Your points",
|
||||
"View answers": "View answers",
|
||||
"Incorrect file type selected": "Incorrect file type selected",
|
||||
"File is too big. Maximum size is 50 MB": "File is too big. Maximum size is 50 MB",
|
||||
"Acceptable file extensions": "Acceptable file extensions",
|
||||
"Add your image": "Add your image",
|
||||
"Add image": "Add image",
|
||||
"Accepts images": "Accepts images",
|
||||
"Question title": "Question title",
|
||||
"Question without a title": "Question without a title",
|
||||
"Your answer": "Your answer",
|
||||
"select emoji": "select emoji",
|
||||
"file is too big": "file is too big",
|
||||
"file type is not supported": "file type is not supported",
|
||||
"The answer was not counted": "The answer was not 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",
|
||||
"You have uploaded": "You have uploaded",
|
||||
From: "From",
|
||||
До: "To",
|
||||
"Your result": "Your result",
|
||||
"Find out more": "Find out more",
|
||||
"Go to website": "Go to website",
|
||||
"The request could not be sent": "The request could not be sent",
|
||||
"Количество баллов не может быть отправлено": "Points cannot be sent",
|
||||
"Regulation on the processing of personal data": "Regulation on the processing of personal data",
|
||||
"Please try again later": "Please try again later",
|
||||
},
|
||||
};
|
||||
|
||||
// 4. Конфиг с полной отладкой
|
||||
const config: any = {
|
||||
lng: getLanguageFromURL(), // Принудительно из URL
|
||||
fallbackLng: "ru",
|
||||
supportedLngs: ["en", "ru", "uz"],
|
||||
debug: true,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
react: {
|
||||
useSuspense: false, // Отключаем для совместимости с React 18
|
||||
},
|
||||
detection: {
|
||||
order: ["path"], // Только из URL
|
||||
lookupFromPathIndex: 0,
|
||||
caches: [], // Не использовать localStorage
|
||||
},
|
||||
parseMissingKeyHandler: (key: string) => {
|
||||
console.warn("Missing translation:", key);
|
||||
// Ищем в встроенных переводах
|
||||
const currentLang = i18n.language || "ru";
|
||||
const builtIn = builtInTranslations[currentLang as keyof typeof builtInTranslations];
|
||||
if (builtIn && key in builtIn) {
|
||||
const fallbackTranslation = (builtIn as any)[key];
|
||||
if (typeof fallbackTranslation === "string") {
|
||||
return fallbackTranslation;
|
||||
}
|
||||
}
|
||||
return key; // Вернёт ключ вместо ошибки
|
||||
},
|
||||
missingKeyHandler: (
|
||||
lngs: readonly string[],
|
||||
ns: string,
|
||||
key: string,
|
||||
fallbackValue: string,
|
||||
updateMissing: boolean,
|
||||
options: any
|
||||
) => {
|
||||
console.error("🚨 Missing i18n key:", {
|
||||
key,
|
||||
languages: lngs,
|
||||
namespace: ns,
|
||||
stack: new Error().stack, // Выведет стек вызовов
|
||||
});
|
||||
},
|
||||
resources: isWidget ? builtInTranslations : undefined, // Встроенные переводы для виджета
|
||||
initImmediate: true, // Всегда синхронная инициализация
|
||||
};
|
||||
|
||||
// Для виджета не используем backend
|
||||
if (!isWidget) {
|
||||
config.backend = {
|
||||
loadPath: "/locales/{{lng}}.json", // Единый путь для всех
|
||||
allowMultiLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
parseMissingKeyHandler: (key) => {
|
||||
console.warn("Missing translation:", key);
|
||||
return key; // Вернёт ключ вместо ошибки
|
||||
},
|
||||
missingKeyHandler: (lngs, ns, key) => {
|
||||
console.error("🚨 Missing i18n key:", {
|
||||
key,
|
||||
languages: lngs,
|
||||
namespace: ns,
|
||||
stack: new Error().stack, // Выведет стек вызовов
|
||||
});
|
||||
},
|
||||
})
|
||||
.init(config)
|
||||
.then(() => {
|
||||
//console.log("i18n инициализирован! Текущий язык:", i18n.language);
|
||||
//console.log("Загруженные переводы:", i18n.store.data);
|
||||
console.log("✅ i18n инициализирован! Текущий язык:", i18n.language);
|
||||
console.log("📚 Загруженные переводы:", Object.keys(i18n.store.data));
|
||||
console.log("🎯 Режим виджета:", isWidget);
|
||||
|
||||
// Если это виджет, принудительно добавляем встроенные переводы
|
||||
if (isWidget) {
|
||||
console.log("🔧 Принудительно добавляю встроенные переводы для виджета");
|
||||
Object.keys(builtInTranslations).forEach((lang) => {
|
||||
i18n.addResourceBundle(
|
||||
lang,
|
||||
"translation",
|
||||
builtInTranslations[lang as keyof typeof builtInTranslations],
|
||||
true,
|
||||
true
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Ошибка i18n:", err);
|
||||
console.error("❌ Ошибка i18n:", err);
|
||||
});
|
||||
|
||||
// 3. Логирование всех событий
|
||||
// 5. Логирование всех событий
|
||||
i18n.on("languageChanged", (lng) => {
|
||||
console.log("Язык изменён на:", lng);
|
||||
console.log("🌍 Язык изменён на:", lng);
|
||||
});
|
||||
|
||||
i18n.on("failedLoading", (lng, ns, msg) => {
|
||||
console.error(`Ошибка загрузки ${lng}.json:`, msg);
|
||||
console.error(`❌ Ошибка загрузки ${lng}.json:`, msg);
|
||||
});
|
||||
|
||||
// 6. Функция для проверки готовности i18n
|
||||
export const isI18nReady = () => {
|
||||
return i18n.isInitialized;
|
||||
};
|
||||
|
||||
// 7. Функция для безопасного перевода
|
||||
export const safeTranslate = (key: string, options?: any): string => {
|
||||
if (!i18n.isInitialized) {
|
||||
console.warn("⚠️ i18n не инициализирован, возвращаю ключ:", key);
|
||||
return key;
|
||||
}
|
||||
|
||||
const translation = i18n.t(key, options);
|
||||
if (translation === key) {
|
||||
// Если перевод не найден, ищем в встроенных переводах
|
||||
const currentLang = i18n.language || "ru";
|
||||
const builtIn = builtInTranslations[currentLang as keyof typeof builtInTranslations];
|
||||
if (builtIn && key in builtIn) {
|
||||
const fallbackTranslation = (builtIn as any)[key];
|
||||
if (typeof fallbackTranslation === "string") {
|
||||
return fallbackTranslation;
|
||||
}
|
||||
}
|
||||
}
|
||||
return String(translation);
|
||||
};
|
||||
|
||||
// 8. Глобальная функция перевода для использования до инициализации i18n
|
||||
export const globalTranslate = (key: string): string => {
|
||||
// Если i18n инициализирован, используем его
|
||||
if (i18n.isInitialized) {
|
||||
return safeTranslate(key);
|
||||
}
|
||||
|
||||
// Иначе используем встроенные переводы
|
||||
const currentLang = getLanguageFromURL();
|
||||
const builtIn = builtInTranslations[currentLang as keyof typeof builtInTranslations];
|
||||
if (builtIn && key in builtIn) {
|
||||
const translation = (builtIn as any)[key];
|
||||
if (typeof translation === "string") {
|
||||
return translation;
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
};
|
||||
|
||||
// 9. Создаем глобальную функцию для использования в компонентах
|
||||
if (typeof window !== "undefined") {
|
||||
(window as any).globalTranslate = globalTranslate;
|
||||
}
|
||||
|
||||
export default i18n;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import QuizAnswerer from "@/components/QuizAnswerer";
|
||||
import { SafeTranslationProvider } from "./components/SafeTranslationProvider";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./i18n/i18n"; // Инициализация i18n для виджета
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export * from "./widgets";
|
||||
|
||||
@ -19,7 +21,15 @@ const widget = {
|
||||
|
||||
const root = createRoot(element);
|
||||
|
||||
root.render(<QuizAnswerer quizId={quizId} changeFaviconAndTitle={changeFaviconAndTitle} disableGlobalCss />);
|
||||
root.render(
|
||||
<SafeTranslationProvider>
|
||||
<QuizAnswerer
|
||||
quizId={quizId}
|
||||
changeFaviconAndTitle={changeFaviconAndTitle}
|
||||
disableGlobalCss
|
||||
/>
|
||||
</SafeTranslationProvider>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user