diff --git a/package.json b/package.json index 8d60a60..7ca53f9 100755 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/components/SafeTranslationProvider.tsx b/src/components/SafeTranslationProvider.tsx new file mode 100644 index 0000000..b6ff470 --- /dev/null +++ b/src/components/SafeTranslationProvider.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; + +interface SafeTranslationProviderProps { + children: React.ReactNode; +} + +export const SafeTranslationProvider: React.FC = ({ children }) => { + return <>{children}; +}; + +// Хук для безопасного использования переводов +export const useSafeTranslation = () => { + const { t } = useTranslation(); + + const safeT = (key: string, options?: any) => { + try { + return t(key, options); + } catch (error) { + console.warn("Translation error:", error); + return key; + } + }; + + return { t: safeT }; +}; diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index e4f8067..d1515f1 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -6,7 +6,7 @@ import Backend from "i18next-http-backend"; 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,7 +18,84 @@ 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": "Добавить изображение", + }, + 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", + }, +}; + +// 4. Конфиг с полной отладкой i18n .use(Backend) .use(initReactI18next) @@ -31,7 +108,7 @@ i18n escapeValue: false, }, backend: { - loadPath: "/locales/{{lng}}.json", + loadPath: "/locales/{{lng}}.json", // Единый путь для всех allowMultiLoading: false, }, react: { @@ -44,6 +121,12 @@ i18n }, parseMissingKeyHandler: (key) => { console.warn("Missing translation:", key); + // Ищем в встроенных переводах + const currentLang = i18n.language || "ru"; + const builtIn = builtInTranslations[currentLang as keyof typeof builtInTranslations]; + if (builtIn && builtIn[key as keyof typeof builtIn]) { + return builtIn[key as keyof typeof builtIn]; + } return key; // Вернёт ключ вместо ошибки }, missingKeyHandler: (lngs, ns, key) => { @@ -54,22 +137,62 @@ i18n stack: new Error().stack, // Выведет стек вызовов }); }, + resources: isWidget ? builtInTranslations : undefined, // Встроенные переводы для виджета }) .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) => { + 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 && builtIn[key as keyof typeof builtIn]) { + return builtIn[key as keyof typeof builtIn]; + } + } + return translation; +}; + export default i18n; diff --git a/src/widget.tsx b/src/widget.tsx index 653e35b..36fef27 100644 --- a/src/widget.tsx +++ b/src/widget.tsx @@ -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(); + root.render( + + + + ); }, };