feat(Language): add search functionality and system language detection to language options

This commit is contained in:
alexsparkes
2026-01-24 21:24:20 +00:00
parent 9824b8e99a
commit 745e4e7106
36 changed files with 276 additions and 43 deletions

View File

@@ -2,7 +2,8 @@ import { useState, useEffect, useRef, useMemo } from 'react';
import { useT, useTranslation } from 'contexts/TranslationContext';
import variables from 'config/variables';
import { MdOutlineOpenInNew } from 'react-icons/md';
import { MdOutlineOpenInNew, MdSearch } from 'react-icons/md';
import { TextField, InputAdornment } from '@mui/material';
import { Radio } from 'components/Form/Settings';
@@ -10,12 +11,14 @@ import languages from '@/i18n/languages.json';
const LanguageOptions = () => {
const t = useT();
const { language: currentLanguage } = useTranslation();
const { language: currentLanguage, changeLanguage } = useTranslation();
const loadingText = t('modals.main.loading');
const offlineText = t('modals.main.marketplace.offline.description');
const languageTitle = t('modals.main.settings.sections.language.title');
const quoteTitle = t('modals.main.settings.sections.language.quote');
const [searchQuery, setSearchQuery] = useState('');
// Create language options with both translated and native names
const languageOptions = useMemo(() => {
// Convert currentLanguage to ISO format (e.g., "de_DE" -> "de-DE")
@@ -24,7 +27,7 @@ const LanguageOptions = () => {
// Use Intl.DisplayNames to get language names in the current language
const displayNames = new Intl.DisplayNames([currentLanguageISO], { type: 'language' });
return languages.map((lang) => {
const mappedLanguages = languages.map((lang) => {
const nativeName = lang.name;
// Convert language code to ISO format for Intl.DisplayNames
@@ -56,10 +59,33 @@ const LanguageOptions = () => {
return {
name: displayName,
value: lang.value,
nativeName,
searchText: `${nativeName} ${translatedName || ''}`.toLowerCase(),
};
});
// Sort alphabetically by native name
return mappedLanguages.sort((a, b) => a.nativeName.localeCompare(b.nativeName));
}, [currentLanguage]);
// Filter languages based on search query
const filteredLanguages = useMemo(() => {
if (!searchQuery.trim()) return languageOptions;
const query = searchQuery.toLowerCase();
return languageOptions.filter(lang => lang.searchText.includes(query));
}, [languageOptions, searchQuery]);
// Detect system language
const systemLanguage = useMemo(() => {
const browserLang = navigator.language.replace('-', '_');
// Check exact match first, then base language
return languages.find(l => l.value === browserLang) ||
languages.find(l => l.value.startsWith(browserLang.split('_')[0]));
}, []);
// Find current language option for display
const currentLangOption = languageOptions.find(l => l.value === currentLanguage);
const [quoteLanguages, setQuoteLanguages] = useState([
{
name: loadingText,
@@ -136,8 +162,54 @@ const LanguageOptions = () => {
</a>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
<TextField
placeholder={t('modals.main.settings.sections.language.search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
variant="outlined"
size="small"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<MdSearch style={{ color: '#888' }} />
</InputAdornment>
),
}}
sx={{
width: '250px',
'& .MuiOutlinedInput-root': {
borderRadius: '24px',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
'& fieldset': {
border: 'none',
},
'&:hover fieldset': {
border: 'none',
},
'&.Mui-focused fieldset': {
border: 'none',
},
},
}}
/>
{currentLangOption && (
<div style={{ color: '#888', whiteSpace: 'nowrap' }}>
{t('modals.main.settings.sections.language.current')}: <strong style={{ color: 'var(--fg)' }}>{currentLangOption.nativeName}</strong>
</div>
)}
</div>
{systemLanguage && systemLanguage.value !== currentLanguage && (
<button
className="uploadbg"
onClick={() => changeLanguage(systemLanguage.value)}
style={{ marginBottom: 12 }}
>
{t('modals.main.settings.sections.language.use_system')} ({systemLanguage.name})
</button>
)}
<div className="languageSettings">
<Radio name="language" options={languageOptions} element=".other" />
<Radio name="language" options={filteredLanguages} element=".other" />
</div>
<span className="mainTitle">
{quoteTitle}

View File

@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { MdOutlineOpenInNew } from 'react-icons/md';
import { useState, useMemo } from 'react';
import { MdOutlineOpenInNew, MdSearch } from 'react-icons/md';
import { TextField, InputAdornment } from '@mui/material';
import languages from '@/i18n/languages.json';
import { useT, useTranslation } from 'contexts/TranslationContext';
import variables from 'config/variables';
@@ -9,15 +10,17 @@ import { Header, Content } from '../Layout';
function ChooseLanguage() {
const t = useT();
const { language: currentLanguage } = useTranslation();
const { language: currentLanguage, changeLanguage } = useTranslation();
const title = t('modals.welcome.sections.language.title');
const description = t('modals.welcome.sections.language.description');
const [searchQuery, setSearchQuery] = useState('');
const languageOptions = useMemo(() => {
const currentLanguageISO = currentLanguage.replace('_', '-');
const displayNames = new Intl.DisplayNames([currentLanguageISO], { type: 'language' });
return languages.map((lang) => {
const mappedLanguages = languages.map((lang) => {
const nativeName = lang.name;
const isoCode = lang.value.replace('_', '-');
@@ -44,10 +47,29 @@ function ChooseLanguage() {
return {
name: displayName,
value: lang.value,
nativeName,
searchText: `${nativeName} ${translatedName || ''}`.toLowerCase(),
};
});
// Sort alphabetically by native name
return mappedLanguages.sort((a, b) => a.nativeName.localeCompare(b.nativeName));
}, [currentLanguage]);
// Filter languages based on search query
const filteredLanguages = useMemo(() => {
if (!searchQuery.trim()) return languageOptions;
const query = searchQuery.toLowerCase();
return languageOptions.filter(lang => lang.searchText.includes(query));
}, [languageOptions, searchQuery]);
// Detect system language
const systemLanguage = useMemo(() => {
const browserLang = navigator.language.replace('-', '_');
return languages.find(l => l.value === browserLang) ||
languages.find(l => l.value.startsWith(browserLang.split('_')[0]));
}, []);
return (
<Content>
<Header
@@ -67,8 +89,48 @@ function ChooseLanguage() {
</>
}
/>
{systemLanguage && systemLanguage.value !== currentLanguage && (
<button
className="uploadbg"
onClick={() => changeLanguage(systemLanguage.value)}
style={{ marginBottom: 12 }}
>
{t('modals.main.settings.sections.language.use_system')} ({systemLanguage.name})
</button>
)}
<TextField
placeholder={t('modals.main.settings.sections.language.search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
variant="outlined"
size="small"
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<MdSearch style={{ color: '#888' }} />
</InputAdornment>
),
}}
sx={{
marginBottom: 2,
'& .MuiOutlinedInput-root': {
borderRadius: '24px',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
'& fieldset': {
border: 'none',
},
'&:hover fieldset': {
border: 'none',
},
'&.Mui-focused fieldset': {
border: 'none',
},
},
}}
/>
<div className="languageSettings">
<Radio name="language" options={languageOptions} category="welcomeLanguage" />
<Radio name="language" options={filteredLanguages} category="welcomeLanguage" />
</div>
</Content>
);