fix language select to work in client-side prod

This commit is contained in:
PhilReact 2025-05-10 14:25:47 +03:00
parent d838fe483a
commit 2b36121bb5
25 changed files with 102 additions and 107 deletions

58
i18n.js
View File

@ -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;

View File

@ -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 (
<div
ref={selectorRef}
@ -44,33 +36,13 @@ const LanguageSelector = () => {
position: 'absolute',
}}
>
<Tooltip
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>
) : (
{!showSelect && (
<Tooltip
key={currentLang}
title={t('core:action.change_language', {
postProcess: 'capitalize',
})}
>
<button
onClick={() => setShowSelect(true)}
style={{
@ -81,10 +53,36 @@ const LanguageSelector = () => {
}}
aria-label={`Current language: ${name}`}
>
{showSelect ? undefined : flag}
{flag}
</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>
);
};

54
src/i18n/i18n.ts Normal file
View 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;

View File

@ -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(
<>

View File

@ -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,