feat(i18n): replace language cycle buttons with flag dropdown selector using inline SVGs
This commit is contained in:
@@ -6523,3 +6523,148 @@ body.app-tour-active .feedback-modal-overlay--tour .disclaimer-modal-panel {
|
||||
cursor: pointer;
|
||||
accent-color: var(--app-accent, #fbbf24);
|
||||
}
|
||||
|
||||
/* Language Dropdown */
|
||||
.lang-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.lang-dropdown-trigger-flag {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.lang-dropdown-chevron {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.75;
|
||||
transition: transform 0.2s ease;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.lang-dropdown.is-open .lang-dropdown-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.lang-dropdown-menu {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: calc(100% + 8px);
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
list-style: none;
|
||||
border: 1px solid var(--app-input-border, rgba(255, 255, 255, 0.1));
|
||||
border-radius: var(--app-radius-input, 12px);
|
||||
box-shadow: var(--app-card-shadow, 0 10px 30px rgba(0, 0, 0, 0.3));
|
||||
min-width: 140px;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
animation: slideDownFade 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes slideDownFade {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.lang-dropdown.align-right .lang-dropdown-menu {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.lang-dropdown.align-left .lang-dropdown-menu {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
html.scheme-light .lang-dropdown-menu {
|
||||
background: #ffffff;
|
||||
color: #0f172a;
|
||||
border-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
html.scheme-dark .lang-dropdown-menu {
|
||||
background: #1c1c1e;
|
||||
color: #f8fafc;
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.lang-dropdown-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: calc(var(--app-radius-input, 12px) - 4px);
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.lang-flag-svg {
|
||||
width: 20px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.lang-flag-svg.trigger-icon-only {
|
||||
width: 24px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
html.scheme-light .lang-dropdown-option {
|
||||
color: #334155;
|
||||
-webkit-text-fill-color: #334155;
|
||||
}
|
||||
|
||||
html.scheme-dark .lang-dropdown-option {
|
||||
color: #cbd5e1;
|
||||
-webkit-text-fill-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.lang-dropdown-option:hover {
|
||||
background: var(--app-accent-bg, rgba(217, 119, 6, 0.1));
|
||||
}
|
||||
|
||||
html.scheme-light .lang-dropdown-option:hover {
|
||||
color: var(--app-accent, #d97706);
|
||||
-webkit-text-fill-color: var(--app-accent, #d97706);
|
||||
}
|
||||
|
||||
html.scheme-dark .lang-dropdown-option:hover {
|
||||
color: var(--app-accent-light, #fbbf24);
|
||||
-webkit-text-fill-color: var(--app-accent-light, #fbbf24);
|
||||
}
|
||||
|
||||
.lang-dropdown-option.is-selected {
|
||||
background: var(--app-accent-bg, rgba(217, 119, 6, 0.15));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
html.scheme-light .lang-dropdown-option.is-selected {
|
||||
color: var(--app-accent, #d97706);
|
||||
-webkit-text-fill-color: var(--app-accent, #d97706);
|
||||
}
|
||||
|
||||
html.scheme-dark .lang-dropdown-option.is-selected {
|
||||
color: var(--app-accent-light, #fbbf24);
|
||||
-webkit-text-fill-color: var(--app-accent-light, #fbbf24);
|
||||
}
|
||||
|
||||
.lang-trigger-name {
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
+4
-11
@@ -46,14 +46,14 @@ import { db } from './services/db.js'
|
||||
import { getLogbookAccess } from './services/logbookAccess.js'
|
||||
import type { LogbookAccessRole } from './services/logbook.js'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { Ship, LogOut, ChevronLeft, Users, FileText, Settings, Wifi, WifiOff, Languages, BarChart2 } from 'lucide-react'
|
||||
import { Ship, LogOut, ChevronLeft, Users, FileText, Settings, Wifi, WifiOff, BarChart2 } from 'lucide-react'
|
||||
import DisclaimerHeaderButton from './components/DisclaimerHeaderButton.tsx'
|
||||
import FeedbackHeaderButton from './components/FeedbackHeaderButton.tsx'
|
||||
import ProfileHeaderButton from './components/ProfileHeaderButton.tsx'
|
||||
import AdminHeaderButton from './components/AdminHeaderButton.tsx'
|
||||
import { checkAdminAccess } from './services/adminApi.js'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cycleAppLanguage } from './utils/i18nLanguages.js'
|
||||
import LanguageDropdown from './components/LanguageDropdown.tsx'
|
||||
import {
|
||||
resolveTourLogbookContext,
|
||||
seedDemoLogbookIfNeeded
|
||||
@@ -66,7 +66,7 @@ import { requestPersistentStorage } from './utils/storagePersist.js'
|
||||
const PENDING_PUSH_LOGBOOK_KEY = 'pending_push_logbook_id'
|
||||
|
||||
function App() {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { t } = useTranslation()
|
||||
const { confirmLeave } = useUnsavedChangesContext()
|
||||
const { registerNavigation, registerDemoTourContext, requestStartAfterLogin, isActive, currentStepId } = useAppTour()
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||
@@ -555,10 +555,6 @@ function App() {
|
||||
localStorage.removeItem('active_logbook_title')
|
||||
}
|
||||
|
||||
const toggleLanguage = () => {
|
||||
cycleAppLanguage(i18n)
|
||||
}
|
||||
|
||||
const handleExitDemo = () => {
|
||||
window.history.replaceState({}, document.title, '/')
|
||||
syncRouteFromLocation()
|
||||
@@ -715,10 +711,7 @@ function App() {
|
||||
{online ? <Wifi size={18} /> : <WifiOff size={18} />}
|
||||
<span>{online ? 'Online' : t('sync.status_offline')}</span>
|
||||
</div>
|
||||
|
||||
<button className="btn-icon" onClick={toggleLanguage} title="Switch Language">
|
||||
<Languages size={18} />
|
||||
</button>
|
||||
<LanguageDropdown variant="icon" align="right" />
|
||||
|
||||
{isAdminUser && <AdminHeaderButton onClick={openAdmin} />}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cycleAppLanguage, getNextLanguage } from '../utils/i18nLanguages.js'
|
||||
import LanguageDropdown from './LanguageDropdown.tsx'
|
||||
import {
|
||||
registerUser,
|
||||
loginUser,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
logoutUser,
|
||||
resolveRestoreUsername
|
||||
} from '../services/auth.js'
|
||||
import { KeyRound, ShieldAlert, Languages, HelpCircle, UserRound, X } from 'lucide-react'
|
||||
import { KeyRound, ShieldAlert, HelpCircle, UserRound, X } from 'lucide-react'
|
||||
import RegistrationDisclaimer from './RegistrationDisclaimer.tsx'
|
||||
import DisclaimerModal from './DisclaimerModal.tsx'
|
||||
import BetaBadge from './BetaBadge.tsx'
|
||||
@@ -37,7 +37,7 @@ export default function AuthOnboarding({
|
||||
onOpenDemo,
|
||||
restoreSession = false
|
||||
}: AuthOnboardingProps) {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { t } = useTranslation()
|
||||
const [username, setUsername] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -267,9 +267,6 @@ export default function AuthOnboarding({
|
||||
setKnownUsers(getKnownUsernames())
|
||||
}
|
||||
|
||||
const toggleLanguage = () => {
|
||||
cycleAppLanguage(i18n)
|
||||
}
|
||||
|
||||
const copyToClipboard = () => {
|
||||
if (recoveryPhrase) {
|
||||
@@ -780,10 +777,7 @@ export default function AuthOnboarding({
|
||||
</div>
|
||||
|
||||
<div className="auth-footer">
|
||||
<button type="button" className="btn-icon-text" onClick={toggleLanguage}>
|
||||
<Languages size={18} />
|
||||
{t(`languages.${getNextLanguage(i18n.language)}`)}
|
||||
</button>
|
||||
<LanguageDropdown variant="text" align="left" />
|
||||
<button
|
||||
type="button"
|
||||
className="btn-icon-text link-sec"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cycleAppLanguage, getNextLanguage } from '../utils/i18nLanguages.js'
|
||||
import LanguageDropdown from './LanguageDropdown.tsx'
|
||||
import LogbookVesselPicker from './LogbookVesselPicker.tsx'
|
||||
import LogbookCrewPicker from './LogbookCrewPicker.tsx'
|
||||
import type { LogbookCrewSelectionData } from '../types/person.js'
|
||||
import { personToSnapshot } from '../utils/personSnapshots.js'
|
||||
import LogEntriesList from './LogEntriesList.tsx'
|
||||
import { Ship, Users, FileText, Lock, Globe, ChevronLeft, UserPlus } from 'lucide-react'
|
||||
import { Ship, Users, FileText, Lock, ChevronLeft, UserPlus } from 'lucide-react'
|
||||
import { buildPublicDemoFixture, type PublicDemoFixture } from '../services/demoLogbookData.js'
|
||||
import type { VesselData } from '../types/vessel.js'
|
||||
import type { LogbookVesselSelectionData } from '../types/vessel.js'
|
||||
@@ -52,9 +52,6 @@ export default function DemoViewer({ onExit }: DemoViewerProps) {
|
||||
}
|
||||
}, [registerNavigation, registerDemoTourContext, startTour, fixture.firstEntryId])
|
||||
|
||||
const toggleLanguage = () => {
|
||||
cycleAppLanguage(i18n)
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
@@ -111,10 +108,7 @@ export default function DemoViewer({ onExit }: DemoViewerProps) {
|
||||
<UserPlus size={14} style={{ marginRight: '4px' }} />
|
||||
{t('demo.cta_register')}
|
||||
</button>
|
||||
<button className="btn secondary" onClick={toggleLanguage} style={{ width: 'auto', padding: '6px 12px', fontSize: '13px' }}>
|
||||
<Globe size={14} style={{ marginRight: '4px' }} />
|
||||
{t(`languages.${getNextLanguage(i18n.language)}`)}
|
||||
</button>
|
||||
<LanguageDropdown variant="secondary-button" align="right" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -172,7 +166,7 @@ export default function DemoViewer({ onExit }: DemoViewerProps) {
|
||||
payloadId: v.payloadId,
|
||||
data: v.data as VesselData
|
||||
}))}
|
||||
preloadedSelection={logbookVesselSelection as LogbookVesselSelectionData}
|
||||
preloadedSelection={logbookVesselSelection as unknown as LogbookVesselSelectionData}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cycleAppLanguage, getNextLanguage } from '../utils/i18nLanguages.js'
|
||||
import { Ship, LogIn, UserPlus, AlertTriangle, ShieldCheck, Languages, ArrowRight, KeyRound } from 'lucide-react'
|
||||
import LanguageDropdown from './LanguageDropdown.tsx'
|
||||
import { Ship, LogIn, UserPlus, AlertTriangle, ShieldCheck, ArrowRight, KeyRound } from 'lucide-react'
|
||||
import {
|
||||
getActiveMasterKey,
|
||||
registerUser,
|
||||
@@ -50,7 +50,7 @@ const hexToBuffer = (hex: string): ArrayBuffer => {
|
||||
}
|
||||
|
||||
export default function InvitationAcceptance({ onAccepted, onCancel }: InvitationAcceptanceProps) {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [accepting, setAccepting] = useState(false)
|
||||
@@ -308,9 +308,6 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
|
||||
setIsLoggedIn(true)
|
||||
}
|
||||
|
||||
const toggleLanguage = () => {
|
||||
cycleAppLanguage(i18n)
|
||||
}
|
||||
|
||||
if (recoveryPhrase) {
|
||||
return (
|
||||
@@ -510,10 +507,7 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
|
||||
)}
|
||||
|
||||
<div className="auth-footer" style={{ borderTop: '1px solid rgba(255,255,255,0.05)', paddingTop: '16px', marginTop: '24px' }}>
|
||||
<button className="btn-icon-text" onClick={toggleLanguage}>
|
||||
<Languages size={18} />
|
||||
{t(`languages.${getNextLanguage(i18n.language)}`)}
|
||||
</button>
|
||||
<LanguageDropdown variant="text" align="left" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
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>
|
||||
)
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cycleAppLanguage, getNextLanguage, isGermanLocale } from '../utils/i18nLanguages.js'
|
||||
import { isGermanLocale } from '../utils/i18nLanguages.js'
|
||||
import LanguageDropdown from './LanguageDropdown.tsx'
|
||||
import { decryptJson } from '../services/crypto.js'
|
||||
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
|
||||
import LogbookVesselPicker from './LogbookVesselPicker.tsx'
|
||||
@@ -12,7 +13,7 @@ import { emptyLogbookCrewSelection } from '../types/person.js'
|
||||
import { legacyCrewRecordsToLogbookSelection } from '../utils/personSnapshots.js'
|
||||
import type { PersonData } from '../types/person.js'
|
||||
import LogEntriesList from './LogEntriesList.tsx'
|
||||
import { Ship, Users, FileText, Lock, AlertCircle, Globe } from 'lucide-react'
|
||||
import { Ship, Users, FileText, Lock, AlertCircle } from 'lucide-react'
|
||||
|
||||
interface ReadOnlyViewerProps {
|
||||
token: string
|
||||
@@ -215,9 +216,6 @@ export default function ReadOnlyViewer({ token, hexKey }: ReadOnlyViewerProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const toggleLanguage = () => {
|
||||
cycleAppLanguage(i18n)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -258,10 +256,7 @@ export default function ReadOnlyViewer({ token, hexKey }: ReadOnlyViewerProps) {
|
||||
</div>
|
||||
|
||||
<div className="header-actions">
|
||||
<button className="btn secondary" onClick={toggleLanguage} style={{ width: 'auto', padding: '6px 12px', fontSize: '13px' }}>
|
||||
<Globe size={14} style={{ marginRight: '4px' }} />
|
||||
{t(`languages.${getNextLanguage(i18n.language)}`)}
|
||||
</button>
|
||||
<LanguageDropdown variant="secondary-button" align="right" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -6,6 +6,14 @@ export const SUPPORTED_LANGUAGES = ['de', 'en', 'da', 'sv', 'nb'] as const
|
||||
|
||||
export type AppLanguage = (typeof SUPPORTED_LANGUAGES)[number]
|
||||
|
||||
export const LANGUAGE_FLAGS: Record<AppLanguage, string> = {
|
||||
de: '🇩🇪',
|
||||
en: '🇬🇧',
|
||||
da: '🇩🇰',
|
||||
sv: '🇸🇪',
|
||||
nb: '🇳🇴'
|
||||
}
|
||||
|
||||
export function normalizeAppLanguage(language?: string): AppLanguage {
|
||||
const base = (language ?? 'en').split('-')[0].toLowerCase()
|
||||
if ((SUPPORTED_LANGUAGES as readonly string[]).includes(base)) {
|
||||
|
||||
Reference in New Issue
Block a user