From 2b36121bb56bd9ce2ea2b2f8bd45631e3062cb30 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Sat, 10 May 2025 14:25:47 +0300 Subject: [PATCH] fix language select to work in client-side prod --- i18n.js | 58 ------------ src/components/Language/LanguageSelector.tsx | 92 +++++++++---------- src/i18n/i18n.ts | 54 +++++++++++ {public => src/i18n}/locales/de/auth.json | 0 {public => src/i18n}/locales/de/core.json | 0 {public => src/i18n}/locales/de/tutorial.json | 0 {public => src/i18n}/locales/en/auth.json | 0 {public => src/i18n}/locales/en/core.json | 0 {public => src/i18n}/locales/en/group.json | 0 {public => src/i18n}/locales/en/tutorial.json | 0 {public => src/i18n}/locales/es/auth.json | 0 {public => src/i18n}/locales/es/core.json | 0 {public => src/i18n}/locales/es/tutorial.json | 0 {public => src/i18n}/locales/fr/auth.json | 0 {public => src/i18n}/locales/fr/core.json | 0 {public => src/i18n}/locales/fr/tutorial.json | 0 {public => src/i18n}/locales/it/auth.json | 0 {public => src/i18n}/locales/it/core.json | 0 {public => src/i18n}/locales/it/group.json | 0 {public => src/i18n}/locales/it/tutorial.json | 0 {public => src/i18n}/locales/ru/auth.json | 0 {public => src/i18n}/locales/ru/core.json | 0 {public => src/i18n}/locales/ru/tutorial.json | 0 src/main.tsx | 2 +- tsconfig.json | 3 +- 25 files changed, 102 insertions(+), 107 deletions(-) delete mode 100644 i18n.js create mode 100644 src/i18n/i18n.ts rename {public => src/i18n}/locales/de/auth.json (100%) rename {public => src/i18n}/locales/de/core.json (100%) rename {public => src/i18n}/locales/de/tutorial.json (100%) rename {public => src/i18n}/locales/en/auth.json (100%) rename {public => src/i18n}/locales/en/core.json (100%) rename {public => src/i18n}/locales/en/group.json (100%) rename {public => src/i18n}/locales/en/tutorial.json (100%) rename {public => src/i18n}/locales/es/auth.json (100%) rename {public => src/i18n}/locales/es/core.json (100%) rename {public => src/i18n}/locales/es/tutorial.json (100%) rename {public => src/i18n}/locales/fr/auth.json (100%) rename {public => src/i18n}/locales/fr/core.json (100%) rename {public => src/i18n}/locales/fr/tutorial.json (100%) rename {public => src/i18n}/locales/it/auth.json (100%) rename {public => src/i18n}/locales/it/core.json (100%) rename {public => src/i18n}/locales/it/group.json (100%) rename {public => src/i18n}/locales/it/tutorial.json (100%) rename {public => src/i18n}/locales/ru/auth.json (100%) rename {public => src/i18n}/locales/ru/core.json (100%) rename {public => src/i18n}/locales/ru/tutorial.json (100%) diff --git a/i18n.js b/i18n.js deleted file mode 100644 index 5277e59..0000000 --- a/i18n.js +++ /dev/null @@ -1,58 +0,0 @@ -import { initReactI18next } from 'react-i18next'; -import HttpBackend from 'i18next-http-backend'; -import LocalStorageBackend from 'i18next-localstorage-backend'; -import HttpApi from 'i18next-http-backend'; -import i18n from 'i18next'; -import LanguageDetector from 'i18next-browser-languagedetector'; - -// Detect environment -const isDev = process.env.NODE_ENV === 'development'; - -// Register custom postProcessor: it capitalizes the first letter of a translation- -// Usage: -// t('greeting', { postProcess: 'capitalize' }) -const capitalize = { - type: 'postProcessor', - name: 'capitalize', - process: (value) => { - return value.charAt(0).toUpperCase() + value.slice(1); - }, -}; - -export const supportedLanguages = { - de: { name: 'Deutsch', flag: '🇩🇪' }, - en: { name: 'English', flag: '🇺🇸' }, - es: { name: 'Español', flag: '🇪🇸' }, - fr: { name: 'Français', flag: '🇫🇷' }, - it: { name: 'Italiano', flag: '🇮🇹' }, - ru: { name: 'Русский', flag: '🇷🇺' }, -}; - -i18n - .use(HttpApi) - .use(LanguageDetector) - .use(initReactI18next) - .use(capitalize) - .init({ - backend: { - backends: [LocalStorageBackend, HttpBackend], - backendOptions: [ - { - expirationTime: 7 * 24 * 60 * 60 * 1000, // 7 days - }, - { - loadPath: '/locales/{{lng}}/{{ns}}.json', - }, - ], - }, - debug: isDev, - fallbackLng: 'en', - interpolation: { - escapeValue: false, - }, - lng: navigator.language, - ns: ['auth', 'core', 'group', 'tutorial'], - supportedLngs: Object.keys(supportedLanguages), - }); - -export default i18n; diff --git a/src/components/Language/LanguageSelector.tsx b/src/components/Language/LanguageSelector.tsx index 751c0a9..ff78246 100644 --- a/src/components/Language/LanguageSelector.tsx +++ b/src/components/Language/LanguageSelector.tsx @@ -1,7 +1,13 @@ -import { useEffect, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { supportedLanguages } from '../../../i18n'; -import { Tooltip, useTheme } from '@mui/material'; +import { supportedLanguages } from '../../i18n/i18n'; +import { + FormControl, + MenuItem, + Select, + Tooltip, + useTheme, +} from '@mui/material'; const LanguageSelector = () => { const { i18n, t } = useTranslation(['core']); @@ -19,20 +25,6 @@ const LanguageSelector = () => { const { name, flag } = supportedLanguages[currentLang] || supportedLanguages['en']; - // Detect clicks outside the component - useEffect(() => { - const handleClickOutside = (event) => { - if (selectorRef.current && !selectorRef.current.contains(event.target)) { - setShowSelect(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - return (
{ position: 'absolute', }} > - - {showSelect ? ( - - ) : ( + {!showSelect && ( + - )} - + + )} + + {showSelect && ( + + + + )}
); }; diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts new file mode 100644 index 0000000..8d267be --- /dev/null +++ b/src/i18n/i18n.ts @@ -0,0 +1,54 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +const capitalize = { + type: 'postProcessor', + name: 'capitalize', + process: (value: string) => value.charAt(0).toUpperCase() + value.slice(1), +}; + +export const supportedLanguages = { + de: { name: 'Deutsch', flag: '🇩🇪' }, + en: { name: 'English', flag: '🇺🇸' }, + es: { name: 'Español', flag: '🇪🇸' }, + fr: { name: 'Français', flag: '🇫🇷' }, + it: { name: 'Italiano', flag: '🇮🇹' }, + ru: { name: 'Русский', flag: '🇷🇺' }, +}; + +// Load all JSON files under locales/**/* +const modules = import.meta.glob('./locales/**/*.json', { + eager: true, +}) as Record; + +// Construct i18n resources object +const resources: Record> = {}; + +for (const path in modules) { + // Path format: './locales/en/core.json' + const match = path.match(/\.\/locales\/([^/]+)\/([^/]+)\.json$/); + if (!match) continue; + + const [, lang, ns] = match; + resources[lang] = resources[lang] || {}; + resources[lang][ns] = modules[path].default; +} + +i18n + .use(initReactI18next) + .use(LanguageDetector) + .use(capitalize as any) + .init({ + resources, + fallbackLng: 'en', + lng: navigator.language, + supportedLngs: Object.keys(supportedLanguages), + ns: ['core', 'auth', 'group', 'tutorial'], + defaultNS: 'core', + interpolation: { escapeValue: false }, + react: { useSuspense: false }, + debug: import.meta.env.MODE === 'development', + }); + +export default i18n; diff --git a/public/locales/de/auth.json b/src/i18n/locales/de/auth.json similarity index 100% rename from public/locales/de/auth.json rename to src/i18n/locales/de/auth.json diff --git a/public/locales/de/core.json b/src/i18n/locales/de/core.json similarity index 100% rename from public/locales/de/core.json rename to src/i18n/locales/de/core.json diff --git a/public/locales/de/tutorial.json b/src/i18n/locales/de/tutorial.json similarity index 100% rename from public/locales/de/tutorial.json rename to src/i18n/locales/de/tutorial.json diff --git a/public/locales/en/auth.json b/src/i18n/locales/en/auth.json similarity index 100% rename from public/locales/en/auth.json rename to src/i18n/locales/en/auth.json diff --git a/public/locales/en/core.json b/src/i18n/locales/en/core.json similarity index 100% rename from public/locales/en/core.json rename to src/i18n/locales/en/core.json diff --git a/public/locales/en/group.json b/src/i18n/locales/en/group.json similarity index 100% rename from public/locales/en/group.json rename to src/i18n/locales/en/group.json diff --git a/public/locales/en/tutorial.json b/src/i18n/locales/en/tutorial.json similarity index 100% rename from public/locales/en/tutorial.json rename to src/i18n/locales/en/tutorial.json diff --git a/public/locales/es/auth.json b/src/i18n/locales/es/auth.json similarity index 100% rename from public/locales/es/auth.json rename to src/i18n/locales/es/auth.json diff --git a/public/locales/es/core.json b/src/i18n/locales/es/core.json similarity index 100% rename from public/locales/es/core.json rename to src/i18n/locales/es/core.json diff --git a/public/locales/es/tutorial.json b/src/i18n/locales/es/tutorial.json similarity index 100% rename from public/locales/es/tutorial.json rename to src/i18n/locales/es/tutorial.json diff --git a/public/locales/fr/auth.json b/src/i18n/locales/fr/auth.json similarity index 100% rename from public/locales/fr/auth.json rename to src/i18n/locales/fr/auth.json diff --git a/public/locales/fr/core.json b/src/i18n/locales/fr/core.json similarity index 100% rename from public/locales/fr/core.json rename to src/i18n/locales/fr/core.json diff --git a/public/locales/fr/tutorial.json b/src/i18n/locales/fr/tutorial.json similarity index 100% rename from public/locales/fr/tutorial.json rename to src/i18n/locales/fr/tutorial.json diff --git a/public/locales/it/auth.json b/src/i18n/locales/it/auth.json similarity index 100% rename from public/locales/it/auth.json rename to src/i18n/locales/it/auth.json diff --git a/public/locales/it/core.json b/src/i18n/locales/it/core.json similarity index 100% rename from public/locales/it/core.json rename to src/i18n/locales/it/core.json diff --git a/public/locales/it/group.json b/src/i18n/locales/it/group.json similarity index 100% rename from public/locales/it/group.json rename to src/i18n/locales/it/group.json diff --git a/public/locales/it/tutorial.json b/src/i18n/locales/it/tutorial.json similarity index 100% rename from public/locales/it/tutorial.json rename to src/i18n/locales/it/tutorial.json diff --git a/public/locales/ru/auth.json b/src/i18n/locales/ru/auth.json similarity index 100% rename from public/locales/ru/auth.json rename to src/i18n/locales/ru/auth.json diff --git a/public/locales/ru/core.json b/src/i18n/locales/ru/core.json similarity index 100% rename from public/locales/ru/core.json rename to src/i18n/locales/ru/core.json diff --git a/public/locales/ru/tutorial.json b/src/i18n/locales/ru/tutorial.json similarity index 100% rename from public/locales/ru/tutorial.json rename to src/i18n/locales/ru/tutorial.json diff --git a/src/main.tsx b/src/main.tsx index 0a35316..d6da0d2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,7 +5,7 @@ import './messaging/messagesToBackground'; import { MessageQueueProvider } from './MessageQueueContext.tsx'; import { ThemeProvider } from './components/Theme/ThemeContext.tsx'; import { CssBaseline } from '@mui/material'; -import '../i18n'; +import './i18n/i18n.js'; createRoot(document.getElementById('root')!).render( <> diff --git a/tsconfig.json b/tsconfig.json index 24c5d39..95bc40f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,12 +8,13 @@ "skipLibCheck": true, /* Bundler mode */ - "moduleResolution": "bundler", + "moduleResolution": "node", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, /* Linting */ "strict": true,