mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-22 19:46:39 +00:00
Disable language selection for everything other than English GB (#4467)
# Description of Changes For the first release of V2, we'll not have any reasonable translations for anything other than English GB, so with that in mind, this PR disables language selection for anything other than English GB, with a tooltip saying the other languages are coming soon. I also split the JSX up a little bit while I was at it to make it easier to manage. --------- Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
This commit is contained in:
parent
9cbd1f7f0c
commit
64beb4d076
@ -104,6 +104,7 @@
|
|||||||
"green": "Green",
|
"green": "Green",
|
||||||
"blue": "Blue",
|
"blue": "Blue",
|
||||||
"custom": "Custom...",
|
"custom": "Custom...",
|
||||||
|
"comingSoon": "Coming soon",
|
||||||
"WorkInProgess": "Work in progress, May not work or be buggy, Please report any problems!",
|
"WorkInProgess": "Work in progress, May not work or be buggy, Please report any problems!",
|
||||||
"poweredBy": "Powered by",
|
"poweredBy": "Powered by",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
|
@ -1,24 +1,161 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Menu, Button, ScrollArea, ActionIcon } from '@mantine/core';
|
import { Menu, Button, ScrollArea, ActionIcon, Tooltip } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { supportedLanguages } from '../../i18n';
|
import { supportedLanguages } from '../../i18n';
|
||||||
import LocalIcon from './LocalIcon';
|
import LocalIcon from './LocalIcon';
|
||||||
import styles from './LanguageSelector.module.css';
|
import styles from './LanguageSelector.module.css';
|
||||||
|
|
||||||
|
// Types
|
||||||
interface LanguageSelectorProps {
|
interface LanguageSelectorProps {
|
||||||
position?: React.ComponentProps<typeof Menu>['position'];
|
position?: React.ComponentProps<typeof Menu>['position'];
|
||||||
offset?: number;
|
offset?: number;
|
||||||
compact?: boolean; // icon-only trigger
|
compact?: boolean; // icon-only trigger
|
||||||
}
|
}
|
||||||
|
|
||||||
const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = false }: LanguageSelectorProps) => {
|
interface LanguageOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RippleEffect {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
key: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub-components
|
||||||
|
interface LanguageItemProps {
|
||||||
|
option: LanguageOption;
|
||||||
|
index: number;
|
||||||
|
animationTriggered: boolean;
|
||||||
|
isSelected: boolean;
|
||||||
|
onClick: (event: React.MouseEvent) => void;
|
||||||
|
rippleEffect?: RippleEffect | null;
|
||||||
|
pendingLanguage: string | null;
|
||||||
|
compact: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LanguageItem: React.FC<LanguageItemProps> = ({
|
||||||
|
option,
|
||||||
|
index,
|
||||||
|
animationTriggered,
|
||||||
|
isSelected,
|
||||||
|
onClick,
|
||||||
|
rippleEffect,
|
||||||
|
pendingLanguage,
|
||||||
|
compact,
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const label = disabled ? (
|
||||||
|
<Tooltip label={t('comingSoon', 'Coming soon')} position="left" withArrow>
|
||||||
|
<p>{option.label}</p>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<p>{option.label}</p>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.languageItem}
|
||||||
|
style={{
|
||||||
|
opacity: animationTriggered ? 1 : 0,
|
||||||
|
transform: animationTriggered ? 'translateY(0px)' : 'translateY(8px)',
|
||||||
|
transition: `opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.02}s, transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.02}s`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
fullWidth
|
||||||
|
onClick={disabled ? undefined : onClick}
|
||||||
|
data-selected={isSelected}
|
||||||
|
disabled={disabled}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
borderRadius: '4px',
|
||||||
|
minHeight: '32px',
|
||||||
|
padding: '4px 8px',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: isSelected
|
||||||
|
? 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))'
|
||||||
|
: 'transparent',
|
||||||
|
color: disabled
|
||||||
|
? 'light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3))'
|
||||||
|
: isSelected
|
||||||
|
? 'light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))'
|
||||||
|
: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-white))',
|
||||||
|
transition: 'all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
|
'&:hover': !disabled ? {
|
||||||
|
backgroundColor: isSelected
|
||||||
|
? 'light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-7))'
|
||||||
|
: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
||||||
|
transform: 'translateY(-1px)',
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
} : {}
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: isSelected ? 600 : 400,
|
||||||
|
textAlign: 'left',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 2,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{!compact && rippleEffect && pendingLanguage === option.value && (
|
||||||
|
<div
|
||||||
|
key={rippleEffect.key}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: rippleEffect.x,
|
||||||
|
top: rippleEffect.y,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: 'var(--mantine-color-blue-4)',
|
||||||
|
opacity: 0.6,
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
animation: 'ripple-expand 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RippleStyles: React.FC = () => (
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
@keyframes ripple-expand {
|
||||||
|
0% { width: 0; height: 0; opacity: 0.6; }
|
||||||
|
50% { opacity: 0.3; }
|
||||||
|
100% { width: 100px; height: 100px; opacity: 0; }
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Main component
|
||||||
|
const LanguageSelector: React.FC<LanguageSelectorProps> = ({ position = 'bottom-start', offset = 8, compact = false }) => {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const [animationTriggered, setAnimationTriggered] = useState(false);
|
const [animationTriggered, setAnimationTriggered] = useState(false);
|
||||||
const [pendingLanguage, setPendingLanguage] = useState<string | null>(null);
|
const [pendingLanguage, setPendingLanguage] = useState<string | null>(null);
|
||||||
const [rippleEffect, setRippleEffect] = useState<{x: number, y: number, key: number} | null>(null);
|
const [rippleEffect, setRippleEffect] = useState<RippleEffect | null>(null);
|
||||||
|
|
||||||
const languageOptions = Object.entries(supportedLanguages)
|
const languageOptions: LanguageOption[] = Object.entries(supportedLanguages)
|
||||||
.sort(([, nameA], [, nameB]) => nameA.localeCompare(nameB))
|
.sort(([, nameA], [, nameB]) => nameA.localeCompare(nameB))
|
||||||
.map(([code, name]) => ({
|
.map(([code, name]) => ({
|
||||||
value: code,
|
value: code,
|
||||||
@ -65,15 +202,7 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style>
|
<RippleStyles />
|
||||||
{`
|
|
||||||
@keyframes ripple-expand {
|
|
||||||
0% { width: 0; height: 0; opacity: 0.6; }
|
|
||||||
50% { opacity: 0.3; }
|
|
||||||
100% { width: 100px; height: 100px; opacity: 0; }
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
<Menu
|
<Menu
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onChange={setOpened}
|
onChange={setOpened}
|
||||||
@ -86,138 +215,85 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal
|
|||||||
timingFunction: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
timingFunction: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
{compact ? (
|
{compact ? (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
title={currentLanguage}
|
title={currentLanguage}
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
color: 'var(--right-rail-icon)',
|
color: 'var(--right-rail-icon)',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<LocalIcon icon="language" width="1.5rem" height="1.5rem" />
|
||||||
<LocalIcon icon="language" width="1.5rem" height="1.5rem" />
|
</ActionIcon>
|
||||||
</ActionIcon>
|
) : (
|
||||||
) : (
|
<Button
|
||||||
<Button
|
variant="subtle"
|
||||||
variant="subtle"
|
size="sm"
|
||||||
size="sm"
|
leftSection={<LocalIcon icon="language" width="1.5rem" height="1.5rem" />}
|
||||||
leftSection={<LocalIcon icon="language" width="1.5rem" height="1.5rem" />}
|
styles={{
|
||||||
styles={{
|
root: {
|
||||||
root: {
|
border: 'none',
|
||||||
border: 'none',
|
color: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-1))',
|
||||||
color: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-1))',
|
transition: 'background-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
transition: 'background-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
'&:hover': {
|
||||||
'&:hover': {
|
backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
||||||
backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
}
|
||||||
}
|
},
|
||||||
},
|
label: { fontSize: '12px', fontWeight: 500 }
|
||||||
label: { fontSize: '12px', fontWeight: 500 }
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<span className={styles.languageText}>
|
||||||
<span className={styles.languageText}>
|
{currentLanguage}
|
||||||
{currentLanguage}
|
</span>
|
||||||
</span>
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
)}
|
</Menu.Target>
|
||||||
</Menu.Target>
|
|
||||||
|
|
||||||
<Menu.Dropdown
|
<Menu.Dropdown
|
||||||
style={{
|
style={{
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||||
backgroundColor: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))',
|
backgroundColor: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))',
|
||||||
border: 'light-dark(1px solid var(--mantine-color-gray-3), 1px solid var(--mantine-color-dark-4))',
|
border: 'light-dark(1px solid var(--mantine-color-gray-3), 1px solid var(--mantine-color-dark-4))',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScrollArea h={190} type="scroll">
|
<ScrollArea h={190} type="scroll">
|
||||||
<div className={styles.languageGrid}>
|
<div className={styles.languageGrid}>
|
||||||
{languageOptions.map((option, index) => (
|
{languageOptions.map((option, index) => {
|
||||||
<div
|
const isEnglishGB = option.value === 'en-GB'; // Currently only English GB has enough translations to use
|
||||||
key={option.value}
|
const isDisabled = !isEnglishGB;
|
||||||
className={styles.languageItem}
|
|
||||||
style={{
|
return (
|
||||||
opacity: animationTriggered ? 1 : 0,
|
<LanguageItem
|
||||||
transform: animationTriggered ? 'translateY(0px)' : 'translateY(8px)',
|
key={option.value}
|
||||||
transition: `opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.02}s, transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.02}s`,
|
option={option}
|
||||||
}}
|
index={index}
|
||||||
>
|
animationTriggered={animationTriggered}
|
||||||
<Button
|
isSelected={option.value === i18n.language}
|
||||||
variant="subtle"
|
onClick={(event) => handleLanguageChange(option.value, event)}
|
||||||
size="sm"
|
rippleEffect={rippleEffect}
|
||||||
fullWidth
|
pendingLanguage={pendingLanguage}
|
||||||
onClick={(event) => handleLanguageChange(option.value, event)}
|
compact={compact}
|
||||||
data-selected={option.value === i18n.language}
|
disabled={isDisabled}
|
||||||
styles={{
|
/>
|
||||||
root: {
|
);
|
||||||
borderRadius: '4px',
|
})}
|
||||||
minHeight: '32px',
|
</div>
|
||||||
padding: '4px 8px',
|
</ScrollArea>
|
||||||
justifyContent: 'flex-start',
|
</Menu.Dropdown>
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden',
|
|
||||||
backgroundColor: option.value === i18n.language
|
|
||||||
? 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))'
|
|
||||||
: 'transparent',
|
|
||||||
color: option.value === i18n.language
|
|
||||||
? 'light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))'
|
|
||||||
: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-white))',
|
|
||||||
transition: 'all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: option.value === i18n.language
|
|
||||||
? 'light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-7))'
|
|
||||||
: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
|
||||||
transform: 'translateY(-1px)',
|
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
fontSize: '13px',
|
|
||||||
fontWeight: option.value === i18n.language ? 600 : 400,
|
|
||||||
textAlign: 'left',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
position: 'relative',
|
|
||||||
zIndex: 2,
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
{!compact && rippleEffect && pendingLanguage === option.value && (
|
|
||||||
<div
|
|
||||||
key={rippleEffect.key}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: rippleEffect.x,
|
|
||||||
top: rippleEffect.y,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
borderRadius: '50%',
|
|
||||||
backgroundColor: 'var(--mantine-color-blue-4)',
|
|
||||||
opacity: 0.6,
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
animation: 'ripple-expand 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LanguageSelector;
|
export default LanguageSelector;
|
||||||
|
export type { LanguageSelectorProps, LanguageOption, RippleEffect };
|
||||||
|
Loading…
x
Reference in New Issue
Block a user