mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-05-15 22:26:58 +00:00
fix language select to work in client-side prod
This commit is contained in:
parent
d838fe483a
commit
2b36121bb5
58
i18n.js
58
i18n.js
@ -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;
|
|
@ -1,7 +1,13 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { supportedLanguages } from '../../../i18n';
|
import { supportedLanguages } from '../../i18n/i18n';
|
||||||
import { Tooltip, useTheme } from '@mui/material';
|
import {
|
||||||
|
FormControl,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
Tooltip,
|
||||||
|
useTheme,
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
const LanguageSelector = () => {
|
const LanguageSelector = () => {
|
||||||
const { i18n, t } = useTranslation(['core']);
|
const { i18n, t } = useTranslation(['core']);
|
||||||
@ -19,20 +25,6 @@ const LanguageSelector = () => {
|
|||||||
const { name, flag } =
|
const { name, flag } =
|
||||||
supportedLanguages[currentLang] || supportedLanguages['en'];
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={selectorRef}
|
ref={selectorRef}
|
||||||
@ -44,33 +36,13 @@ const LanguageSelector = () => {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip
|
{!showSelect && (
|
||||||
title={t('core:action.change_language', {
|
<Tooltip
|
||||||
postProcess: 'capitalize',
|
key={currentLang}
|
||||||
})}
|
title={t('core:action.change_language', {
|
||||||
>
|
postProcess: 'capitalize',
|
||||||
{showSelect ? (
|
})}
|
||||||
<select
|
>
|
||||||
style={{
|
|
||||||
fontSize: '1rem',
|
|
||||||
border: '2px',
|
|
||||||
background: theme.palette.background.default,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
cursor: 'pointer',
|
|
||||||
position: 'relative',
|
|
||||||
bottom: '7px',
|
|
||||||
}}
|
|
||||||
value={currentLang}
|
|
||||||
onChange={handleChange}
|
|
||||||
autoFocus
|
|
||||||
>
|
|
||||||
{Object.entries(supportedLanguages).map(([code, { name }]) => (
|
|
||||||
<option key={code} value={code}>
|
|
||||||
{code.toUpperCase()} - {name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
) : (
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowSelect(true)}
|
onClick={() => setShowSelect(true)}
|
||||||
style={{
|
style={{
|
||||||
@ -81,10 +53,36 @@ const LanguageSelector = () => {
|
|||||||
}}
|
}}
|
||||||
aria-label={`Current language: ${name}`}
|
aria-label={`Current language: ${name}`}
|
||||||
>
|
>
|
||||||
{showSelect ? undefined : flag}
|
{flag}
|
||||||
</button>
|
</button>
|
||||||
)}
|
</Tooltip>
|
||||||
</Tooltip>
|
)}
|
||||||
|
|
||||||
|
{showSelect && (
|
||||||
|
<FormControl
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
minWidth: 120,
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
open
|
||||||
|
labelId="language-select-label"
|
||||||
|
id="language-select"
|
||||||
|
value={currentLang}
|
||||||
|
onChange={handleChange}
|
||||||
|
autoFocus
|
||||||
|
onClose={() => setShowSelect(false)}
|
||||||
|
>
|
||||||
|
{Object.entries(supportedLanguages).map(([code, { name }]) => (
|
||||||
|
<MenuItem key={code} value={code}>
|
||||||
|
{code.toUpperCase()} – {name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
54
src/i18n/i18n.ts
Normal file
54
src/i18n/i18n.ts
Normal file
@ -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<string, any>;
|
||||||
|
|
||||||
|
// Construct i18n resources object
|
||||||
|
const resources: Record<string, Record<string, any>> = {};
|
||||||
|
|
||||||
|
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;
|
@ -5,7 +5,7 @@ import './messaging/messagesToBackground';
|
|||||||
import { MessageQueueProvider } from './MessageQueueContext.tsx';
|
import { MessageQueueProvider } from './MessageQueueContext.tsx';
|
||||||
import { ThemeProvider } from './components/Theme/ThemeContext.tsx';
|
import { ThemeProvider } from './components/Theme/ThemeContext.tsx';
|
||||||
import { CssBaseline } from '@mui/material';
|
import { CssBaseline } from '@mui/material';
|
||||||
import '../i18n';
|
import './i18n/i18n.js';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<>
|
<>
|
||||||
|
@ -8,12 +8,13 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "node",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user