mirror of
https://github.com/mue/mue.git
synced 2026-07-16 13:34:03 +02:00
176 lines
6.1 KiB
JavaScript
176 lines
6.1 KiB
JavaScript
import { useState, useMemo } from 'react';
|
|
import { useT, useTranslation } from 'contexts/TranslationContext';
|
|
|
|
import { MdOutlineOpenInNew, MdComputer } from 'react-icons/md';
|
|
|
|
import { Radio, Checkbox, SearchInput } from 'components/Form/Settings';
|
|
|
|
import languages from '@/i18n/languages.json';
|
|
import translationPercentages from '@/i18n/translationPercentages.json';
|
|
|
|
const LanguageOptions = () => {
|
|
const t = useT();
|
|
const { language: currentLanguage, changeLanguage } = useTranslation();
|
|
const languageTitle = t('modals.main.settings.sections.language.title');
|
|
|
|
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")
|
|
const currentLanguageISO = currentLanguage.replace('_', '-');
|
|
|
|
// Use Intl.DisplayNames to get language names in the current language
|
|
const displayNames = new Intl.DisplayNames([currentLanguageISO], { type: 'language' });
|
|
|
|
const mappedLanguages = languages.map((lang) => {
|
|
const nativeName = lang.name;
|
|
|
|
// Convert language code to ISO format for Intl.DisplayNames
|
|
// e.g., "en_GB" -> "en-GB", "zh_CN" -> "zh-CN"
|
|
const isoCode = lang.value.replace('_', '-');
|
|
const percentage = translationPercentages[lang.value]?.percent || 0;
|
|
|
|
let translatedName;
|
|
try {
|
|
translatedName = displayNames.of(isoCode);
|
|
// Simplify by removing country suffixes: "German (Germany)" → "German"
|
|
if (translatedName) {
|
|
translatedName = translatedName.split(' (')[0];
|
|
}
|
|
} catch {
|
|
// Fallback if the code isn't recognized
|
|
translatedName = nativeName;
|
|
}
|
|
|
|
// Show native name first, then translated name in brackets (greyed and smaller)
|
|
const displayName =
|
|
!translatedName || translatedName === nativeName ? (
|
|
<>
|
|
{nativeName} <span style={{ color: '#999', fontSize: '0.85em' }}>({percentage}%)</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
{nativeName}{' '}
|
|
<span style={{ color: '#999', fontSize: '0.85em' }}>
|
|
({translatedName} • {percentage}%)
|
|
</span>
|
|
</>
|
|
);
|
|
|
|
return {
|
|
name: displayName,
|
|
value: lang.value,
|
|
nativeName,
|
|
percentage,
|
|
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);
|
|
|
|
return (
|
|
<>
|
|
<div className="modalHeader">
|
|
<span className="mainTitle">{languageTitle}</span>
|
|
<div className="headerActions">
|
|
<a
|
|
className="link"
|
|
href="https://hosted.weblate.org/new-lang/mue/mue-tab/"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
Add translation
|
|
<MdOutlineOpenInNew />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div style={{ marginBottom: '16px' }}>
|
|
<Checkbox
|
|
name="localeFormatting"
|
|
text={t('modals.main.settings.sections.language.locale_formatting')}
|
|
category="other"
|
|
/>
|
|
</div>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: 16,
|
|
}}
|
|
>
|
|
<SearchInput
|
|
placeholder={t('modals.main.settings.sections.language.search')}
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
{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
|
|
onClick={() => changeLanguage(systemLanguage.value)}
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
padding: '10px 16px',
|
|
marginBottom: 16,
|
|
background: 'rgba(255, 255, 255, 0.08)',
|
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
borderRadius: '8px',
|
|
color: 'var(--fg)',
|
|
cursor: 'pointer',
|
|
fontSize: '14px',
|
|
transition: 'all 0.2s ease',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.12)';
|
|
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.2)';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.08)';
|
|
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.1)';
|
|
}}
|
|
>
|
|
<MdComputer style={{ fontSize: '18px', opacity: 0.8 }} />
|
|
{t('modals.main.settings.sections.language.use_system')}
|
|
<span style={{ opacity: 0.6 }}>({systemLanguage.name})</span>
|
|
</button>
|
|
)}
|
|
<div className="languageSettings">
|
|
<Radio name="language" options={filteredLanguages} element=".other" />
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export { LanguageOptions as default, LanguageOptions };
|