mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
180 lines
5.6 KiB
TypeScript
180 lines
5.6 KiB
TypeScript
import type { ComponentType, SVGProps } from 'react';
|
|
import { Menu, UnstyledButton, Text, Group, rem } from '@mantine/core';
|
|
import { IconCheck, IconChevronDown, IconWorld } from '@tabler/icons-react';
|
|
import { GB, ET } from 'country-flag-icons/react/3x2';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
interface LanguageSwitcherProps {
|
|
supportedLanguages: readonly string[];
|
|
/** `icon` renders a compact flag button; `button` shows the language label. */
|
|
variant?: 'icon' | 'button';
|
|
}
|
|
|
|
type FlagComponentType = ComponentType<SVGProps<SVGSVGElement>>;
|
|
|
|
const LANGUAGE_CONFIG: Record<
|
|
string,
|
|
{ Flag: FlagComponentType; nativeName: string; shortCode: string }
|
|
> = {
|
|
en: { Flag: GB, nativeName: 'English', shortCode: 'EN' },
|
|
am: { Flag: ET, nativeName: 'አማርኛ', shortCode: 'AM' },
|
|
};
|
|
|
|
export function LanguageSwitcher({
|
|
supportedLanguages,
|
|
variant = 'icon',
|
|
}: LanguageSwitcherProps) {
|
|
const { t, i18n } = useTranslation();
|
|
const current = i18n.language || 'en';
|
|
|
|
const change = (lng: string) => {
|
|
if (lng !== current) i18n.changeLanguage(lng);
|
|
};
|
|
|
|
const currentConfig = LANGUAGE_CONFIG[current] || {
|
|
Flag: null,
|
|
nativeName: current.toUpperCase(),
|
|
shortCode: current.toUpperCase(),
|
|
};
|
|
|
|
const CurrentFlag = currentConfig.Flag;
|
|
|
|
return (
|
|
<Menu
|
|
shadow="lg"
|
|
width={190}
|
|
position="bottom-end"
|
|
withinPortal
|
|
transitionProps={{ transition: 'pop-top-right', duration: 150 }}
|
|
>
|
|
<Menu.Target>
|
|
<UnstyledButton
|
|
aria-label={t('language.label', 'Language')}
|
|
style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: rem(6),
|
|
width: variant === 'icon' ? rem(38) : 'auto',
|
|
height: rem(38),
|
|
padding: variant === 'icon' ? 0 : '0 12px',
|
|
borderRadius: rem(12),
|
|
background: 'var(--mantine-color-body)',
|
|
color: 'var(--mantine-color-gray-7)',
|
|
boxShadow: '0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.08)',
|
|
transition: 'all 200ms ease',
|
|
cursor: 'pointer',
|
|
border: 'none',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.transform = 'translateY(-1px)';
|
|
e.currentTarget.style.boxShadow =
|
|
'0 4px 12px rgba(0,0,0,0.1), 0 0 0 1px var(--mantine-primary-color-3)';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.transform = 'translateY(0)';
|
|
e.currentTarget.style.boxShadow =
|
|
'0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.08)';
|
|
}}
|
|
>
|
|
{CurrentFlag ? (
|
|
<CurrentFlag
|
|
style={{
|
|
width: rem(20),
|
|
height: rem(14),
|
|
borderRadius: rem(2),
|
|
display: 'block',
|
|
boxShadow: '0 1px 2px rgba(0,0,0,0.2)',
|
|
}}
|
|
/>
|
|
) : (
|
|
<IconWorld size={19} />
|
|
)}
|
|
|
|
{variant !== 'icon' && (
|
|
<Group gap={6} wrap="nowrap">
|
|
<Text size="sm" fw={600}>
|
|
{t(`language.${current}`, currentConfig.nativeName)}
|
|
</Text>
|
|
<IconChevronDown size={14} stroke={2} style={{ opacity: 0.7 }} />
|
|
</Group>
|
|
)}
|
|
</UnstyledButton>
|
|
</Menu.Target>
|
|
<Menu.Dropdown
|
|
style={{
|
|
borderRadius: rem(14),
|
|
padding: rem(6),
|
|
boxShadow: '0 10px 30px rgba(0,0,0,0.15)',
|
|
backdropFilter: 'blur(8px)',
|
|
}}
|
|
>
|
|
<Menu.Label
|
|
style={{
|
|
fontWeight: 600,
|
|
fontSize: rem(11),
|
|
letterSpacing: '0.5px',
|
|
textTransform: 'uppercase',
|
|
}}
|
|
>
|
|
{t('language.label', 'Language')}
|
|
</Menu.Label>
|
|
{supportedLanguages.map((lng) => {
|
|
const config = LANGUAGE_CONFIG[lng] || {
|
|
Flag: null,
|
|
nativeName: lng.toUpperCase(),
|
|
shortCode: lng.toUpperCase(),
|
|
};
|
|
const Flag = config.Flag;
|
|
const isSelected = current === lng;
|
|
|
|
return (
|
|
<Menu.Item
|
|
key={lng}
|
|
onClick={() => change(lng)}
|
|
leftSection={
|
|
Flag ? (
|
|
<Flag
|
|
style={{
|
|
width: rem(20),
|
|
height: rem(14),
|
|
borderRadius: rem(2),
|
|
display: 'block',
|
|
boxShadow: '0 1px 2px rgba(0,0,0,0.15)',
|
|
}}
|
|
/>
|
|
) : (
|
|
<IconWorld size={16} />
|
|
)
|
|
}
|
|
rightSection={
|
|
isSelected ? (
|
|
<IconCheck
|
|
size={16}
|
|
stroke={2.4}
|
|
style={{ color: 'var(--mantine-primary-color-filled)' }}
|
|
/>
|
|
) : undefined
|
|
}
|
|
style={{
|
|
borderRadius: rem(8),
|
|
fontWeight: isSelected ? 600 : 400,
|
|
backgroundColor: isSelected
|
|
? 'var(--mantine-primary-color-light)'
|
|
: undefined,
|
|
color: isSelected ? 'var(--mantine-primary-color-filled)' : undefined,
|
|
}}
|
|
>
|
|
<Group justify="space-between" wrap="nowrap" style={{ width: '100%' }}>
|
|
<Text size="sm">{t(`language.${lng}`, config.nativeName)}</Text>
|
|
</Group>
|
|
</Menu.Item>
|
|
);
|
|
})}
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
);
|
|
}
|
|
|
|
|