From 31c4db160ed0db0707e9529432ee01b3fa997d16 Mon Sep 17 00:00:00 2001 From: Nastya Date: Tue, 1 Jul 2025 01:29:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=B4?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=B2=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8?= =?UTF-8?q?=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20=D1=8E=D0=B7=D0=B5=D1=80=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewPublicationPage/ApologyPage.tsx | 3 +- package.json | 8 +- src/components/SafeTranslationProvider.tsx | 43 +++ src/i18n/i18n.ts | 279 +++++++++++++++--- src/widget.tsx | 12 +- 5 files changed, 300 insertions(+), 45 deletions(-) create mode 100644 src/components/SafeTranslationProvider.tsx diff --git a/lib/components/ViewPublicationPage/ApologyPage.tsx b/lib/components/ViewPublicationPage/ApologyPage.tsx index a6afa60..ee2d3a5 100644 --- a/lib/components/ViewPublicationPage/ApologyPage.tsx +++ b/lib/components/ViewPublicationPage/ApologyPage.tsx @@ -28,7 +28,8 @@ export const ApologyPage = ({ error }: Props) => { color: "text.primary", }} > - {t(message.toLowerCase())} + ошибка + {/* {t(message.toLowerCase())} */} ); 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..1e8d1a1 --- /dev/null +++ b/src/components/SafeTranslationProvider.tsx @@ -0,0 +1,43 @@ +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 = ({ children }) => { + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + const checkReady = () => { + if (i18n.isInitialized) { + setIsReady(true); + } else { + setTimeout(checkReady, 50); + } + }; + checkReady(); + }, []); + + if (!isReady) { + return
Загрузка...
; + } + + return <>{children}; +}; + +// Хук для безопасного использования переводов +export const useSafeTranslation = () => { + const translation = useTranslation(); + + // Проверяем, что t функция существует + if (!translation.t) { + console.warn("useTranslation вернул undefined, используем globalTranslate"); + return { + t: globalTranslate, + }; + } + + return translation; +}; diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index e4f8067..a24d4f4 100644 --- a/src/i18n/i18n.ts +++ b/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,248 @@ 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": "Добавить изображение", + "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", + "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 = { + 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 && builtIn[key as keyof typeof builtIn]) { + return builtIn[key as keyof typeof builtIn]; + } + return key; // Вернёт ключ вместо ошибки + }, + missingKeyHandler: (lngs: string[], ns: string, key: string) => { + console.error("🚨 Missing i18n key:", { + key, + languages: lngs, + namespace: ns, + stack: new Error().stack, // Выведет стек вызовов + }); + }, + resources: isWidget ? builtInTranslations : undefined, // Встроенные переводы для виджета + initImmediate: isWidget, // Синхронная инициализация для виджета +}; + +// Для виджета не используем 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) => { + 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; +}; + +// 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 && builtIn[key as keyof typeof builtIn]) { + return builtIn[key as keyof typeof builtIn]; + } + + return key; +}; + +// 9. Создаем глобальную функцию для использования в компонентах +if (typeof window !== "undefined") { + (window as any).globalTranslate = globalTranslate; +} + 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( + + + + ); }, };