Files
kapteins-daagbok/client/src/components/LanguageDropdown.tsx
T

207 lines
6.4 KiB
TypeScript

import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Languages, Globe, ChevronDown } from 'lucide-react'
import {
SUPPORTED_LANGUAGES,
changeAppLanguage,
normalizeAppLanguage,
type AppLanguage
} from '../utils/i18nLanguages.js'
function FlagIcon({ lang, className, style }: { lang: string; className?: string; style?: React.CSSProperties }) {
const baseStyle = {
display: 'inline-block',
verticalAlign: 'middle',
borderRadius: '2px',
overflow: 'hidden',
border: '1px solid rgba(255, 255, 255, 0.15)',
boxSizing: 'border-box' as const,
...style
}
switch (lang) {
case 'de':
return (
<svg viewBox="0 0 5 3" className={className} style={baseStyle}>
<rect width="5" height="3" fill="#FFCE00"/>
<rect width="5" height="2" fill="#DD0000"/>
<rect width="5" height="1" fill="#000000"/>
</svg>
)
case 'en':
return (
<svg viewBox="0 0 60 30" className={className} style={baseStyle}>
<clipPath id="union-jack-clip">
<path d="M0,0 L60,30 M60,0 L0,30"/>
</clipPath>
<rect width="60" height="30" fill="#012169"/>
<path d="M0,0 L60,30 M60,0 L0,30" stroke="#fff" strokeWidth="6"/>
<path d="M0,0 L60,30 M60,0 L0,30" stroke="#C8102E" strokeWidth="4" clipPath="url(#union-jack-clip)"/>
<path d="M30,0 v30 M0,15 h60" stroke="#fff" strokeWidth="10"/>
<path d="M30,0 v30 M0,15 h60" stroke="#C8102E" strokeWidth="6"/>
</svg>
)
case 'da':
return (
<svg viewBox="0 0 37 28" className={className} style={baseStyle}>
<rect width="37" height="28" fill="#C8102E"/>
<path d="M12,0 h4 v28 h-4 z M0,12 h37 v4 h-37 z" fill="#FFFFFF"/>
</svg>
)
case 'sv':
return (
<svg viewBox="0 0 16 10" className={className} style={baseStyle}>
<rect width="16" height="10" fill="#006AA7"/>
<path d="M5,0 h2 v10 h-2 z M0,4 h16 v2 h-16 z" fill="#FECC00"/>
</svg>
)
case 'nb':
return (
<svg viewBox="0 0 22 16" className={className} style={baseStyle}>
<rect width="22" height="16" fill="#BA0C2F"/>
<path d="M6,0 h4 v16 h-4 z M0,6 h22 v4 h-22 z" fill="#FFFFFF"/>
<path d="M7,0 h2 v16 h-2 z M0,7 h22 v2 h-22 z" fill="#00205B"/>
</svg>
)
case 'fr':
return (
<svg viewBox="0 0 3 2" className={className} style={baseStyle}>
<rect width="3" height="2" fill="#FFFFFF"/>
<rect width="1" height="2" fill="#002395"/>
<rect x="2" width="1" height="2" fill="#ED2939"/>
</svg>
)
case 'es':
return (
<svg viewBox="0 0 3 2" className={className} style={baseStyle}>
<rect width="3" height="2" fill="#C1272D"/>
<rect y="0.5" width="3" height="1" fill="#FEE100"/>
</svg>
)
default:
return null
}
}
interface LanguageDropdownProps {
variant?: 'icon' | 'text' | 'secondary-button'
align?: 'left' | 'right'
}
export default function LanguageDropdown({
variant = 'icon',
align = 'right'
}: LanguageDropdownProps) {
const { t, i18n } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const rootRef = useRef<HTMLDivElement>(null)
const activeLang = normalizeAppLanguage(i18n.language)
useEffect(() => {
if (!isOpen) return
const closeOnOutsideClick = (event: MouseEvent) => {
if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') setIsOpen(false)
}
document.addEventListener('mousedown', closeOnOutsideClick)
document.addEventListener('keydown', closeOnEscape)
return () => {
document.removeEventListener('mousedown', closeOnOutsideClick)
document.removeEventListener('keydown', closeOnEscape)
}
}, [isOpen])
const selectLanguage = (lang: AppLanguage) => {
changeAppLanguage(i18n, lang)
setIsOpen(false)
}
// Trigger button content based on variant
const renderTriggerContent = () => {
const name = t(`languages.${activeLang}`)
if (variant === 'icon') {
return (
<span className="lang-dropdown-trigger-flag" aria-hidden="true">
<FlagIcon lang={activeLang} className="lang-flag-svg trigger-icon-only" />
</span>
)
}
if (variant === 'secondary-button') {
return (
<>
<Globe size={14} style={{ marginRight: '4px' }} />
<FlagIcon lang={activeLang} className="lang-flag-svg" style={{ marginRight: '4px' }} />
<span className="lang-trigger-name">{name}</span>
<ChevronDown size={12} className="lang-dropdown-chevron" />
</>
)
}
// Default or "text" variant (used in footer)
return (
<>
<Languages size={18} />
<FlagIcon lang={activeLang} className="lang-flag-svg" style={{ margin: '0 4px' }} />
<span>{name}</span>
<ChevronDown size={14} className="lang-dropdown-chevron" />
</>
)
}
const triggerClass =
variant === 'icon'
? 'btn-icon'
: variant === 'secondary-button'
? 'btn secondary compact'
: 'btn-icon-text'
return (
<div
className={`lang-dropdown ${isOpen ? 'is-open' : ''} align-${align}`}
ref={rootRef}
>
<button
type="button"
className={triggerClass}
onClick={() => setIsOpen((prev) => !prev)}
aria-haspopup="listbox"
aria-expanded={isOpen}
title="Switch Language"
style={variant === 'secondary-button' ? { width: 'auto', padding: '6px 12px', fontSize: '13px' } : undefined}
>
{renderTriggerContent()}
</button>
{isOpen && (
<ul className="lang-dropdown-menu" role="listbox">
{SUPPORTED_LANGUAGES.map((lang) => {
const isSelected = lang === activeLang
return (
<li
key={lang}
role="option"
aria-selected={isSelected}
className={`lang-dropdown-option ${isSelected ? 'is-selected' : ''}`}
onClick={() => selectLanguage(lang)}
>
<FlagIcon lang={lang} className="lang-flag-svg" />
<span className="lang-option-name">{t(`languages.${lang}`)}</span>
</li>
)
})}
</ul>
)}
</div>
)
}