добавление переводов в зависимости юзеров
This commit is contained in:
parent
b09e587220
commit
56ccd1dacb
@ -87,7 +87,11 @@
|
|||||||
"react-router-dom": "^6.21.3",
|
"react-router-dom": "^6.21.3",
|
||||||
"swr": "^2.2.4",
|
"swr": "^2.2.4",
|
||||||
"use-debounce": "^9.0.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": {
|
"dependencies": {
|
||||||
"bowser": "1.9.4",
|
"bowser": "1.9.4",
|
||||||
@ -97,10 +101,10 @@
|
|||||||
"i18next": "^25.0.0",
|
"i18next": "^25.0.0",
|
||||||
"i18next-browser-languagedetector": "^8.0.5",
|
"i18next-browser-languagedetector": "^8.0.5",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
|
"react-i18next": "^15.4.1",
|
||||||
"mobile-detect": "^1.4.5",
|
"mobile-detect": "^1.4.5",
|
||||||
"mui-tel-input": "^5.1.2",
|
"mui-tel-input": "^5.1.2",
|
||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-i18next": "^15.4.1",
|
|
||||||
"react-imask": "^7.6.0",
|
"react-imask": "^7.6.0",
|
||||||
"react-phone-number-input": "^3.4.1"
|
"react-phone-number-input": "^3.4.1"
|
||||||
},
|
},
|
||||||
|
26
src/components/SafeTranslationProvider.tsx
Normal file
26
src/components/SafeTranslationProvider.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface SafeTranslationProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SafeTranslationProvider: React.FC<SafeTranslationProviderProps> = ({ 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 };
|
||||||
|
};
|
141
src/i18n/i18n.ts
141
src/i18n/i18n.ts
@ -6,7 +6,7 @@ import Backend from "i18next-http-backend";
|
|||||||
const getLanguageFromURL = (): string => {
|
const getLanguageFromURL = (): string => {
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
|
|
||||||
// Регулярка для /en/ /ru/ /uz/ в начале пути
|
// Регулярка для /en/ /ru/ /uz в начале пути
|
||||||
const langMatch = path.match(/^\/(en|ru|uz)(\/|$)/i);
|
const langMatch = path.match(/^\/(en|ru|uz)(\/|$)/i);
|
||||||
|
|
||||||
if (langMatch) {
|
if (langMatch) {
|
||||||
@ -18,7 +18,84 @@ const getLanguageFromURL = (): string => {
|
|||||||
return "ru"; // Жёсткий фолбэк
|
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
|
i18n
|
||||||
.use(Backend)
|
.use(Backend)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
@ -31,7 +108,7 @@ i18n
|
|||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
},
|
},
|
||||||
backend: {
|
backend: {
|
||||||
loadPath: "/locales/{{lng}}.json",
|
loadPath: "/locales/{{lng}}.json", // Единый путь для всех
|
||||||
allowMultiLoading: false,
|
allowMultiLoading: false,
|
||||||
},
|
},
|
||||||
react: {
|
react: {
|
||||||
@ -44,6 +121,12 @@ i18n
|
|||||||
},
|
},
|
||||||
parseMissingKeyHandler: (key) => {
|
parseMissingKeyHandler: (key) => {
|
||||||
console.warn("Missing translation:", 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; // Вернёт ключ вместо ошибки
|
return key; // Вернёт ключ вместо ошибки
|
||||||
},
|
},
|
||||||
missingKeyHandler: (lngs, ns, key) => {
|
missingKeyHandler: (lngs, ns, key) => {
|
||||||
@ -54,22 +137,62 @@ i18n
|
|||||||
stack: new Error().stack, // Выведет стек вызовов
|
stack: new Error().stack, // Выведет стек вызовов
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
resources: isWidget ? builtInTranslations : undefined, // Встроенные переводы для виджета
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
//console.log("i18n инициализирован! Текущий язык:", i18n.language);
|
console.log("✅ i18n инициализирован! Текущий язык:", i18n.language);
|
||||||
//console.log("Загруженные переводы:", i18n.store.data);
|
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) => {
|
.catch((err) => {
|
||||||
console.error("Ошибка i18n:", err);
|
console.error("❌ Ошибка i18n:", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Логирование всех событий
|
// 5. Логирование всех событий
|
||||||
i18n.on("languageChanged", (lng) => {
|
i18n.on("languageChanged", (lng) => {
|
||||||
console.log("Язык изменён на:", lng);
|
console.log("🌍 Язык изменён на:", lng);
|
||||||
});
|
});
|
||||||
|
|
||||||
i18n.on("failedLoading", (lng, ns, msg) => {
|
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;
|
export default i18n;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import QuizAnswerer from "@/components/QuizAnswerer";
|
import QuizAnswerer from "@/components/QuizAnswerer";
|
||||||
|
import { SafeTranslationProvider } from "./components/SafeTranslationProvider";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
import "./i18n/i18n"; // Инициализация i18n для виджета
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export * from "./widgets";
|
export * from "./widgets";
|
||||||
|
|
||||||
@ -19,7 +21,15 @@ const widget = {
|
|||||||
|
|
||||||
const root = createRoot(element);
|
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