diff --git a/client/src/components/Admin/AddonManager.tsx b/client/src/components/Admin/AddonManager.tsx index b8eb170..b82d6df 100644 --- a/client/src/components/Admin/AddonManager.tsx +++ b/client/src/components/Admin/AddonManager.tsx @@ -136,8 +136,21 @@ interface AddonRowProps { t: (key: string) => string } +function getAddonLabel(t: (key: string) => string, addon: Addon): { name: string; description: string } { + const nameKey = `admin.addons.catalog.${addon.id}.name` + const descKey = `admin.addons.catalog.${addon.id}.description` + const translatedName = t(nameKey) + const translatedDescription = t(descKey) + + return { + name: translatedName !== nameKey ? translatedName : addon.name, + description: translatedDescription !== descKey ? translatedDescription : addon.description, + } +} + function AddonRow({ addon, onToggle, t }: AddonRowProps) { const isComingSoon = false + const label = getAddonLabel(t, addon) return (
{/* Icon */} @@ -148,7 +161,7 @@ function AddonRow({ addon, onToggle, t }: AddonRowProps) { {/* Info */}
- {addon.name} + {label.name} {isComingSoon && ( Coming Soon @@ -161,7 +174,7 @@ function AddonRow({ addon, onToggle, t }: AddonRowProps) { {addon.type === 'global' ? t('admin.addons.type.global') : t('admin.addons.type.trip')}
-

{addon.description}

+

{label.description}

{/* Toggle */} diff --git a/client/src/components/Admin/GitHubPanel.tsx b/client/src/components/Admin/GitHubPanel.tsx index c31f375..141b701 100644 --- a/client/src/components/Admin/GitHubPanel.tsx +++ b/client/src/components/Admin/GitHubPanel.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' import { Tag, Calendar, ExternalLink, ChevronDown, ChevronUp, Loader2, Heart, Coffee } from 'lucide-react' -import { useTranslation } from '../../i18n' +import { getLocaleForLanguage, useTranslation } from '../../i18n' import apiClient from '../../api/client' const REPO = 'mauriceboe/NOMAD' @@ -46,7 +46,7 @@ export default function GitHubPanel() { const formatDate = (dateStr) => { const d = new Date(dateStr) - return d.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', { day: 'numeric', month: 'short', year: 'numeric' }) + return d.toLocaleDateString(getLocaleForLanguage(language), { day: 'numeric', month: 'short', year: 'numeric' }) } // Simple markdown-to-html for release notes (handles headers, bold, lists, links) diff --git a/client/src/components/Dashboard/TimezoneWidget.tsx b/client/src/components/Dashboard/TimezoneWidget.tsx index 70ffff4..087937a 100644 --- a/client/src/components/Dashboard/TimezoneWidget.tsx +++ b/client/src/components/Dashboard/TimezoneWidget.tsx @@ -23,9 +23,9 @@ const POPULAR_ZONES = [ { label: 'Cairo', tz: 'Africa/Cairo' }, ] -function getTime(tz) { +function getTime(tz, locale) { try { - return new Date().toLocaleTimeString('de-DE', { timeZone: tz, hour: '2-digit', minute: '2-digit' }) + return new Date().toLocaleTimeString(locale, { timeZone: tz, hour: '2-digit', minute: '2-digit' }) } catch { return '—' } } @@ -41,7 +41,7 @@ function getOffset(tz) { } export default function TimezoneWidget() { - const { t } = useTranslation() + const { t, locale } = useTranslation() const [zones, setZones] = useState(() => { const saved = localStorage.getItem('dashboard_timezones') return saved ? JSON.parse(saved) : [ @@ -51,6 +51,9 @@ export default function TimezoneWidget() { }) const [now, setNow] = useState(Date.now()) const [showAdd, setShowAdd] = useState(false) + const [customLabel, setCustomLabel] = useState('') + const [customTz, setCustomTz] = useState('') + const [customError, setCustomError] = useState('') useEffect(() => { const i = setInterval(() => setNow(Date.now()), 10000) @@ -61,6 +64,20 @@ export default function TimezoneWidget() { localStorage.setItem('dashboard_timezones', JSON.stringify(zones)) }, [zones]) + const isValidTz = (tz: string) => { + try { Intl.DateTimeFormat('en-US', { timeZone: tz }).format(new Date()); return true } catch { return false } + } + + const addCustomZone = () => { + const tz = customTz.trim() + if (!tz) { setCustomError(t('dashboard.timezoneCustomErrorEmpty')); return } + if (!isValidTz(tz)) { setCustomError(t('dashboard.timezoneCustomErrorInvalid')); return } + if (zones.find(z => z.tz === tz)) { setCustomError(t('dashboard.timezoneCustomErrorDuplicate')); return } + const label = customLabel.trim() || tz.split('/').pop()?.replace(/_/g, ' ') || tz + setZones([...zones, { label, tz }]) + setCustomLabel(''); setCustomTz(''); setCustomError(''); setShowAdd(false) + } + const addZone = (zone) => { if (!zones.find(z => z.tz === zone.tz)) { setZones([...zones, zone]) @@ -70,7 +87,7 @@ export default function TimezoneWidget() { const removeZone = (tz) => setZones(zones.filter(z => z.tz !== tz)) - const localTime = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + const localTime = new Date().toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }) const rawZone = Intl.DateTimeFormat().resolvedOptions().timeZone const localZone = rawZone.split('/').pop().replace(/_/g, ' ') // Show abbreviated timezone name (e.g. CET, CEST, EST) @@ -96,7 +113,7 @@ export default function TimezoneWidget() { {zones.map(z => (
-

{getTime(z.tz)}

+

{getTime(z.tz, locale)}

{z.label} {getOffset(z.tz)}

+
+
+ {/* Popular zones */} {POPULAR_ZONES.filter(z => !zones.find(existing => existing.tz === z.tz)).map(z => ( ))} diff --git a/client/src/components/Layout/DemoBanner.tsx b/client/src/components/Layout/DemoBanner.tsx index 7a4386f..9d0bc20 100644 --- a/client/src/components/Layout/DemoBanner.tsx +++ b/client/src/components/Layout/DemoBanner.tsx @@ -86,6 +86,38 @@ const texts: Record = { selfHostLink: 'self-host it', close: 'Got it', }, + es: { + titleBefore: 'Bienvenido a ', + titleAfter: '', + title: 'Bienvenido a la demo de TREK', + description: 'Puedes ver, editar y crear viajes. Todos los cambios se restablecen automáticamente cada hora.', + resetIn: 'Próximo reinicio en', + minutes: 'minutos', + uploadNote: 'Las subidas de archivos (fotos, documentos, portadas) están desactivadas en el modo demo.', + fullVersionTitle: 'Además, en la versión completa:', + features: [ + 'Subida de archivos (fotos, documentos, portadas)', + 'Gestión de claves API (Google Maps, tiempo)', + 'Gestión de usuarios y permisos', + 'Copias de seguridad automáticas', + 'Gestión de addons (activar/desactivar)', + 'Inicio de sesión único OIDC / SSO', + ], + addonsTitle: 'Complementos modulares (se pueden desactivar en la versión completa)', + addons: [ + ['Vacaciones', 'Planificador de vacaciones con calendario, festivos y fusión de usuarios'], + ['Atlas', 'Mapa del mundo con países visitados y estadísticas de viaje'], + ['Equipaje', 'Listas de comprobación para cada viaje'], + ['Presupuesto', 'Control de gastos con reparto'], + ['Documentos', 'Adjunta archivos a los viajes'], + ['Widgets', 'Conversor de divisas y zonas horarias'], + ], + whatIs: '¿Qué es TREK?', + whatIsDesc: 'Un planificador de viajes autohospedado con colaboración en tiempo real, mapas interactivos, inicio de sesión OIDC y modo oscuro.', + selfHost: 'Código abierto — ', + selfHostLink: 'alójalo tú mismo', + close: 'Entendido', + }, } const featureIcons = [Upload, Key, Users, Database, Puzzle, Shield] @@ -159,7 +191,7 @@ export default function DemoBanner(): React.ReactElement | null {
- {language === 'de' ? 'Was ist ' : 'What is '}TREK? + {t.whatIs}

{t.whatIsDesc}

diff --git a/client/src/components/Layout/Navbar.tsx b/client/src/components/Layout/Navbar.tsx index 52ce0cf..0e85d14 100644 --- a/client/src/components/Layout/Navbar.tsx +++ b/client/src/components/Layout/Navbar.tsx @@ -67,6 +67,12 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }: updateSetting('dark_mode', dark ? 'light' : 'dark').catch(() => {}) } + const getAddonName = (addon: Addon): string => { + const key = `admin.addons.catalog.${addon.id}.name` + const translated = t(key) + return translated !== key ? translated : addon.name + } + return (