From 706548c45dea4adeabacec1cf6ee783ff953c54b Mon Sep 17 00:00:00 2001 From: Joaquin Date: Sat, 28 Mar 2026 22:56:17 +0100 Subject: [PATCH 1/4] feat: add full Spanish translation (#57) * feat(i18n): add spanish translation support * refactor(i18n): refine spanish copy for es-es * refactor(i18n): translate addon titles to spanish --- client/src/components/Admin/AddonManager.tsx | 17 +- client/src/components/Admin/GitHubPanel.tsx | 4 +- .../components/Dashboard/TimezoneWidget.tsx | 12 +- client/src/components/Layout/DemoBanner.tsx | 34 +- client/src/components/Layout/Navbar.tsx | 8 +- client/src/components/PDF/TripPDF.tsx | 4 +- client/src/components/Photos/PhotoGallery.tsx | 18 +- .../src/components/Photos/PhotoLightbox.tsx | 4 +- .../src/components/Planner/DayDetailPanel.tsx | 12 +- .../src/components/Planner/PlaceInspector.tsx | 2 +- .../src/components/Vacay/VacayMonthCard.tsx | 6 +- client/src/components/Vacay/VacaySettings.tsx | 6 +- client/src/i18n/TranslationContext.tsx | 22 +- client/src/i18n/index.ts | 2 +- client/src/i18n/translations/es.js | 1061 +++++++++++++++++ client/src/i18n/translations/es.ts | 1 + client/src/pages/AtlasPage.tsx | 7 +- client/src/pages/DashboardPage.tsx | 4 +- client/src/pages/LoginPage.tsx | 11 +- client/src/pages/SettingsPage.tsx | 1 + client/src/pages/TripPlannerPage.tsx | 2 +- client/src/pages/VacayPage.tsx | 2 +- 22 files changed, 1185 insertions(+), 55 deletions(-) create mode 100644 client/src/i18n/translations/es.js create mode 100644 client/src/i18n/translations/es.ts 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..5ee97e5 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) : [ @@ -70,7 +70,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 +96,7 @@ export default function TimezoneWidget() { {zones.map(z => (
-

{getTime(z.tz)}

+

{getTime(z.tz, locale)}

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

))}
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 (
${totalCost > 0 ? `
-
${totalCost.toLocaleString('de-DE')}
+
${totalCost.toLocaleString(loc)}
${escHtml(tr('pdf.costLabel'))}
` : ''} diff --git a/client/src/components/Photos/PhotoGallery.tsx b/client/src/components/Photos/PhotoGallery.tsx index fdbdf35..fe64a19 100644 --- a/client/src/components/Photos/PhotoGallery.tsx +++ b/client/src/components/Photos/PhotoGallery.tsx @@ -3,7 +3,7 @@ import { PhotoLightbox } from './PhotoLightbox' import { PhotoUpload } from './PhotoUpload' import { Upload, Camera } from 'lucide-react' import Modal from '../shared/Modal' -import { useTranslation } from '../../i18n' +import { getLocaleForLanguage, useTranslation } from '../../i18n' import type { Photo, Place, Day } from '../../types' interface PhotoGalleryProps { @@ -17,7 +17,7 @@ interface PhotoGalleryProps { } export default function PhotoGallery({ photos, onUpload, onDelete, onUpdate, places, days, tripId }: PhotoGalleryProps) { - const { t } = useTranslation() + const { t, language } = useTranslation() const [lightboxIndex, setLightboxIndex] = useState(null) const [showUpload, setShowUpload] = useState(false) const [filterDayId, setFilterDayId] = useState('') @@ -53,7 +53,7 @@ export default function PhotoGallery({ photos, onUpload, onDelete, onUpdate, pla

Fotos

- {photos.length} Foto{photos.length !== 1 ? 's' : ''} + {photos.length} {photos.length !== 1 ? 'Fotos' : 'Foto'}

@@ -65,7 +65,7 @@ export default function PhotoGallery({ photos, onUpload, onDelete, onUpdate, pla {(days || []).map(day => ( ))} @@ -84,7 +84,7 @@ export default function PhotoGallery({ photos, onUpload, onDelete, onUpdate, pla className="flex items-center gap-2 bg-slate-900 text-white px-4 py-2 rounded-lg hover:bg-slate-700 text-sm font-medium whitespace-nowrap" > - Fotos hochladen + {t('common.upload')} @@ -101,7 +101,7 @@ export default function PhotoGallery({ photos, onUpload, onDelete, onUpdate, pla style={{ display: 'inline-flex', margin: '0 auto' }} > - Fotos hochladen + {t('common.upload')} ) : ( @@ -146,7 +146,7 @@ export default function PhotoGallery({ photos, onUpload, onDelete, onUpdate, pla setShowUpload(false)} - title="Fotos hochladen" + title={t('common.upload')} size="lg" > s.settings.temperature_unit) === 'fahrenheit' const is12h = useSettingsStore(s => s.settings.time_format) === '12h' const fmtTime = (v) => formatTime12(v, is12h) @@ -138,7 +138,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri if (!day) return null const formattedDate = day.date ? new Date(day.date + 'T00:00:00').toLocaleDateString( - language === 'de' ? 'de-DE' : 'en-US', + getLocaleForLanguage(language), { weekday: 'long', day: 'numeric', month: 'long' } ) : null @@ -270,7 +270,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri {r.reservation_time?.includes('T') && ( - {new Date(r.reservation_time).toLocaleTimeString(language === 'de' ? 'de-DE' : 'en-US', { hour: '2-digit', minute: '2-digit', hour12: is12h })} + {new Date(r.reservation_time).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: is12h })} {r.reservation_end_time && ` – ${fmtTime(r.reservation_end_time)}`} )} @@ -423,7 +423,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri onChange={v => setHotelDayRange(prev => ({ start: v, end: Math.max(v, prev.end) }))} options={days.map((d, i) => ({ value: d.id, - label: `${d.title || t('planner.dayN', { n: i + 1 })}${d.date ? ` — ${new Date(d.date + 'T00:00:00').toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', { day: 'numeric', month: 'short' })}` : ''}`, + label: `${d.title || t('planner.dayN', { n: i + 1 })}${d.date ? ` — ${new Date(d.date + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short' })}` : ''}`, }))} size="sm" /> @@ -435,7 +435,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri onChange={v => setHotelDayRange(prev => ({ start: Math.min(prev.start, v), end: v }))} options={days.map((d, i) => ({ value: d.id, - label: `${d.title || t('planner.dayN', { n: i + 1 })}${d.date ? ` — ${new Date(d.date + 'T00:00:00').toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', { day: 'numeric', month: 'short' })}` : ''}`, + label: `${d.title || t('planner.dayN', { n: i + 1 })}${d.date ? ` — ${new Date(d.date + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short' })}` : ''}`, }))} size="sm" /> diff --git a/client/src/components/Planner/PlaceInspector.tsx b/client/src/components/Planner/PlaceInspector.tsx index e1cf7ae..a6b993e 100644 --- a/client/src/components/Planner/PlaceInspector.tsx +++ b/client/src/components/Planner/PlaceInspector.tsx @@ -312,7 +312,7 @@ export default function PlaceInspector({ icon={} text={<> {googleDetails.rating.toFixed(1)} - {googleDetails.rating_count ? ({googleDetails.rating_count.toLocaleString('de-DE')}) : ''} + {googleDetails.rating_count ? ({googleDetails.rating_count.toLocaleString(locale)}) : ''} {shortReview && · „{shortReview.text}"} } color="var(--text-secondary)" bg="var(--bg-hover)" diff --git a/client/src/components/Vacay/VacayMonthCard.tsx b/client/src/components/Vacay/VacayMonthCard.tsx index a3f7894..a012d0d 100644 --- a/client/src/components/Vacay/VacayMonthCard.tsx +++ b/client/src/components/Vacay/VacayMonthCard.tsx @@ -5,8 +5,10 @@ import type { HolidaysMap, VacayEntry } from '../../types' const WEEKDAYS_EN = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'] const WEEKDAYS_DE = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'] +const WEEKDAYS_ES = ['Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sa', 'Do'] const MONTHS_EN = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] const MONTHS_DE = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'] +const MONTHS_ES = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'] interface VacayMonthCardProps { year: number @@ -25,8 +27,8 @@ export default function VacayMonthCard({ onCellClick, companyMode, blockWeekends }: VacayMonthCardProps) { const { language } = useTranslation() - const weekdays = language === 'de' ? WEEKDAYS_DE : WEEKDAYS_EN - const monthNames = language === 'de' ? MONTHS_DE : MONTHS_EN + const weekdays = language === 'de' ? WEEKDAYS_DE : language === 'es' ? WEEKDAYS_ES : WEEKDAYS_EN + const monthNames = language === 'de' ? MONTHS_DE : language === 'es' ? MONTHS_ES : MONTHS_EN const weeks = useMemo(() => { const firstDay = new Date(year, month, 1) diff --git a/client/src/components/Vacay/VacaySettings.tsx b/client/src/components/Vacay/VacaySettings.tsx index c700eff..626f29e 100644 --- a/client/src/components/Vacay/VacaySettings.tsx +++ b/client/src/components/Vacay/VacaySettings.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' import { MapPin, CalendarOff, AlertCircle, Building2, Unlink, ArrowRightLeft, Globe } from 'lucide-react' import { useVacayStore } from '../../store/vacayStore' -import { useTranslation } from '../../i18n' +import { getIntlLanguage, useTranslation } from '../../i18n' import { useToast } from '../shared/Toast' import CustomSelect from '../shared/CustomSelect' import apiClient from '../../api/client' @@ -24,7 +24,7 @@ export default function VacaySettings({ onClose }: VacaySettingsProps) { useEffect(() => { apiClient.get('/addons/vacay/holidays/countries').then(r => { let displayNames - try { displayNames = new Intl.DisplayNames([language === 'de' ? 'de' : 'en'], { type: 'region' }) } catch { /* */ } + try { displayNames = new Intl.DisplayNames([getIntlLanguage(language)], { type: 'region' }) } catch { /* */ } const list = r.data.map(c => ({ value: c.countryCode, label: displayNames ? (displayNames.of(c.countryCode) || c.name) : c.name, @@ -49,7 +49,7 @@ export default function VacaySettings({ onClose }: VacaySettingsProps) { }) if (allCounties.size > 0) { let subdivisionNames - try { subdivisionNames = new Intl.DisplayNames([language === 'de' ? 'de' : 'en'], { type: 'region' }) } catch { /* */ } + try { subdivisionNames = new Intl.DisplayNames([getIntlLanguage(language)], { type: 'region' }) } catch { /* */ } const regionList = [...allCounties].sort().map(c => { let label = c.split('-')[1] || c // Try Intl for full subdivision name (not all browsers support subdivision codes) diff --git a/client/src/i18n/TranslationContext.tsx b/client/src/i18n/TranslationContext.tsx index ffa7bdb..408bdf7 100644 --- a/client/src/i18n/TranslationContext.tsx +++ b/client/src/i18n/TranslationContext.tsx @@ -2,10 +2,20 @@ import React, { createContext, useContext, useMemo, ReactNode } from 'react' import { useSettingsStore } from '../store/settingsStore' import de from './translations/de' import en from './translations/en' +import es from './translations/es' type TranslationStrings = Record -const translations: Record = { de, en } +const translations: Record = { de, en, es } +const LOCALES: Record = { de: 'de-DE', en: 'en-US', es: 'es-ES' } + +export function getLocaleForLanguage(language: string): string { + return LOCALES[language] || LOCALES.en +} + +export function getIntlLanguage(language: string): string { + return language === 'de' || language === 'es' ? language : 'en' +} interface TranslationContextValue { t: (key: string, params?: Record) => string @@ -13,18 +23,18 @@ interface TranslationContextValue { locale: string } -const TranslationContext = createContext({ t: (k: string) => k, language: 'de', locale: 'de-DE' }) +const TranslationContext = createContext({ t: (k: string) => k, language: 'en', locale: 'en-US' }) interface TranslationProviderProps { children: ReactNode } export function TranslationProvider({ children }: TranslationProviderProps) { - const language = useSettingsStore((s) => s.settings.language) || 'de' + const language = useSettingsStore((s) => s.settings.language) || 'en' const value = useMemo((): TranslationContextValue => { - const strings = translations[language] || translations.de - const fallback = translations.de + const strings = translations[language] || translations.en + const fallback = translations.en function t(key: string, params?: Record): string { let val: string = strings[key] ?? fallback[key] ?? key @@ -36,7 +46,7 @@ export function TranslationProvider({ children }: TranslationProviderProps) { return val } - return { t, language, locale: language === 'en' ? 'en-US' : 'de-DE' } + return { t, language, locale: getLocaleForLanguage(language) } }, [language]) return {children} diff --git a/client/src/i18n/index.ts b/client/src/i18n/index.ts index 957f127..31d9369 100644 --- a/client/src/i18n/index.ts +++ b/client/src/i18n/index.ts @@ -1 +1 @@ -export { TranslationProvider, useTranslation } from './TranslationContext' +export { TranslationProvider, useTranslation, getLocaleForLanguage, getIntlLanguage } from './TranslationContext' diff --git a/client/src/i18n/translations/es.js b/client/src/i18n/translations/es.js new file mode 100644 index 0000000..2146dfb --- /dev/null +++ b/client/src/i18n/translations/es.js @@ -0,0 +1,1061 @@ +const es = { + // Common + 'common.save': 'Guardar', + 'common.cancel': 'Cancelar', + 'common.delete': 'Eliminar', + 'common.edit': 'Editar', + 'common.add': 'Añadir', + 'common.loading': 'Cargando...', + 'common.error': 'Error', + 'common.back': 'Atrás', + 'common.all': 'Todo', + 'common.close': 'Cerrar', + 'common.open': 'Abrir', + 'common.upload': 'Subir', + 'common.search': 'Buscar', + 'common.confirm': 'Confirmar', + 'common.ok': 'Aceptar', + 'common.yes': 'Sí', + 'common.no': 'No', + 'common.or': 'o', + 'common.none': 'Ninguno', + 'common.date': 'Fecha', + 'common.rename': 'Renombrar', + 'common.name': 'Nombre', + 'common.email': 'Correo', + 'common.password': 'Contraseña', + 'common.saving': 'Guardando...', + 'common.update': 'Actualizar', + 'common.change': 'Cambiar', + 'common.uploading': 'Subiendo…', + 'common.backToPlanning': 'Volver a la planificación', + 'common.reset': 'Restablecer', + + // Navbar + 'nav.trip': 'Viaje', + 'nav.share': 'Compartir', + 'nav.settings': 'Ajustes', + 'nav.admin': 'Administración', + 'nav.logout': 'Cerrar sesión', + 'nav.lightMode': 'Modo claro', + 'nav.darkMode': 'Modo oscuro', + 'nav.autoMode': 'Modo automático', + 'nav.administrator': 'Administrador', + 'nav.myTrips': 'Mis viajes', + + // Dashboard + 'dashboard.title': 'Mis viajes', + 'dashboard.subtitle.loading': 'Cargando viajes...', + 'dashboard.subtitle.trips': '{count} viajes ({archived} archivados)', + 'dashboard.subtitle.empty': 'Empieza tu primer viaje', + 'dashboard.subtitle.activeOne': '{count} viaje activo', + 'dashboard.subtitle.activeMany': '{count} viajes activos', + 'dashboard.subtitle.archivedSuffix': ' · {count} archivados', + 'dashboard.newTrip': 'Nuevo viaje', + 'dashboard.currency': 'Divisa', + 'dashboard.timezone': 'Zonas horarias', + 'dashboard.localTime': 'Hora local', + 'dashboard.emptyTitle': 'Aún no hay viajes', + 'dashboard.emptyText': 'Crea tu primer viaje y empieza a planificar', + 'dashboard.emptyButton': 'Crear primer viaje', + 'dashboard.nextTrip': 'Próximo viaje', + 'dashboard.shared': 'Compartido', + 'dashboard.sharedBy': 'Compartido por {name}', + 'dashboard.days': 'Días', + 'dashboard.places': 'Lugares', + 'dashboard.archive': 'Archivar', + 'dashboard.restore': 'Restaurar', + 'dashboard.archived': 'Archivado', + 'dashboard.status.ongoing': 'En curso', + 'dashboard.status.today': 'Hoy', + 'dashboard.status.tomorrow': 'Mañana', + 'dashboard.status.past': 'Pasado', + 'dashboard.status.daysLeft': 'Quedan {count} días', + 'dashboard.toast.loadError': 'No se pudieron cargar los viajes', + 'dashboard.toast.created': '¡Viaje creado correctamente!', + 'dashboard.toast.createError': 'No se pudo crear el viaje', + 'dashboard.toast.updated': '¡Viaje actualizado!', + 'dashboard.toast.updateError': 'No se pudo actualizar el viaje', + 'dashboard.toast.deleted': 'Viaje eliminado', + 'dashboard.toast.deleteError': 'No se pudo eliminar el viaje', + 'dashboard.toast.archived': 'Viaje archivado', + 'dashboard.toast.archiveError': 'No se pudo archivar el viaje', + 'dashboard.toast.restored': 'Viaje restaurado', + 'dashboard.toast.restoreError': 'No se pudo restaurar el viaje', + 'dashboard.confirm.delete': '¿Eliminar el viaje "{title}"? Todos los lugares y planes se borrarán permanentemente.', + 'dashboard.editTrip': 'Editar viaje', + 'dashboard.createTrip': 'Crear nuevo viaje', + 'dashboard.tripTitle': 'Título', + 'dashboard.tripTitlePlaceholder': 'p. ej. Verano en Japón', + 'dashboard.tripDescription': 'Descripción', + 'dashboard.tripDescriptionPlaceholder': '¿De qué trata este viaje?', + 'dashboard.startDate': 'Fecha de inicio', + 'dashboard.endDate': 'Fecha de fin', + 'dashboard.noDateHint': 'Sin fecha definida: se crearán 7 días por defecto. Puedes cambiarlo cuando quieras.', + 'dashboard.coverImage': 'Imagen de portada', + 'dashboard.addCoverImage': 'Añadir imagen de portada', + 'dashboard.coverSaved': 'Imagen de portada guardada', + 'dashboard.coverUploadError': 'Error al subir la imagen', + 'dashboard.coverRemoveError': 'Error al eliminar la imagen', + 'dashboard.titleRequired': 'El título es obligatorio', + 'dashboard.endDateError': 'La fecha de fin debe ser posterior a la de inicio', + + // Settings + 'settings.title': 'Ajustes', + 'settings.subtitle': 'Configura tus ajustes personales', + 'settings.map': 'Mapa', + 'settings.mapTemplate': 'Plantilla del mapa', + 'settings.mapTemplatePlaceholder.select': 'Seleccionar plantilla...', + 'settings.mapDefaultHint': 'Déjalo vacío para OpenStreetMap (por defecto)', + 'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + 'settings.mapHint': 'Plantilla de URL para los mosaicos del mapa', + 'settings.latitude': 'Latitud', + 'settings.longitude': 'Longitud', + 'settings.saveMap': 'Guardar mapa', + 'settings.apiKeys': 'Claves API', + 'settings.mapsKey': 'Clave API de Google Maps', + 'settings.mapsKeyHint': 'Necesaria para buscar lugares. Consíguela en console.cloud.google.com', + 'settings.weatherKey': 'Clave API de OpenWeatherMap', + 'settings.weatherKeyHint': 'Para datos meteorológicos. Gratis en openweathermap.org/api', + 'settings.keyPlaceholder': 'Introduce la clave...', + 'settings.configured': 'Configurado', + 'settings.saveKeys': 'Guardar claves', + 'settings.display': 'Visualización', + 'settings.colorMode': 'Modo de color', + 'settings.light': 'Claro', + 'settings.dark': 'Oscuro', + 'settings.auto': 'Automático', + 'settings.language': 'Idioma', + 'settings.temperature': 'Unidad de temperatura', + 'settings.timeFormat': 'Formato de hora', + 'settings.routeCalculation': 'Cálculo de ruta', + 'settings.on': 'Activado', + 'settings.off': 'Desactivado', + 'settings.account': 'Cuenta', + 'settings.username': 'Usuario', + 'settings.email': 'Correo', + 'settings.role': 'Rol', + 'settings.roleAdmin': 'Administrador', + 'settings.oidcLinked': 'Vinculado con', + 'settings.changePassword': 'Cambiar contraseña', + 'settings.currentPassword': 'Contraseña actual', + 'settings.newPassword': 'Nueva contraseña', + 'settings.confirmPassword': 'Confirmar nueva contraseña', + 'settings.updatePassword': 'Actualizar contraseña', + 'settings.passwordRequired': 'Introduce la contraseña actual y la nueva', + 'settings.passwordTooShort': 'La contraseña debe tener al menos 8 caracteres', + 'settings.passwordMismatch': 'Las contraseñas no coinciden', + 'settings.passwordChanged': 'Contraseña cambiada correctamente', + 'settings.deleteAccount': 'Eliminar cuenta', + 'settings.deleteAccountTitle': '¿Eliminar tu cuenta?', + 'settings.deleteAccountWarning': 'Tu cuenta y todos tus viajes, lugares y archivos se eliminarán permanentemente. Esta acción no se puede deshacer.', + 'settings.deleteAccountConfirm': 'Eliminar permanentemente', + 'settings.deleteBlockedTitle': 'No es posible eliminarla', + 'settings.deleteBlockedMessage': 'Eres el único administrador. Asciende a otro usuario a administrador antes de eliminar tu cuenta.', + 'settings.roleUser': 'Usuario', + 'settings.saveProfile': 'Guardar perfil', + 'settings.toast.mapSaved': 'Ajustes del mapa guardados', + 'settings.toast.keysSaved': 'Claves API guardadas', + 'settings.toast.displaySaved': 'Ajustes de visualización guardados', + 'settings.toast.profileSaved': 'Perfil guardado', + 'settings.uploadAvatar': 'Subir foto de perfil', + 'settings.removeAvatar': 'Eliminar foto de perfil', + 'settings.avatarUploaded': 'Foto de perfil actualizada', + 'settings.avatarRemoved': 'Foto de perfil eliminada', + 'settings.avatarError': 'Falló la subida', + + // Login + 'login.error': 'Inicio de sesión fallido. Revisa tus credenciales.', + 'login.tagline': 'Tus viajes.\nTu plan.', + 'login.description': 'Planifica viajes en colaboración con mapas interactivos, presupuestos y sincronización en tiempo real.', + 'login.features.maps': 'Mapas interactivos', + 'login.features.mapsDesc': 'Google Places, rutas y agrupación', + 'login.features.realtime': 'Sincronización en tiempo real', + 'login.features.realtimeDesc': 'Planificad juntos mediante WebSocket', + 'login.features.budget': 'Control de presupuesto', + 'login.features.budgetDesc': 'Categorías, gráficos y costes por persona', + 'login.features.collab': 'Colaboración', + 'login.features.collabDesc': 'Multiusuario con viajes compartidos', + 'login.features.packing': 'Listas de equipaje', + 'login.features.packingDesc': 'Categorías, progreso y sugerencias', + 'login.features.bookings': 'Reservas', + 'login.features.bookingsDesc': 'Vuelos, hoteles, restaurantes y más', + 'login.features.files': 'Documentos', + 'login.features.filesDesc': 'Sube y gestiona documentos', + 'login.features.routes': 'Rutas inteligentes', + 'login.features.routesDesc': 'Optimización automática y exportación a Google Maps', + 'login.selfHosted': 'Autoalojado · Código abierto · Tus datos siguen siendo tuyos', + 'login.title': 'Iniciar sesión', + 'login.subtitle': 'Bienvenido de nuevo', + 'login.signingIn': 'Iniciando sesión…', + 'login.signIn': 'Entrar', + 'login.createAdmin': 'Crear cuenta de administrador', + 'login.createAdminHint': 'Configura la primera cuenta administradora de NOMAD.', + 'login.createAccount': 'Crear cuenta', + 'login.createAccountHint': 'Crea una cuenta nueva.', + 'login.creating': 'Creando…', + 'login.noAccount': '¿No tienes cuenta?', + 'login.hasAccount': '¿Ya tienes cuenta?', + 'login.register': 'Registrarse', + 'login.emailPlaceholder': 'tu@correo.com', + 'login.username': 'Usuario', + 'login.oidc.registrationDisabled': 'El registro está desactivado. Contacta con tu administrador.', + 'login.oidc.noEmail': 'No se recibió ningún correo del proveedor.', + 'login.oidc.tokenFailed': 'La autenticación falló.', + 'login.oidc.invalidState': 'Sesión no válida. Inténtalo de nuevo.', + 'login.demoFailed': 'Falló el acceso a la demo', + 'login.oidcSignIn': 'Entrar con {name}', + 'login.demoHint': 'Prueba la demo: no necesitas registrarte', + + // Register + 'register.passwordMismatch': 'Las contraseñas no coinciden', + 'register.passwordTooShort': 'La contraseña debe tener al menos 6 caracteres', + 'register.failed': 'Falló el registro', + 'register.getStarted': 'Empezar', + 'register.subtitle': 'Crea una cuenta y empieza a planificar tus viajes.', + 'register.feature1': 'Planes de viaje ilimitados', + 'register.feature2': 'Vista de mapa interactiva', + 'register.feature3': 'Gestiona lugares y categorías', + 'register.feature4': 'Haz seguimiento de las reservas', + 'register.feature5': 'Crea listas de equipaje', + 'register.feature6': 'Guarda fotos y archivos', + 'register.createAccount': 'Crear cuenta', + 'register.startPlanning': 'Empieza a planificar tu viaje', + 'register.minChars': 'Mín. 6 caracteres', + 'register.confirmPassword': 'Confirmar contraseña', + 'register.repeatPassword': 'Repetir contraseña', + 'register.registering': 'Registrando...', + 'register.register': 'Registrarse', + 'register.hasAccount': '¿Ya tienes cuenta?', + 'register.signIn': 'Iniciar sesión', + + // Admin + 'admin.title': 'Administración', + 'admin.subtitle': 'Gestión de usuarios y ajustes del sistema', + 'admin.tabs.users': 'Usuarios', + 'admin.tabs.categories': 'Categorías', + 'admin.tabs.backup': 'Copia de seguridad', + 'admin.stats.users': 'Usuarios', + 'admin.stats.trips': 'Viajes', + 'admin.stats.places': 'Lugares', + 'admin.stats.photos': 'Fotos', + 'admin.stats.files': 'Archivos', + 'admin.table.user': 'Usuario', + 'admin.table.email': 'Correo', + 'admin.table.role': 'Rol', + 'admin.table.created': 'Creado', + 'admin.table.lastLogin': 'Último acceso', + 'admin.table.actions': 'Acciones', + 'admin.you': '(Tú)', + 'admin.editUser': 'Editar usuario', + 'admin.newPassword': 'Nueva contraseña', + 'admin.newPasswordHint': 'Déjalo vacío para mantener la contraseña actual', + 'admin.deleteUser': '¿Eliminar al usuario "{name}"? Todos sus viajes se borrarán permanentemente.', + 'admin.deleteUserTitle': 'Eliminar usuario', + 'admin.newPasswordPlaceholder': 'Introduce una nueva contraseña…', + 'admin.toast.loadError': 'No se pudieron cargar los datos de administración', + 'admin.toast.userUpdated': 'Usuario actualizado', + 'admin.toast.updateError': 'No se pudo actualizar', + 'admin.toast.userDeleted': 'Usuario eliminado', + 'admin.toast.deleteError': 'No se pudo eliminar', + 'admin.toast.cannotDeleteSelf': 'No puedes eliminar tu propia cuenta', + 'admin.toast.userCreated': 'Usuario creado', + 'admin.toast.createError': 'No se pudo crear el usuario', + 'admin.toast.fieldsRequired': 'Usuario, correo y contraseña son obligatorios', + 'admin.createUser': 'Crear usuario', + 'admin.tabs.settings': 'Ajustes', + 'admin.allowRegistration': 'Permitir el registro', + 'admin.allowRegistrationHint': 'Los nuevos usuarios pueden registrarse por sí mismos', + 'admin.apiKeys': 'Claves API', + 'admin.apiKeysHint': 'Opcional. Activa datos ampliados de lugares, como fotos y previsión del tiempo.', + 'admin.mapsKey': 'Clave API de Google Maps', + 'admin.mapsKeyHint': 'Obligatoria para buscar lugares. Consíguela en console.cloud.google.com', + 'admin.mapsKeyHintLong': 'Sin una clave API, la búsqueda de lugares usa OpenStreetMap. Con una clave de Google también se pueden cargar fotos, valoraciones y horarios de apertura. Consíguela en console.cloud.google.com.', + 'admin.recommended': 'Recomendado', + 'admin.weatherKey': 'Clave API de OpenWeatherMap', + 'admin.weatherKeyHint': 'Para datos meteorológicos. Gratis en openweathermap.org', + 'admin.validateKey': 'Probar', + 'admin.keyValid': 'Conectado', + 'admin.keyInvalid': 'No válida', + 'admin.keySaved': 'Claves API guardadas', + 'admin.oidcTitle': 'Inicio de sesión único (OIDC)', + 'admin.oidcSubtitle': 'Permite iniciar sesión mediante proveedores externos como Google, Apple, Authentik o Keycloak.', + 'admin.oidcDisplayName': 'Nombre visible', + 'admin.oidcIssuer': 'URL del emisor', + 'admin.oidcIssuerHint': 'La URL Issuer de OpenID Connect del proveedor. Ej.: https://accounts.google.com', + 'admin.oidcSaved': 'Configuración OIDC guardada', + + // File Types + 'admin.fileTypes': 'Tipos de archivo permitidos', + 'admin.fileTypesHint': 'Configura qué tipos de archivo pueden subir los usuarios.', + 'admin.fileTypesFormat': 'Extensiones separadas por comas (p. ej. jpg,png,pdf,doc). Usa * para permitir todos los tipos.', + 'admin.fileTypesSaved': 'Ajustes de tipos de archivo guardados', + + // Addons + 'admin.tabs.addons': 'Complementos', + 'admin.addons.title': 'Complementos', + 'admin.addons.subtitle': 'Activa o desactiva funciones para personalizar tu experiencia en NOMAD.', + 'admin.addons.subtitleBefore': 'Activa o desactiva funciones para personalizar tu experiencia en ', + 'admin.addons.subtitleAfter': '.', + 'admin.addons.enabled': 'Activo', + 'admin.addons.disabled': 'Desactivado', + 'admin.addons.type.trip': 'Viaje', + 'admin.addons.type.global': 'Global', + 'admin.addons.tripHint': 'Disponible como pestaña dentro de cada viaje', + 'admin.addons.globalHint': 'Disponible como sección independiente en la navegación principal', + 'admin.addons.toast.updated': 'Complemento actualizado', + 'admin.addons.toast.error': 'No se pudo actualizar el complemento', + 'admin.addons.noAddons': 'No hay complementos disponibles', + 'admin.weather.title': 'Datos meteorológicos', + 'admin.weather.badge': 'Desde el 24 de marzo de 2026', + 'admin.weather.description': 'NOMAD utiliza Open-Meteo como fuente de datos meteorológicos. Open-Meteo es un servicio meteorológico gratuito y de código abierto: no requiere clave API.', + 'admin.weather.forecast': 'Pronóstico de 16 días', + 'admin.weather.forecastDesc': 'Antes eran 5 días (OpenWeatherMap)', + 'admin.weather.climate': 'Datos climáticos históricos', + 'admin.weather.climateDesc': 'Promedios de los últimos 85 años para fechas posteriores al pronóstico de 16 días', + 'admin.weather.requests': '10.000 solicitudes / día', + 'admin.weather.requestsDesc': 'Gratis, sin necesidad de clave API', + 'admin.weather.locationHint': 'El tiempo se basa en el primer lugar con coordenadas de cada día. Si no hay ningún lugar asignado a un día, se usa como referencia cualquier lugar de la lista.', + + // GitHub + 'admin.tabs.github': 'GitHub', + 'admin.github.title': 'Historial de versiones', + 'admin.github.subtitle': 'Últimas novedades de {repo}', + 'admin.github.latest': 'Última', + 'admin.github.prerelease': 'Prelanzamiento', + 'admin.github.showDetails': 'Mostrar detalles', + 'admin.github.hideDetails': 'Ocultar detalles', + 'admin.github.loadMore': 'Cargar más', + 'admin.github.loading': 'Cargando...', + 'admin.github.error': 'No se pudieron cargar las versiones', + 'admin.github.by': 'por', + 'admin.update.available': 'Actualización disponible', + 'admin.update.text': 'NOMAD {version} está disponible. Estás usando {current}.', + 'admin.update.button': 'Ver en GitHub', + 'admin.update.install': 'Instalar actualización', + 'admin.update.confirmTitle': '¿Instalar actualización?', + 'admin.update.confirmText': 'NOMAD se actualizará de {current} a {version}. Después, el servidor se reiniciará automáticamente.', + 'admin.update.dataInfo': 'Todos tus datos (viajes, usuarios, claves API, subidas, Vacay, Atlas, presupuestos) se conservarán.', + 'admin.update.warning': 'La app estará brevemente no disponible durante el reinicio.', + 'admin.update.confirm': 'Actualizar ahora', + 'admin.update.installing': 'Actualizando…', + 'admin.update.success': '¡Actualización instalada! El servidor se está reiniciando…', + 'admin.update.failed': 'La actualización falló', + 'admin.update.backupHint': 'Recomendamos crear una copia de seguridad antes de actualizar.', + 'admin.update.backupLink': 'Ir a Copia de seguridad', + 'admin.update.howTo': 'Cómo actualizar', + 'admin.update.dockerText': 'Tu instancia de NOMAD se ejecuta en Docker. Para actualizar a {version}, ejecuta los siguientes comandos en tu servidor:', + 'admin.update.reloadHint': 'Recarga la página en unos segundos.', + + // Vacay addon + 'vacay.subtitle': 'Planifica y gestiona días de vacaciones', + 'vacay.settings': 'Ajustes', + 'vacay.year': 'Año', + 'vacay.addYear': 'Añadir año', + 'vacay.removeYear': 'Eliminar año', + 'vacay.removeYearConfirm': '¿Eliminar {year}?', + 'vacay.removeYearHint': 'Todas las vacaciones y festivos de empresa de este año se borrarán permanentemente.', + 'vacay.remove': 'Eliminar', + 'vacay.persons': 'Personas', + 'vacay.noPersons': 'No se han añadido personas', + 'vacay.addPerson': 'Añadir persona', + 'vacay.editPerson': 'Editar persona', + 'vacay.removePerson': 'Eliminar persona', + 'vacay.removePersonConfirm': '¿Eliminar a {name}?', + 'vacay.removePersonHint': 'Todas las vacaciones de esta persona se borrarán permanentemente.', + 'vacay.personName': 'Nombre', + 'vacay.personNamePlaceholder': 'Introduce un nombre', + 'vacay.color': 'Color', + 'vacay.add': 'Añadir', + 'vacay.legend': 'Leyenda', + 'vacay.publicHoliday': 'Festivo', + 'vacay.companyHoliday': 'Festivo de empresa', + 'vacay.weekend': 'Fin de semana', + 'vacay.modeVacation': 'Vacaciones', + 'vacay.modeCompany': 'Festivo de empresa', + 'vacay.entitlement': 'Derecho', + 'vacay.entitlementDays': 'Días', + 'vacay.used': 'Usados', + 'vacay.remaining': 'Restantes', + 'vacay.carriedOver': 'de {year}', + 'vacay.blockWeekends': 'Bloquear fines de semana', + 'vacay.blockWeekendsHint': 'Impide marcar vacaciones en sábados y domingos', + 'vacay.publicHolidays': 'Festivos', + 'vacay.publicHolidaysHint': 'Marcar festivos en el calendario', + 'vacay.selectCountry': 'Seleccionar país', + 'vacay.selectRegion': 'Seleccionar región (opcional)', + 'vacay.companyHolidays': 'Festivos de empresa', + 'vacay.companyHolidaysHint': 'Permitir marcar días festivos comunes de la empresa', + 'vacay.companyHolidaysNoDeduct': 'Los festivos de empresa no descuentan días de vacaciones.', + 'vacay.carryOver': 'Arrastrar saldo', + 'vacay.carryOverHint': 'Trasladar automáticamente los días restantes al año siguiente', + 'vacay.sharing': 'Compartir', + 'vacay.sharingHint': 'Comparte tu calendario de vacaciones con otros usuarios de NOMAD', + 'vacay.owner': 'Propietario', + 'vacay.shareEmailPlaceholder': 'Correo electrónico del usuario de NOMAD', + 'vacay.shareSuccess': 'Plan compartido correctamente', + 'vacay.shareError': 'No se pudo compartir el plan', + 'vacay.dissolve': 'Deshacer fusión', + 'vacay.dissolveHint': 'Separar de nuevo los calendarios. Tus entradas se conservarán.', + 'vacay.dissolveAction': 'Disolver', + 'vacay.dissolved': 'Calendario separado', + 'vacay.fusedWith': 'Fusionado con', + 'vacay.you': 'tú', + 'vacay.noData': 'Sin datos', + 'vacay.changeColor': 'Cambiar color', + 'vacay.inviteUser': 'Invitar usuario', + 'vacay.inviteHint': 'Invita a otro usuario de NOMAD a compartir un calendario combinado de vacaciones.', + 'vacay.selectUser': 'Seleccionar usuario', + 'vacay.sendInvite': 'Enviar invitación', + 'vacay.inviteSent': 'Invitación enviada', + 'vacay.inviteError': 'No se pudo enviar la invitación', + 'vacay.pending': 'pendiente', + 'vacay.noUsersAvailable': 'No hay usuarios disponibles', + 'vacay.accept': 'Aceptar', + 'vacay.decline': 'Rechazar', + 'vacay.acceptFusion': 'Aceptar y fusionar', + 'vacay.inviteTitle': 'Solicitud de fusión', + 'vacay.inviteWantsToFuse': 'quiere compartir un calendario de vacaciones contigo.', + 'vacay.fuseInfo1': 'Ambos veréis todas las entradas de vacaciones en un único calendario compartido.', + 'vacay.fuseInfo2': 'Ambas partes pueden crear y editar entradas mutuamente.', + 'vacay.fuseInfo3': 'Ambas partes pueden borrar entradas y cambiar el número de días de vacaciones disponibles.', + 'vacay.fuseInfo4': 'Ajustes como festivos y festivos de empresa se comparten.', + 'vacay.fuseInfo5': 'La fusión puede disolverse en cualquier momento por cualquiera de las partes. Tus entradas se conservarán.', + + // Atlas addon + 'atlas.subtitle': 'Tu huella viajera por el mundo', + 'atlas.countries': 'Países', + 'atlas.trips': 'Viajes', + 'atlas.places': 'Lugares', + 'atlas.days': 'Días', + 'atlas.visitedCountries': 'Países visitados', + 'atlas.cities': 'Ciudades', + 'atlas.noData': 'Aún no hay datos de viaje', + 'atlas.noDataHint': 'Crea un viaje y añade lugares para ver tu mapa del mundo', + 'atlas.lastTrip': 'Último viaje', + 'atlas.nextTrip': 'Próximo viaje', + 'atlas.daysLeft': 'días restantes', + 'atlas.streak': 'Racha', + 'atlas.year': 'año', + 'atlas.years': 'años', + 'atlas.yearInRow': 'año seguido', + 'atlas.yearsInRow': 'años seguidos', + 'atlas.tripIn': 'viaje en', + 'atlas.tripsIn': 'viajes en', + 'atlas.since': 'desde', + 'atlas.europe': 'Europa', + 'atlas.asia': 'Asia', + 'atlas.northAmerica': 'América del Norte', + 'atlas.southAmerica': 'América del Sur', + 'atlas.africa': 'África', + 'atlas.oceania': 'Oceanía', + 'atlas.other': 'Otros', + 'atlas.firstVisit': 'Primer viaje', + 'atlas.lastVisitLabel': 'Último viaje', + 'atlas.tripSingular': 'Viaje', + 'atlas.tripPlural': 'Viajes', + 'atlas.placeVisited': 'Lugar visitado', + 'atlas.placesVisited': 'Lugares visitados', + + // Trip Planner + 'trip.tabs.plan': 'Plan', + 'trip.tabs.reservations': 'Reservas', + 'trip.tabs.reservationsShort': 'Reservas', + 'trip.tabs.packing': 'Lista de equipaje', + 'trip.tabs.packingShort': 'Equipaje', + 'trip.tabs.budget': 'Presupuesto', + 'trip.tabs.memories': 'Recuerdos', + 'trip.tabs.files': 'Archivos', + 'trip.loading': 'Cargando viaje...', + 'trip.mobilePlan': 'Plan', + 'trip.mobilePlaces': 'Lugares', + 'trip.toast.placeUpdated': 'Lugar actualizado', + 'trip.toast.placeAdded': 'Lugar añadido', + 'trip.toast.placeDeleted': 'Lugar eliminado', + 'trip.toast.selectDay': 'Selecciona primero un día', + 'trip.toast.assignedToDay': 'Lugar asignado al día', + 'trip.toast.reorderError': 'No se pudo reordenar', + 'trip.toast.reservationUpdated': 'Reserva actualizada', + 'trip.toast.reservationAdded': 'Reserva añadida', + 'trip.toast.deleted': 'Eliminado', + 'trip.confirm.deletePlace': '¿Seguro que quieres eliminar este lugar?', + + // Day Plan Sidebar + 'dayplan.emptyDay': 'No hay lugares planificados para este día', + 'dayplan.addNote': 'Añadir nota', + 'dayplan.editNote': 'Editar nota', + 'dayplan.noteAdd': 'Añadir nota', + 'dayplan.noteEdit': 'Editar nota', + 'dayplan.noteTitle': 'Nota', + 'dayplan.noteSubtitle': 'Nota diaria', + 'dayplan.totalCost': 'Coste total', + 'dayplan.days': 'Días', + 'dayplan.dayN': 'Día {n}', + 'dayplan.calculating': 'Calculando...', + 'dayplan.route': 'Ruta', + 'dayplan.optimize': 'Optimizar', + 'dayplan.optimized': 'Ruta optimizada', + 'dayplan.routeError': 'No se pudo calcular la ruta', + 'dayplan.toast.needTwoPlaces': 'Se necesitan al menos dos lugares para optimizar la ruta', + 'dayplan.toast.routeOptimized': 'Ruta optimizada', + 'dayplan.toast.noGeoPlaces': 'No se encontraron lugares con coordenadas para calcular la ruta', + 'dayplan.confirmed': 'Confirmado', + 'dayplan.pendingRes': 'Pendiente', + 'dayplan.pdf': 'PDF', + 'dayplan.pdfTooltip': 'Exportar plan diario como PDF', + 'dayplan.pdfError': 'No se pudo exportar el PDF', + + // Places Sidebar + 'places.addPlace': 'Añadir lugar/actividad', + 'places.assignToDay': '¿A qué día añadirlo?', + 'places.all': 'Todo', + 'places.unplanned': 'Sin planificar', + 'places.search': 'Buscar lugares...', + 'places.allCategories': 'Todas las categorías', + 'places.count': '{count} lugares', + 'places.countSingular': '1 lugar', + 'places.allPlanned': 'Todos los lugares están planificados', + 'places.noneFound': 'No se encontraron lugares', + 'places.editPlace': 'Editar lugar', + 'places.formName': 'Nombre', + 'places.formNamePlaceholder': 'p. ej. Torre Eiffel', + 'places.formDescription': 'Descripción', + 'places.formDescriptionPlaceholder': 'Descripción breve...', + 'places.formAddress': 'Dirección', + 'places.formAddressPlaceholder': 'Calle, ciudad, país', + 'places.formLat': 'Latitud (p. ej. 48.8566)', + 'places.formLng': 'Longitud (p. ej. 2.3522)', + 'places.formCategory': 'Categoría', + 'places.noCategory': 'Sin categoría', + 'places.categoryNamePlaceholder': 'Nombre de la categoría', + 'places.formTime': 'Hora', + 'places.startTime': 'Inicio', + 'places.endTime': 'Fin', + 'places.endTimeBeforeStart': 'La hora de fin es anterior a la de inicio', + 'places.timeCollision': 'Solapamiento horario con:', + 'places.formWebsite': 'Página web', + 'places.formNotesPlaceholder': 'Notas personales...', + 'places.formReservation': 'Reserva', + 'places.reservationNotesPlaceholder': 'Notas de reserva, número de confirmación...', + 'places.mapsSearchPlaceholder': 'Buscar lugares...', + 'places.mapsSearchError': 'La búsqueda de lugares falló.', + 'places.osmHint': 'Usando búsqueda con OpenStreetMap (sin fotos, horarios ni valoraciones). Añade una clave API de Google en Ajustes para obtener todos los detalles.', + 'places.osmActive': 'Búsqueda mediante OpenStreetMap (sin fotos, valoraciones ni horarios). Añade una clave API de Google en Ajustes para datos ampliados.', + 'places.categoryCreateError': 'No se pudo crear la categoría', + 'places.nameRequired': 'Introduce un nombre', + 'places.saveError': 'No se pudo guardar', + + // Place Inspector + 'inspector.opened': 'Abierto', + 'inspector.closed': 'Cerrado', + 'inspector.openingHours': 'Horario de apertura', + 'inspector.showHours': 'Mostrar horario', + 'inspector.files': 'Archivos', + 'inspector.filesCount': '{count} archivos', + 'inspector.removeFromDay': 'Quitar del día', + 'inspector.addToDay': 'Añadir al día', + 'inspector.confirmedRes': 'Reserva confirmada', + 'inspector.pendingRes': 'Reserva pendiente', + 'inspector.google': 'Abrir en Google Maps', + 'inspector.website': 'Abrir la web', + 'inspector.addRes': 'Reserva', + 'inspector.editRes': 'Editar reserva', + 'inspector.participants': 'Participantes', + + // Reservations + 'reservations.title': 'Reservas', + 'reservations.empty': 'Aún no hay reservas', + 'reservations.emptyHint': 'Añade reservas de vuelos, hoteles y más', + 'reservations.add': 'Añadir reserva', + 'reservations.addManual': 'Reserva manual', + 'reservations.placeHint': 'Consejo: es mejor crear las reservas directamente desde un lugar para vincularlas con el plan del día.', + 'reservations.confirmed': 'Confirmada', + 'reservations.pending': 'Pendiente', + 'reservations.summary': '{confirmed} confirmadas, {pending} pendientes', + 'reservations.fromPlan': 'Del plan', + 'reservations.showFiles': 'Mostrar archivos', + 'reservations.editTitle': 'Editar reserva', + 'reservations.status': 'Estado', + 'reservations.datetime': 'Fecha y hora', + 'reservations.startTime': 'Hora de inicio', + 'reservations.endTime': 'Hora de fin', + 'reservations.date': 'Fecha', + 'reservations.time': 'Hora', + 'reservations.timeAlt': 'Hora (alternativa, p. ej. 19:30)', + 'reservations.notes': 'Notas', + 'reservations.notesPlaceholder': 'Notas adicionales...', + 'reservations.type.flight': 'Vuelo', + 'reservations.type.hotel': 'Hotel', + 'reservations.type.restaurant': 'Restaurante', + 'reservations.type.train': 'Tren', + 'reservations.type.car': 'Coche de alquiler', + 'reservations.type.cruise': 'Crucero', + 'reservations.type.event': 'Evento', + 'reservations.type.tour': 'Tour', + 'reservations.type.other': 'Otro', + 'reservations.confirm.delete': '¿Seguro que quieres eliminar la reserva "{name}"?', + 'reservations.toast.updated': 'Reserva actualizada', + 'reservations.toast.removed': 'Reserva eliminada', + 'reservations.toast.fileUploaded': 'Archivo subido', + 'reservations.toast.uploadError': 'No se pudo subir', + 'reservations.newTitle': 'Nueva reserva', + 'reservations.bookingType': 'Tipo de reserva', + 'reservations.titleLabel': 'Título', + 'reservations.titlePlaceholder': 'p. ej. Lufthansa LH123, Hotel Adlon, ...', + 'reservations.locationAddress': 'Ubicación / dirección', + 'reservations.locationPlaceholder': 'Dirección, aeropuerto, hotel...', + 'reservations.confirmationCode': 'Código de reserva', + 'reservations.confirmationPlaceholder': 'p. ej. ABC12345', + 'reservations.day': 'Día', + 'reservations.noDay': 'Sin día', + 'reservations.place': 'Lugar', + 'reservations.noPlace': 'Sin lugar', + 'reservations.pendingSave': 'se guardará…', + 'reservations.uploading': 'Subiendo...', + 'reservations.attachFile': 'Adjuntar archivo', + 'reservations.toast.saveError': 'No se pudo guardar', + 'reservations.toast.updateError': 'No se pudo actualizar', + 'reservations.toast.deleteError': 'No se pudo eliminar', + 'reservations.confirm.remove': '¿Eliminar la reserva de "{name}"?', + 'reservations.linkAssignment': 'Vincular a una asignación del día', + 'reservations.pickAssignment': 'Selecciona una asignación de tu plan...', + 'reservations.noAssignment': 'Sin vínculo (independiente)', + + // Budget + 'budget.title': 'Presupuesto', + 'budget.emptyTitle': 'Aún no se ha creado ningún presupuesto', + 'budget.emptyText': 'Crea categorías y entradas para planificar el presupuesto de tu viaje', + 'budget.emptyPlaceholder': 'Introduce el nombre de la categoría...', + 'budget.createCategory': 'Crear categoría', + 'budget.category': 'Categoría', + 'budget.categoryName': 'Nombre de la categoría', + 'budget.table.name': 'Nombre', + 'budget.table.total': 'Total', + 'budget.table.persons': 'Personas', + 'budget.table.days': 'Días', + 'budget.table.perPerson': 'Por persona', + 'budget.table.perDay': 'Por día', + 'budget.table.perPersonDay': 'Por pers. / día', + 'budget.table.note': 'Nota', + 'budget.newEntry': 'Nueva entrada', + 'budget.defaultEntry': 'Nueva entrada', + 'budget.defaultCategory': 'Nueva categoría', + 'budget.total': 'Total', + 'budget.totalBudget': 'Presupuesto total', + 'budget.byCategory': 'Por categoría', + 'budget.editTooltip': 'Haz clic para editar', + 'budget.confirm.deleteCategory': '¿Seguro que quieres eliminar la categoría "{name}" con {count} entradas?', + 'budget.deleteCategory': 'Eliminar categoría', + 'budget.perPerson': 'Por persona', + 'budget.paid': 'Pagado', + 'budget.open': 'Abrir', + 'budget.noMembers': 'No hay miembros asignados', + + // Files + 'files.title': 'Archivos', + 'files.count': '{count} archivos', + 'files.countSingular': '1 archivo', + 'files.uploaded': '{count} archivos subidos', + 'files.uploadError': 'La subida falló', + 'files.dropzone': 'Arrastra aquí los archivos', + 'files.dropzoneHint': 'o haz clic para explorar', + 'files.allowedTypes': 'Imágenes, PDF, DOC, DOCX, XLS, XLSX, TXT, CSV · Máx. 50 MB', + 'files.uploading': 'Subiendo...', + 'files.filterAll': 'Todo', + 'files.filterPdf': 'PDF', + 'files.filterImages': 'Imágenes', + 'files.filterDocs': 'Documentos', + 'files.filterCollab': 'Notas de colaboración', + 'files.sourceCollab': 'Desde notas de colaboración', + 'files.empty': 'Aún no hay archivos', + 'files.emptyHint': 'Sube archivos para adjuntarlos a tu viaje', + 'files.openTab': 'Abrir en una pestaña nueva', + 'files.confirm.delete': '¿Seguro que quieres eliminar este archivo?', + 'files.toast.deleted': 'Archivo eliminado', + 'files.toast.deleteError': 'No se pudo eliminar el archivo', + 'files.sourcePlan': 'Plan diario', + 'files.sourceBooking': 'Reserva', + 'files.attach': 'Adjuntar', + 'files.pasteHint': 'También puedes pegar imágenes desde el portapapeles (Ctrl+V)', + + // Packing + 'packing.title': 'Lista de equipaje', + 'packing.empty': 'La lista de equipaje está vacía', + 'packing.progress': '{packed} de {total} preparados ({percent}%)', + 'packing.clearChecked': 'Eliminar {count} marcados', + 'packing.clearCheckedShort': 'Eliminar {count}', + 'packing.suggestions': 'Sugerencias', + 'packing.suggestionsTitle': 'Añadir sugerencias', + 'packing.allSuggested': 'Todas las sugerencias añadidas', + 'packing.allPacked': '¡Todo preparado!', + 'packing.addPlaceholder': 'Añadir nuevo elemento...', + 'packing.categoryPlaceholder': 'Categoría...', + 'packing.filterAll': 'Todo', + 'packing.filterOpen': 'Pendientes', + 'packing.filterDone': 'Hecho', + 'packing.emptyTitle': 'La lista de equipaje está vacía', + 'packing.emptyHint': 'Añade elementos o usa las sugerencias', + 'packing.emptyFiltered': 'Ningún elemento coincide con este filtro', + 'packing.menuRename': 'Renombrar', + 'packing.menuCheckAll': 'Marcar todo', + 'packing.menuUncheckAll': 'Desmarcar todo', + 'packing.menuDeleteCat': 'Eliminar categoría', + 'packing.changeCategory': 'Cambiar categoría', + 'packing.confirm.clearChecked': '¿Seguro que quieres eliminar {count} elementos marcados?', + 'packing.confirm.deleteCat': '¿Seguro que quieres eliminar la categoría "{name}" con {count} elementos?', + 'packing.defaultCategory': 'Otros', + 'packing.toast.saveError': 'No se pudo guardar', + 'packing.toast.deleteError': 'No se pudo eliminar', + 'packing.toast.renameError': 'No se pudo renombrar', + 'packing.toast.addError': 'No se pudo añadir', + + // Packing suggestions + 'packing.suggestions.items': [ + { name: 'Pasaporte', category: 'Documentos' }, + { name: 'Documento de identidad', category: 'Documentos' }, + { name: 'Seguro de viaje', category: 'Documentos' }, + { name: 'Billetes de vuelo', category: 'Documentos' }, + { name: 'Tarjeta de crédito', category: 'Finanzas' }, + { name: 'Efectivo', category: 'Finanzas' }, + { name: 'Visado', category: 'Documentos' }, + { name: 'Camisetas', category: 'Ropa' }, + { name: 'Pantalones', category: 'Ropa' }, + { name: 'Ropa interior', category: 'Ropa' }, + { name: 'Calcetines', category: 'Ropa' }, + { name: 'Chaqueta', category: 'Ropa' }, + { name: 'Pijama', category: 'Ropa' }, + { name: 'Ropa de baño', category: 'Ropa' }, + { name: 'Impermeable', category: 'Ropa' }, + { name: 'Zapatos cómodos', category: 'Ropa' }, + { name: 'Cepillo de dientes', category: 'Aseo' }, + { name: 'Pasta de dientes', category: 'Aseo' }, + { name: 'Champú', category: 'Aseo' }, + { name: 'Desodorante', category: 'Aseo' }, + { name: 'Protector solar', category: 'Aseo' }, + { name: 'Maquinilla de afeitar', category: 'Aseo' }, + { name: 'Cargador', category: 'Electrónica' }, + { name: 'Batería externa', category: 'Electrónica' }, + { name: 'Auriculares', category: 'Electrónica' }, + { name: 'Adaptador de viaje', category: 'Electrónica' }, + { name: 'Cámara', category: 'Electrónica' }, + { name: 'Analgésicos', category: 'Salud' }, + { name: 'Tiritas', category: 'Salud' }, + { name: 'Desinfectante', category: 'Salud' }, + ], + + // Members / Sharing + 'members.shareTrip': 'Compartir viaje', + 'members.inviteUser': 'Invitar usuario', + 'members.selectUser': 'Seleccionar usuario…', + 'members.invite': 'Invitar', + 'members.allHaveAccess': 'Todos los usuarios ya tienen acceso.', + 'members.access': 'Acceso', + 'members.person': 'persona', + 'members.persons': 'personas', + 'members.you': 'tú', + 'members.owner': 'Propietario', + 'members.leaveTrip': 'Abandonar viaje', + 'members.removeAccess': 'Quitar acceso', + 'members.confirmLeave': '¿Abandonar el viaje? Perderás el acceso.', + 'members.confirmRemove': '¿Quitar el acceso de este usuario?', + 'members.loadError': 'No se pudieron cargar los miembros', + 'members.added': 'añadido', + 'members.addError': 'No se pudo añadir', + 'members.removed': 'Miembro eliminado', + 'members.removeError': 'No se pudo eliminar', + + // Categories (Admin) + 'categories.title': 'Categorías', + 'categories.subtitle': 'Gestiona categorías para lugares', + 'categories.new': 'Nueva categoría', + 'categories.empty': 'Aún no hay categorías', + 'categories.namePlaceholder': 'Nombre de la categoría', + 'categories.icon': 'Icono', + 'categories.color': 'Color', + 'categories.customColor': 'Elegir color personalizado', + 'categories.preview': 'Vista previa', + 'categories.defaultName': 'Categoría', + 'categories.update': 'Actualizar', + 'categories.create': 'Crear', + 'categories.confirm.delete': '¿Eliminar la categoría? Los lugares de esta categoría no se eliminarán.', + 'categories.toast.loadError': 'No se pudieron cargar las categorías', + 'categories.toast.nameRequired': 'Introduce un nombre', + 'categories.toast.updated': 'Categoría actualizada', + 'categories.toast.created': 'Categoría creada', + 'categories.toast.saveError': 'No se pudo guardar', + 'categories.toast.deleted': 'Categoría eliminada', + 'categories.toast.deleteError': 'No se pudo eliminar', + + // Backup (Admin) + 'backup.title': 'Copia de seguridad de datos', + 'backup.subtitle': 'Base de datos y todos los archivos subidos', + 'backup.refresh': 'Actualizar', + 'backup.upload': 'Subir copia de seguridad', + 'backup.uploading': 'Subiendo…', + 'backup.create': 'Crear copia', + 'backup.creating': 'Creando…', + 'backup.empty': 'Aún no hay copias', + 'backup.createFirst': 'Crear la primera copia', + 'backup.download': 'Descargar', + 'backup.restore': 'Restaurar', + 'backup.confirm.restore': '¿Restaurar la copia "{name}"?\n\nTodos los datos actuales serán reemplazados por la copia.', + 'backup.confirm.uploadRestore': '¿Subir y restaurar el archivo de copia "{name}"?\n\nTodos los datos actuales se sobrescribirán.', + 'backup.confirm.delete': '¿Eliminar la copia "{name}"?', + 'backup.toast.loadError': 'No se pudieron cargar las copias', + 'backup.toast.created': 'Copia de seguridad creada correctamente', + 'backup.toast.createError': 'No se pudo crear la copia', + 'backup.toast.restored': 'Copia restaurada. La página se recargará…', + 'backup.toast.restoreError': 'No se pudo restaurar', + 'backup.toast.uploadError': 'No se pudo subir', + 'backup.toast.deleted': 'Copia eliminada', + 'backup.toast.deleteError': 'No se pudo eliminar', + 'backup.toast.downloadError': 'La descarga falló', + 'backup.toast.settingsSaved': 'Ajustes de copia automática guardados', + 'backup.toast.settingsError': 'No se pudieron guardar los ajustes', + 'backup.auto.title': 'Copia automática', + 'backup.auto.subtitle': 'Copia de seguridad automática según una programación', + 'backup.auto.enable': 'Activar copia automática', + 'backup.auto.enableHint': 'Se crearán copias automáticamente según la frecuencia elegida', + 'backup.auto.interval': 'Intervalo', + 'backup.auto.keepLabel': 'Eliminar copias antiguas después de', + 'backup.interval.hourly': 'Cada hora', + 'backup.interval.daily': 'Diaria', + 'backup.interval.weekly': 'Semanal', + 'backup.interval.monthly': 'Mensual', + 'backup.keep.1day': '1 día', + 'backup.keep.3days': '3 días', + 'backup.keep.7days': '7 días', + 'backup.keep.14days': '14 días', + 'backup.keep.30days': '30 días', + 'backup.keep.forever': 'Conservar para siempre', + + // Photos + 'photos.allDays': 'Todos los días', + 'photos.title': 'Recuerdos', + 'photos.noPhotos': 'Aún no hay fotos', + 'photos.uploadHint': 'Sube y organiza las fotos compartidas de este viaje', + 'photos.clickToSelect': 'o haz clic para seleccionar', + 'photos.dropHere': 'Suelta aquí las fotos...', + 'photos.dropTitle': 'Suelta aquí las fotos', + 'photos.fileHint': 'JPG, PNG, GIF, WebP · máx. 10 MB · hasta 30 fotos', + 'photos.selectedCount': '{count} foto(s) seleccionada(s)', + 'photos.sharedAlbum': '{count} recuerdos en este álbum compartido', + 'photos.sharedAlbumFor': '{count} recuerdos en {trip}', + 'photos.allPlaces': 'Todos los lugares', + 'photos.view.grid': 'Cuadrícula', + 'photos.view.day': 'Por día', + 'photos.view.place': 'Por lugar', + 'photos.stats.total': 'Fotos', + 'photos.stats.days': 'Días', + 'photos.stats.places': 'Lugares', + 'photos.stats.latest': 'Última subida', + 'photos.sectionCount': '{count} foto(s)', + 'photos.ungrouped': 'Sin clasificar', + 'photos.featured': 'Recuerdo destacado', + 'photos.coverFallback': 'Portada del álbum compartido', + 'photos.coverHint': 'Una imagen destacada para este álbum de viaje', + 'photos.mapTitle': 'Mapa de recuerdos', + 'photos.mapHint': 'Explora los lugares vinculados en el mismo mapa que usamos en el plan', + 'photos.mapEmpty': 'Vincula tus fotos a lugares para verlas ubicadas en el mapa.', + 'photos.linkDay': 'Vincular día', + 'photos.noDay': 'Sin día', + 'photos.linkPlace': 'Vincular lugar', + 'photos.noPlace': 'Sin lugar', + 'photos.captionLabel': 'Pie de foto (para todas)', + 'photos.captionPlaceholder': 'Pie de foto opcional...', + 'photos.addCaption': 'Añadir un pie de foto...', + 'photos.uploadN': 'Subida de {n} foto(s)', + 'admin.addons.catalog.memories.name': 'Recuerdos', + 'admin.addons.catalog.memories.description': 'Álbumes de fotos compartidos para cada viaje', + 'admin.addons.catalog.packing.name': 'Equipaje', + 'admin.addons.catalog.packing.description': 'Prepara tu equipaje con listas de comprobación para cada viaje', + 'admin.addons.catalog.budget.name': 'Presupuesto', + 'admin.addons.catalog.budget.description': 'Controla los gastos y planifica el presupuesto del viaje', + 'admin.addons.catalog.documents.name': 'Documentos', + 'admin.addons.catalog.documents.description': 'Guarda y gestiona la documentación del viaje', + 'admin.addons.catalog.vacay.name': 'Vacaciones', + 'admin.addons.catalog.vacay.description': 'Planificador personal de vacaciones con vista de calendario', + 'admin.addons.catalog.atlas.name': 'Atlas', + 'admin.addons.catalog.atlas.description': 'Mapa del mundo con los países visitados y estadísticas de viaje', + 'admin.addons.catalog.collab.name': 'Colaboración', + 'admin.addons.catalog.collab.description': 'Notas, encuestas y chat en tiempo real para organizar el viaje', + + // Backup restore modal + 'backup.restoreConfirmTitle': '¿Restaurar copia?', + 'backup.restoreWarning': 'Todos los datos actuales (viajes, lugares, usuarios, subidas) serán reemplazados permanentemente por la copia. Esta acción no se puede deshacer.', + 'backup.restoreTip': 'Consejo: crea una copia del estado actual antes de restaurar.', + 'backup.restoreConfirm': 'Sí, restaurar', + + // PDF + 'pdf.travelPlan': 'Plan de viaje', + 'pdf.planned': 'Planificado', + 'pdf.costLabel': 'Coste EUR', + 'pdf.preview': 'Vista previa PDF', + 'pdf.saveAsPdf': 'Guardar como PDF', + + // Planner + 'planner.places': 'Lugares', + 'planner.bookings': 'Reservas', + 'planner.packingList': 'Lista de equipaje', + 'planner.documents': 'Documentos', + 'planner.dayPlan': 'Plan por días', + 'planner.reservations': 'Reservas', + 'planner.minTwoPlaces': 'Se necesitan al menos 2 lugares con coordenadas', + 'planner.noGeoPlaces': 'No hay lugares con coordenadas disponibles', + 'planner.routeCalculated': 'Ruta calculada', + 'planner.routeCalcFailed': 'No se pudo calcular la ruta', + 'planner.routeError': 'Error al calcular la ruta', + 'planner.routeOptimized': 'Ruta optimizada', + 'planner.reservationUpdated': 'Reserva actualizada', + 'planner.reservationAdded': 'Reserva añadida', + 'planner.confirmDeleteReservation': '¿Eliminar reserva?', + 'planner.reservationDeleted': 'Reserva eliminada', + 'planner.days': 'Días', + 'planner.allPlaces': 'Todos los lugares', + 'planner.totalPlaces': '{n} lugares en total', + 'planner.noDaysPlanned': 'Aún no hay días planificados', + 'planner.editTrip': 'Editar viaje →', + 'planner.placeOne': '1 lugar', + 'planner.placeN': '{n} lugares', + 'planner.addNote': 'Añadir nota', + 'planner.noEntries': 'No hay entradas para este día', + 'planner.addPlace': 'Añadir lugar/actividad', + 'planner.addPlaceShort': '+ Añadir lugar/actividad', + 'planner.resPending': 'Reserva pendiente · ', + 'planner.resConfirmed': 'Reserva confirmada · ', + 'planner.notePlaceholder': 'Nota…', + 'planner.noteTimePlaceholder': 'Hora (opcional)', + 'planner.noteExamplePlaceholder': 'p. ej. S3 a las 14:30 desde la estación central, ferry desde el muelle 7, pausa para comer…', + 'planner.totalCost': 'Coste total', + 'planner.searchPlaces': 'Buscar lugares…', + 'planner.allCategories': 'Todas las categorías', + 'planner.noPlacesFound': 'No se encontraron lugares', + 'planner.addFirstPlace': 'Añadir el primer lugar', + 'planner.noReservations': 'Sin reservas', + 'planner.addFirstReservation': 'Añadir la primera reserva', + 'planner.new': 'Nuevo', + 'planner.addToDay': '+ Día', + 'planner.calculating': 'Calculando…', + 'planner.route': 'Ruta', + 'planner.optimize': 'Optimizar', + 'planner.openGoogleMaps': 'Abrir en Google Maps', + 'planner.selectDayHint': 'Selecciona un día de la lista izquierda para ver su plan', + 'planner.noPlacesForDay': 'Aún no hay lugares para este día', + 'planner.addPlacesLink': 'Añadir lugares →', + 'planner.minTotal': 'min en total', + 'planner.noReservation': 'Sin reserva', + 'planner.removeFromDay': 'Quitar del día', + 'planner.addToThisDay': 'Añadir al día', + 'planner.overview': 'Vista general', + 'planner.noDays': 'No hay días todavía', + 'planner.editTripToAddDays': 'Edita el viaje para añadir días', + 'planner.dayCount': '{n} días', + 'planner.clickToUnlock': 'Haz clic para desbloquear', + 'planner.keepPosition': 'Mantener posición durante la optimización de ruta', + 'planner.dayDetails': 'Detalles del día', + 'planner.dayN': 'Día {n}', + 'planner.notes': 'Notas', + 'planner.addDayNote': 'Añadir notas para este día...', + + // Dashboard Stats + 'stats.countries': 'Países', + 'stats.cities': 'Ciudades', + 'stats.trips': 'Viajes', + 'stats.places': 'Lugares', + 'stats.worldProgress': 'Progreso mundial', + 'stats.visited': 'visitados', + 'stats.remaining': 'restantes', + 'stats.visitedCountries': 'Países visitados', + + // Day Detail Panel + 'day.precipProb': 'Probabilidad de lluvia', + 'day.precipitation': 'Precipitación', + 'day.wind': 'Viento', + 'day.sunrise': 'Amanecer', + 'day.sunset': 'Atardecer', + 'day.hourlyForecast': 'Pronóstico por horas', + 'day.climateHint': 'Promedios históricos: el pronóstico real está disponible dentro de los 16 días previos a la fecha.', + 'day.noWeather': 'No hay datos meteorológicos disponibles. Añade un lugar con coordenadas.', + 'day.overview': 'Resumen diario', + 'day.accommodation': 'Alojamiento', + 'day.addAccommodation': 'Añadir alojamiento', + 'day.hotelDayRange': 'Aplicar a los días', + 'day.noPlacesForHotel': 'Añade primero lugares al viaje', + 'day.allDays': 'Todos', + 'day.checkIn': 'Check-in', + 'day.checkOut': 'Check-out', + 'day.confirmation': 'Confirmación', + 'day.editAccommodation': 'Editar alojamiento', + 'day.reservations': 'Reservas', + + // Collab Addon + 'collab.tabs.chat': 'Mensajes', + 'collab.tabs.notes': 'Notas', + 'collab.tabs.polls': 'Encuestas', + 'collab.whatsNext.title': 'Qué viene ahora', + 'collab.whatsNext.today': 'Hoy', + 'collab.whatsNext.tomorrow': 'Mañana', + 'collab.whatsNext.empty': 'No hay actividades próximas', + 'collab.whatsNext.until': 'hasta', + 'collab.whatsNext.emptyHint': 'Las actividades con hora aparecerán aquí', + 'collab.chat.send': 'Enviar', + 'collab.chat.placeholder': 'Escribe un mensaje...', + 'collab.chat.empty': 'Empieza la conversación', + 'collab.chat.emptyHint': 'Los mensajes se comparten con todos los miembros del viaje', + 'collab.chat.emptyDesc': 'Comparte ideas, planes y novedades con tu grupo de viaje', + 'collab.chat.today': 'Hoy', + 'collab.chat.yesterday': 'Ayer', + 'collab.chat.deletedMessage': 'eliminó un mensaje', + 'collab.chat.loadMore': 'Cargar mensajes anteriores', + 'collab.chat.justNow': 'justo ahora', + 'collab.chat.minutesAgo': 'hace {n} min', + 'collab.chat.hoursAgo': 'hace {n} h', + 'collab.notes.title': 'Notas', + 'collab.notes.new': 'Nueva nota', + 'collab.notes.empty': 'Aún no hay notas', + 'collab.notes.emptyHint': 'Empieza a capturar ideas y planes', + 'collab.notes.all': 'Todas', + 'collab.notes.titlePlaceholder': 'Título de la nota', + 'collab.notes.contentPlaceholder': 'Escribe algo...', + 'collab.notes.categoryPlaceholder': 'Categoría', + 'collab.notes.newCategory': 'Nueva categoría...', + 'collab.notes.category': 'Categoría', + 'collab.notes.noCategory': 'Sin categoría', + 'collab.notes.color': 'Color', + 'collab.notes.save': 'Guardar', + 'collab.notes.cancel': 'Cancelar', + 'collab.notes.edit': 'Editar', + 'collab.notes.delete': 'Eliminar', + 'collab.notes.pin': 'Fijar', + 'collab.notes.unpin': 'Desfijar', + 'collab.notes.daysAgo': 'hace {n} d', + 'collab.notes.categorySettings': 'Gestionar categorías', + 'collab.notes.create': 'Crear', + 'collab.notes.website': 'Sitio web', + 'collab.notes.websitePlaceholder': 'https://...', + 'collab.notes.attachFiles': 'Adjuntar archivos', + 'collab.notes.noCategoriesYet': 'Aún no hay categorías', + 'collab.notes.emptyDesc': 'Crea una nota para empezar', + 'collab.polls.title': 'Encuestas', + 'collab.polls.new': 'Nueva encuesta', + 'collab.polls.empty': 'Aún no hay encuestas', + 'collab.polls.emptyHint': 'Pregunta al grupo y votad juntos', + 'collab.polls.question': 'Pregunta', + 'collab.polls.questionPlaceholder': '¿Qué deberíamos hacer?', + 'collab.polls.addOption': '+ Añadir opción', + 'collab.polls.optionPlaceholder': 'Opción {n}', + 'collab.polls.create': 'Crear encuesta', + 'collab.polls.close': 'Cerrar', + 'collab.polls.closed': 'Cerrada', + 'collab.polls.votes': '{n} votos', + 'collab.polls.vote': '{n} voto', + 'collab.polls.multipleChoice': 'Selección múltiple', + 'collab.polls.multiChoice': 'Selección múltiple', + 'collab.polls.deadline': 'Fecha límite', + 'collab.polls.option': 'Opción', + 'collab.polls.options': 'Opciones', + 'collab.polls.delete': 'Eliminar', + 'collab.polls.closedSection': 'Cerradas', +} + +export default es diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts new file mode 100644 index 0000000..9e11b4e --- /dev/null +++ b/client/src/i18n/translations/es.ts @@ -0,0 +1 @@ +export { default } from './es.js' diff --git a/client/src/pages/AtlasPage.tsx b/client/src/pages/AtlasPage.tsx index d8bfe75..02558ee 100644 --- a/client/src/pages/AtlasPage.tsx +++ b/client/src/pages/AtlasPage.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useRef } from 'react' import { useNavigate } from 'react-router-dom' -import { useTranslation } from '../i18n' +import { getIntlLanguage, getLocaleForLanguage, useTranslation } from '../i18n' import { useSettingsStore } from '../store/settingsStore' import Navbar from '../components/Layout/Navbar' import apiClient from '../api/client' @@ -100,7 +100,7 @@ function useCountryNames(language: string): (code: string) => string { const [resolver, setResolver] = useState<(code: string) => string>(() => (code: string) => code) useEffect(() => { try { - const dn = new Intl.DisplayNames([language === 'de' ? 'de' : 'en'], { type: 'region' }) + const dn = new Intl.DisplayNames([getIntlLanguage(language)], { type: 'region' }) setResolver(() => (code: string) => { try { return dn.of(code) || code } catch { return code } }) } catch { /* */ } }, [language]) @@ -255,7 +255,7 @@ export default function AtlasPage(): React.ReactElement { const c = countryMap[a3] if (c) { const name = resolveName(c.code) - const formatDate = (d) => { if (!d) return '—'; const dt = new Date(d); return dt.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', { month: 'short', year: 'numeric' }) } + const formatDate = (d) => { if (!d) return '—'; const dt = new Date(d); return dt.toLocaleDateString(getLocaleForLanguage(language), { month: 'short', year: 'numeric' }) } const tooltipHtml = `
${name}
@@ -515,4 +515,3 @@ function SidebarContent({ data, stats, countries, selectedCountry, countryDetail
) } - diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index 072fd83..c9b2e75 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -53,12 +53,12 @@ function getTripStatus(trip: DashboardTrip): string | null { return 'past' } -function formatDate(dateStr: string | null | undefined, locale: string = 'de-DE'): string | null { +function formatDate(dateStr: string | null | undefined, locale: string = 'en-US'): string | null { if (!dateStr) return null return new Date(dateStr + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric' }) } -function formatDateShort(dateStr: string | null | undefined, locale: string = 'de-DE'): string | null { +function formatDateShort(dateStr: string | null | undefined, locale: string = 'en-US'): string | null { if (!dateStr) return null return new Date(dateStr + 'T00:00:00').toLocaleDateString(locale, { day: 'numeric', month: 'short' }) } diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index f846ad9..2b4cc4e 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -270,9 +270,14 @@ export default function LoginPage(): React.ReactElement { return (
- {/* Sprach-Toggle oben rechts */} + {/* Language toggle */} {/* Left — branding */} diff --git a/client/src/pages/SettingsPage.tsx b/client/src/pages/SettingsPage.tsx index 1d13881..1ebc869 100644 --- a/client/src/pages/SettingsPage.tsx +++ b/client/src/pages/SettingsPage.tsx @@ -269,6 +269,7 @@ export default function SettingsPage(): React.ReactElement { {[ { value: 'de', label: 'Deutsch' }, { value: 'en', label: 'English' }, + { value: 'es', label: 'Español' }, ].map(opt => (
-

Vacay

+

{t('admin.addons.catalog.vacay.name')}

{t('vacay.subtitle')}

From 0d9dbb62861bd1008848538ca9122633e1317ddc Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 28 Mar 2026 23:00:53 +0100 Subject: [PATCH 2/4] i18n: consolidate es.js into es.ts, add missing 2.6.2 Spanish translations --- client/src/i18n/translations/es.js | 1061 -------------------------- client/src/i18n/translations/es.ts | 1114 +++++++++++++++++++++++++++- 2 files changed, 1113 insertions(+), 1062 deletions(-) delete mode 100644 client/src/i18n/translations/es.js diff --git a/client/src/i18n/translations/es.js b/client/src/i18n/translations/es.js deleted file mode 100644 index 2146dfb..0000000 --- a/client/src/i18n/translations/es.js +++ /dev/null @@ -1,1061 +0,0 @@ -const es = { - // Common - 'common.save': 'Guardar', - 'common.cancel': 'Cancelar', - 'common.delete': 'Eliminar', - 'common.edit': 'Editar', - 'common.add': 'Añadir', - 'common.loading': 'Cargando...', - 'common.error': 'Error', - 'common.back': 'Atrás', - 'common.all': 'Todo', - 'common.close': 'Cerrar', - 'common.open': 'Abrir', - 'common.upload': 'Subir', - 'common.search': 'Buscar', - 'common.confirm': 'Confirmar', - 'common.ok': 'Aceptar', - 'common.yes': 'Sí', - 'common.no': 'No', - 'common.or': 'o', - 'common.none': 'Ninguno', - 'common.date': 'Fecha', - 'common.rename': 'Renombrar', - 'common.name': 'Nombre', - 'common.email': 'Correo', - 'common.password': 'Contraseña', - 'common.saving': 'Guardando...', - 'common.update': 'Actualizar', - 'common.change': 'Cambiar', - 'common.uploading': 'Subiendo…', - 'common.backToPlanning': 'Volver a la planificación', - 'common.reset': 'Restablecer', - - // Navbar - 'nav.trip': 'Viaje', - 'nav.share': 'Compartir', - 'nav.settings': 'Ajustes', - 'nav.admin': 'Administración', - 'nav.logout': 'Cerrar sesión', - 'nav.lightMode': 'Modo claro', - 'nav.darkMode': 'Modo oscuro', - 'nav.autoMode': 'Modo automático', - 'nav.administrator': 'Administrador', - 'nav.myTrips': 'Mis viajes', - - // Dashboard - 'dashboard.title': 'Mis viajes', - 'dashboard.subtitle.loading': 'Cargando viajes...', - 'dashboard.subtitle.trips': '{count} viajes ({archived} archivados)', - 'dashboard.subtitle.empty': 'Empieza tu primer viaje', - 'dashboard.subtitle.activeOne': '{count} viaje activo', - 'dashboard.subtitle.activeMany': '{count} viajes activos', - 'dashboard.subtitle.archivedSuffix': ' · {count} archivados', - 'dashboard.newTrip': 'Nuevo viaje', - 'dashboard.currency': 'Divisa', - 'dashboard.timezone': 'Zonas horarias', - 'dashboard.localTime': 'Hora local', - 'dashboard.emptyTitle': 'Aún no hay viajes', - 'dashboard.emptyText': 'Crea tu primer viaje y empieza a planificar', - 'dashboard.emptyButton': 'Crear primer viaje', - 'dashboard.nextTrip': 'Próximo viaje', - 'dashboard.shared': 'Compartido', - 'dashboard.sharedBy': 'Compartido por {name}', - 'dashboard.days': 'Días', - 'dashboard.places': 'Lugares', - 'dashboard.archive': 'Archivar', - 'dashboard.restore': 'Restaurar', - 'dashboard.archived': 'Archivado', - 'dashboard.status.ongoing': 'En curso', - 'dashboard.status.today': 'Hoy', - 'dashboard.status.tomorrow': 'Mañana', - 'dashboard.status.past': 'Pasado', - 'dashboard.status.daysLeft': 'Quedan {count} días', - 'dashboard.toast.loadError': 'No se pudieron cargar los viajes', - 'dashboard.toast.created': '¡Viaje creado correctamente!', - 'dashboard.toast.createError': 'No se pudo crear el viaje', - 'dashboard.toast.updated': '¡Viaje actualizado!', - 'dashboard.toast.updateError': 'No se pudo actualizar el viaje', - 'dashboard.toast.deleted': 'Viaje eliminado', - 'dashboard.toast.deleteError': 'No se pudo eliminar el viaje', - 'dashboard.toast.archived': 'Viaje archivado', - 'dashboard.toast.archiveError': 'No se pudo archivar el viaje', - 'dashboard.toast.restored': 'Viaje restaurado', - 'dashboard.toast.restoreError': 'No se pudo restaurar el viaje', - 'dashboard.confirm.delete': '¿Eliminar el viaje "{title}"? Todos los lugares y planes se borrarán permanentemente.', - 'dashboard.editTrip': 'Editar viaje', - 'dashboard.createTrip': 'Crear nuevo viaje', - 'dashboard.tripTitle': 'Título', - 'dashboard.tripTitlePlaceholder': 'p. ej. Verano en Japón', - 'dashboard.tripDescription': 'Descripción', - 'dashboard.tripDescriptionPlaceholder': '¿De qué trata este viaje?', - 'dashboard.startDate': 'Fecha de inicio', - 'dashboard.endDate': 'Fecha de fin', - 'dashboard.noDateHint': 'Sin fecha definida: se crearán 7 días por defecto. Puedes cambiarlo cuando quieras.', - 'dashboard.coverImage': 'Imagen de portada', - 'dashboard.addCoverImage': 'Añadir imagen de portada', - 'dashboard.coverSaved': 'Imagen de portada guardada', - 'dashboard.coverUploadError': 'Error al subir la imagen', - 'dashboard.coverRemoveError': 'Error al eliminar la imagen', - 'dashboard.titleRequired': 'El título es obligatorio', - 'dashboard.endDateError': 'La fecha de fin debe ser posterior a la de inicio', - - // Settings - 'settings.title': 'Ajustes', - 'settings.subtitle': 'Configura tus ajustes personales', - 'settings.map': 'Mapa', - 'settings.mapTemplate': 'Plantilla del mapa', - 'settings.mapTemplatePlaceholder.select': 'Seleccionar plantilla...', - 'settings.mapDefaultHint': 'Déjalo vacío para OpenStreetMap (por defecto)', - 'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - 'settings.mapHint': 'Plantilla de URL para los mosaicos del mapa', - 'settings.latitude': 'Latitud', - 'settings.longitude': 'Longitud', - 'settings.saveMap': 'Guardar mapa', - 'settings.apiKeys': 'Claves API', - 'settings.mapsKey': 'Clave API de Google Maps', - 'settings.mapsKeyHint': 'Necesaria para buscar lugares. Consíguela en console.cloud.google.com', - 'settings.weatherKey': 'Clave API de OpenWeatherMap', - 'settings.weatherKeyHint': 'Para datos meteorológicos. Gratis en openweathermap.org/api', - 'settings.keyPlaceholder': 'Introduce la clave...', - 'settings.configured': 'Configurado', - 'settings.saveKeys': 'Guardar claves', - 'settings.display': 'Visualización', - 'settings.colorMode': 'Modo de color', - 'settings.light': 'Claro', - 'settings.dark': 'Oscuro', - 'settings.auto': 'Automático', - 'settings.language': 'Idioma', - 'settings.temperature': 'Unidad de temperatura', - 'settings.timeFormat': 'Formato de hora', - 'settings.routeCalculation': 'Cálculo de ruta', - 'settings.on': 'Activado', - 'settings.off': 'Desactivado', - 'settings.account': 'Cuenta', - 'settings.username': 'Usuario', - 'settings.email': 'Correo', - 'settings.role': 'Rol', - 'settings.roleAdmin': 'Administrador', - 'settings.oidcLinked': 'Vinculado con', - 'settings.changePassword': 'Cambiar contraseña', - 'settings.currentPassword': 'Contraseña actual', - 'settings.newPassword': 'Nueva contraseña', - 'settings.confirmPassword': 'Confirmar nueva contraseña', - 'settings.updatePassword': 'Actualizar contraseña', - 'settings.passwordRequired': 'Introduce la contraseña actual y la nueva', - 'settings.passwordTooShort': 'La contraseña debe tener al menos 8 caracteres', - 'settings.passwordMismatch': 'Las contraseñas no coinciden', - 'settings.passwordChanged': 'Contraseña cambiada correctamente', - 'settings.deleteAccount': 'Eliminar cuenta', - 'settings.deleteAccountTitle': '¿Eliminar tu cuenta?', - 'settings.deleteAccountWarning': 'Tu cuenta y todos tus viajes, lugares y archivos se eliminarán permanentemente. Esta acción no se puede deshacer.', - 'settings.deleteAccountConfirm': 'Eliminar permanentemente', - 'settings.deleteBlockedTitle': 'No es posible eliminarla', - 'settings.deleteBlockedMessage': 'Eres el único administrador. Asciende a otro usuario a administrador antes de eliminar tu cuenta.', - 'settings.roleUser': 'Usuario', - 'settings.saveProfile': 'Guardar perfil', - 'settings.toast.mapSaved': 'Ajustes del mapa guardados', - 'settings.toast.keysSaved': 'Claves API guardadas', - 'settings.toast.displaySaved': 'Ajustes de visualización guardados', - 'settings.toast.profileSaved': 'Perfil guardado', - 'settings.uploadAvatar': 'Subir foto de perfil', - 'settings.removeAvatar': 'Eliminar foto de perfil', - 'settings.avatarUploaded': 'Foto de perfil actualizada', - 'settings.avatarRemoved': 'Foto de perfil eliminada', - 'settings.avatarError': 'Falló la subida', - - // Login - 'login.error': 'Inicio de sesión fallido. Revisa tus credenciales.', - 'login.tagline': 'Tus viajes.\nTu plan.', - 'login.description': 'Planifica viajes en colaboración con mapas interactivos, presupuestos y sincronización en tiempo real.', - 'login.features.maps': 'Mapas interactivos', - 'login.features.mapsDesc': 'Google Places, rutas y agrupación', - 'login.features.realtime': 'Sincronización en tiempo real', - 'login.features.realtimeDesc': 'Planificad juntos mediante WebSocket', - 'login.features.budget': 'Control de presupuesto', - 'login.features.budgetDesc': 'Categorías, gráficos y costes por persona', - 'login.features.collab': 'Colaboración', - 'login.features.collabDesc': 'Multiusuario con viajes compartidos', - 'login.features.packing': 'Listas de equipaje', - 'login.features.packingDesc': 'Categorías, progreso y sugerencias', - 'login.features.bookings': 'Reservas', - 'login.features.bookingsDesc': 'Vuelos, hoteles, restaurantes y más', - 'login.features.files': 'Documentos', - 'login.features.filesDesc': 'Sube y gestiona documentos', - 'login.features.routes': 'Rutas inteligentes', - 'login.features.routesDesc': 'Optimización automática y exportación a Google Maps', - 'login.selfHosted': 'Autoalojado · Código abierto · Tus datos siguen siendo tuyos', - 'login.title': 'Iniciar sesión', - 'login.subtitle': 'Bienvenido de nuevo', - 'login.signingIn': 'Iniciando sesión…', - 'login.signIn': 'Entrar', - 'login.createAdmin': 'Crear cuenta de administrador', - 'login.createAdminHint': 'Configura la primera cuenta administradora de NOMAD.', - 'login.createAccount': 'Crear cuenta', - 'login.createAccountHint': 'Crea una cuenta nueva.', - 'login.creating': 'Creando…', - 'login.noAccount': '¿No tienes cuenta?', - 'login.hasAccount': '¿Ya tienes cuenta?', - 'login.register': 'Registrarse', - 'login.emailPlaceholder': 'tu@correo.com', - 'login.username': 'Usuario', - 'login.oidc.registrationDisabled': 'El registro está desactivado. Contacta con tu administrador.', - 'login.oidc.noEmail': 'No se recibió ningún correo del proveedor.', - 'login.oidc.tokenFailed': 'La autenticación falló.', - 'login.oidc.invalidState': 'Sesión no válida. Inténtalo de nuevo.', - 'login.demoFailed': 'Falló el acceso a la demo', - 'login.oidcSignIn': 'Entrar con {name}', - 'login.demoHint': 'Prueba la demo: no necesitas registrarte', - - // Register - 'register.passwordMismatch': 'Las contraseñas no coinciden', - 'register.passwordTooShort': 'La contraseña debe tener al menos 6 caracteres', - 'register.failed': 'Falló el registro', - 'register.getStarted': 'Empezar', - 'register.subtitle': 'Crea una cuenta y empieza a planificar tus viajes.', - 'register.feature1': 'Planes de viaje ilimitados', - 'register.feature2': 'Vista de mapa interactiva', - 'register.feature3': 'Gestiona lugares y categorías', - 'register.feature4': 'Haz seguimiento de las reservas', - 'register.feature5': 'Crea listas de equipaje', - 'register.feature6': 'Guarda fotos y archivos', - 'register.createAccount': 'Crear cuenta', - 'register.startPlanning': 'Empieza a planificar tu viaje', - 'register.minChars': 'Mín. 6 caracteres', - 'register.confirmPassword': 'Confirmar contraseña', - 'register.repeatPassword': 'Repetir contraseña', - 'register.registering': 'Registrando...', - 'register.register': 'Registrarse', - 'register.hasAccount': '¿Ya tienes cuenta?', - 'register.signIn': 'Iniciar sesión', - - // Admin - 'admin.title': 'Administración', - 'admin.subtitle': 'Gestión de usuarios y ajustes del sistema', - 'admin.tabs.users': 'Usuarios', - 'admin.tabs.categories': 'Categorías', - 'admin.tabs.backup': 'Copia de seguridad', - 'admin.stats.users': 'Usuarios', - 'admin.stats.trips': 'Viajes', - 'admin.stats.places': 'Lugares', - 'admin.stats.photos': 'Fotos', - 'admin.stats.files': 'Archivos', - 'admin.table.user': 'Usuario', - 'admin.table.email': 'Correo', - 'admin.table.role': 'Rol', - 'admin.table.created': 'Creado', - 'admin.table.lastLogin': 'Último acceso', - 'admin.table.actions': 'Acciones', - 'admin.you': '(Tú)', - 'admin.editUser': 'Editar usuario', - 'admin.newPassword': 'Nueva contraseña', - 'admin.newPasswordHint': 'Déjalo vacío para mantener la contraseña actual', - 'admin.deleteUser': '¿Eliminar al usuario "{name}"? Todos sus viajes se borrarán permanentemente.', - 'admin.deleteUserTitle': 'Eliminar usuario', - 'admin.newPasswordPlaceholder': 'Introduce una nueva contraseña…', - 'admin.toast.loadError': 'No se pudieron cargar los datos de administración', - 'admin.toast.userUpdated': 'Usuario actualizado', - 'admin.toast.updateError': 'No se pudo actualizar', - 'admin.toast.userDeleted': 'Usuario eliminado', - 'admin.toast.deleteError': 'No se pudo eliminar', - 'admin.toast.cannotDeleteSelf': 'No puedes eliminar tu propia cuenta', - 'admin.toast.userCreated': 'Usuario creado', - 'admin.toast.createError': 'No se pudo crear el usuario', - 'admin.toast.fieldsRequired': 'Usuario, correo y contraseña son obligatorios', - 'admin.createUser': 'Crear usuario', - 'admin.tabs.settings': 'Ajustes', - 'admin.allowRegistration': 'Permitir el registro', - 'admin.allowRegistrationHint': 'Los nuevos usuarios pueden registrarse por sí mismos', - 'admin.apiKeys': 'Claves API', - 'admin.apiKeysHint': 'Opcional. Activa datos ampliados de lugares, como fotos y previsión del tiempo.', - 'admin.mapsKey': 'Clave API de Google Maps', - 'admin.mapsKeyHint': 'Obligatoria para buscar lugares. Consíguela en console.cloud.google.com', - 'admin.mapsKeyHintLong': 'Sin una clave API, la búsqueda de lugares usa OpenStreetMap. Con una clave de Google también se pueden cargar fotos, valoraciones y horarios de apertura. Consíguela en console.cloud.google.com.', - 'admin.recommended': 'Recomendado', - 'admin.weatherKey': 'Clave API de OpenWeatherMap', - 'admin.weatherKeyHint': 'Para datos meteorológicos. Gratis en openweathermap.org', - 'admin.validateKey': 'Probar', - 'admin.keyValid': 'Conectado', - 'admin.keyInvalid': 'No válida', - 'admin.keySaved': 'Claves API guardadas', - 'admin.oidcTitle': 'Inicio de sesión único (OIDC)', - 'admin.oidcSubtitle': 'Permite iniciar sesión mediante proveedores externos como Google, Apple, Authentik o Keycloak.', - 'admin.oidcDisplayName': 'Nombre visible', - 'admin.oidcIssuer': 'URL del emisor', - 'admin.oidcIssuerHint': 'La URL Issuer de OpenID Connect del proveedor. Ej.: https://accounts.google.com', - 'admin.oidcSaved': 'Configuración OIDC guardada', - - // File Types - 'admin.fileTypes': 'Tipos de archivo permitidos', - 'admin.fileTypesHint': 'Configura qué tipos de archivo pueden subir los usuarios.', - 'admin.fileTypesFormat': 'Extensiones separadas por comas (p. ej. jpg,png,pdf,doc). Usa * para permitir todos los tipos.', - 'admin.fileTypesSaved': 'Ajustes de tipos de archivo guardados', - - // Addons - 'admin.tabs.addons': 'Complementos', - 'admin.addons.title': 'Complementos', - 'admin.addons.subtitle': 'Activa o desactiva funciones para personalizar tu experiencia en NOMAD.', - 'admin.addons.subtitleBefore': 'Activa o desactiva funciones para personalizar tu experiencia en ', - 'admin.addons.subtitleAfter': '.', - 'admin.addons.enabled': 'Activo', - 'admin.addons.disabled': 'Desactivado', - 'admin.addons.type.trip': 'Viaje', - 'admin.addons.type.global': 'Global', - 'admin.addons.tripHint': 'Disponible como pestaña dentro de cada viaje', - 'admin.addons.globalHint': 'Disponible como sección independiente en la navegación principal', - 'admin.addons.toast.updated': 'Complemento actualizado', - 'admin.addons.toast.error': 'No se pudo actualizar el complemento', - 'admin.addons.noAddons': 'No hay complementos disponibles', - 'admin.weather.title': 'Datos meteorológicos', - 'admin.weather.badge': 'Desde el 24 de marzo de 2026', - 'admin.weather.description': 'NOMAD utiliza Open-Meteo como fuente de datos meteorológicos. Open-Meteo es un servicio meteorológico gratuito y de código abierto: no requiere clave API.', - 'admin.weather.forecast': 'Pronóstico de 16 días', - 'admin.weather.forecastDesc': 'Antes eran 5 días (OpenWeatherMap)', - 'admin.weather.climate': 'Datos climáticos históricos', - 'admin.weather.climateDesc': 'Promedios de los últimos 85 años para fechas posteriores al pronóstico de 16 días', - 'admin.weather.requests': '10.000 solicitudes / día', - 'admin.weather.requestsDesc': 'Gratis, sin necesidad de clave API', - 'admin.weather.locationHint': 'El tiempo se basa en el primer lugar con coordenadas de cada día. Si no hay ningún lugar asignado a un día, se usa como referencia cualquier lugar de la lista.', - - // GitHub - 'admin.tabs.github': 'GitHub', - 'admin.github.title': 'Historial de versiones', - 'admin.github.subtitle': 'Últimas novedades de {repo}', - 'admin.github.latest': 'Última', - 'admin.github.prerelease': 'Prelanzamiento', - 'admin.github.showDetails': 'Mostrar detalles', - 'admin.github.hideDetails': 'Ocultar detalles', - 'admin.github.loadMore': 'Cargar más', - 'admin.github.loading': 'Cargando...', - 'admin.github.error': 'No se pudieron cargar las versiones', - 'admin.github.by': 'por', - 'admin.update.available': 'Actualización disponible', - 'admin.update.text': 'NOMAD {version} está disponible. Estás usando {current}.', - 'admin.update.button': 'Ver en GitHub', - 'admin.update.install': 'Instalar actualización', - 'admin.update.confirmTitle': '¿Instalar actualización?', - 'admin.update.confirmText': 'NOMAD se actualizará de {current} a {version}. Después, el servidor se reiniciará automáticamente.', - 'admin.update.dataInfo': 'Todos tus datos (viajes, usuarios, claves API, subidas, Vacay, Atlas, presupuestos) se conservarán.', - 'admin.update.warning': 'La app estará brevemente no disponible durante el reinicio.', - 'admin.update.confirm': 'Actualizar ahora', - 'admin.update.installing': 'Actualizando…', - 'admin.update.success': '¡Actualización instalada! El servidor se está reiniciando…', - 'admin.update.failed': 'La actualización falló', - 'admin.update.backupHint': 'Recomendamos crear una copia de seguridad antes de actualizar.', - 'admin.update.backupLink': 'Ir a Copia de seguridad', - 'admin.update.howTo': 'Cómo actualizar', - 'admin.update.dockerText': 'Tu instancia de NOMAD se ejecuta en Docker. Para actualizar a {version}, ejecuta los siguientes comandos en tu servidor:', - 'admin.update.reloadHint': 'Recarga la página en unos segundos.', - - // Vacay addon - 'vacay.subtitle': 'Planifica y gestiona días de vacaciones', - 'vacay.settings': 'Ajustes', - 'vacay.year': 'Año', - 'vacay.addYear': 'Añadir año', - 'vacay.removeYear': 'Eliminar año', - 'vacay.removeYearConfirm': '¿Eliminar {year}?', - 'vacay.removeYearHint': 'Todas las vacaciones y festivos de empresa de este año se borrarán permanentemente.', - 'vacay.remove': 'Eliminar', - 'vacay.persons': 'Personas', - 'vacay.noPersons': 'No se han añadido personas', - 'vacay.addPerson': 'Añadir persona', - 'vacay.editPerson': 'Editar persona', - 'vacay.removePerson': 'Eliminar persona', - 'vacay.removePersonConfirm': '¿Eliminar a {name}?', - 'vacay.removePersonHint': 'Todas las vacaciones de esta persona se borrarán permanentemente.', - 'vacay.personName': 'Nombre', - 'vacay.personNamePlaceholder': 'Introduce un nombre', - 'vacay.color': 'Color', - 'vacay.add': 'Añadir', - 'vacay.legend': 'Leyenda', - 'vacay.publicHoliday': 'Festivo', - 'vacay.companyHoliday': 'Festivo de empresa', - 'vacay.weekend': 'Fin de semana', - 'vacay.modeVacation': 'Vacaciones', - 'vacay.modeCompany': 'Festivo de empresa', - 'vacay.entitlement': 'Derecho', - 'vacay.entitlementDays': 'Días', - 'vacay.used': 'Usados', - 'vacay.remaining': 'Restantes', - 'vacay.carriedOver': 'de {year}', - 'vacay.blockWeekends': 'Bloquear fines de semana', - 'vacay.blockWeekendsHint': 'Impide marcar vacaciones en sábados y domingos', - 'vacay.publicHolidays': 'Festivos', - 'vacay.publicHolidaysHint': 'Marcar festivos en el calendario', - 'vacay.selectCountry': 'Seleccionar país', - 'vacay.selectRegion': 'Seleccionar región (opcional)', - 'vacay.companyHolidays': 'Festivos de empresa', - 'vacay.companyHolidaysHint': 'Permitir marcar días festivos comunes de la empresa', - 'vacay.companyHolidaysNoDeduct': 'Los festivos de empresa no descuentan días de vacaciones.', - 'vacay.carryOver': 'Arrastrar saldo', - 'vacay.carryOverHint': 'Trasladar automáticamente los días restantes al año siguiente', - 'vacay.sharing': 'Compartir', - 'vacay.sharingHint': 'Comparte tu calendario de vacaciones con otros usuarios de NOMAD', - 'vacay.owner': 'Propietario', - 'vacay.shareEmailPlaceholder': 'Correo electrónico del usuario de NOMAD', - 'vacay.shareSuccess': 'Plan compartido correctamente', - 'vacay.shareError': 'No se pudo compartir el plan', - 'vacay.dissolve': 'Deshacer fusión', - 'vacay.dissolveHint': 'Separar de nuevo los calendarios. Tus entradas se conservarán.', - 'vacay.dissolveAction': 'Disolver', - 'vacay.dissolved': 'Calendario separado', - 'vacay.fusedWith': 'Fusionado con', - 'vacay.you': 'tú', - 'vacay.noData': 'Sin datos', - 'vacay.changeColor': 'Cambiar color', - 'vacay.inviteUser': 'Invitar usuario', - 'vacay.inviteHint': 'Invita a otro usuario de NOMAD a compartir un calendario combinado de vacaciones.', - 'vacay.selectUser': 'Seleccionar usuario', - 'vacay.sendInvite': 'Enviar invitación', - 'vacay.inviteSent': 'Invitación enviada', - 'vacay.inviteError': 'No se pudo enviar la invitación', - 'vacay.pending': 'pendiente', - 'vacay.noUsersAvailable': 'No hay usuarios disponibles', - 'vacay.accept': 'Aceptar', - 'vacay.decline': 'Rechazar', - 'vacay.acceptFusion': 'Aceptar y fusionar', - 'vacay.inviteTitle': 'Solicitud de fusión', - 'vacay.inviteWantsToFuse': 'quiere compartir un calendario de vacaciones contigo.', - 'vacay.fuseInfo1': 'Ambos veréis todas las entradas de vacaciones en un único calendario compartido.', - 'vacay.fuseInfo2': 'Ambas partes pueden crear y editar entradas mutuamente.', - 'vacay.fuseInfo3': 'Ambas partes pueden borrar entradas y cambiar el número de días de vacaciones disponibles.', - 'vacay.fuseInfo4': 'Ajustes como festivos y festivos de empresa se comparten.', - 'vacay.fuseInfo5': 'La fusión puede disolverse en cualquier momento por cualquiera de las partes. Tus entradas se conservarán.', - - // Atlas addon - 'atlas.subtitle': 'Tu huella viajera por el mundo', - 'atlas.countries': 'Países', - 'atlas.trips': 'Viajes', - 'atlas.places': 'Lugares', - 'atlas.days': 'Días', - 'atlas.visitedCountries': 'Países visitados', - 'atlas.cities': 'Ciudades', - 'atlas.noData': 'Aún no hay datos de viaje', - 'atlas.noDataHint': 'Crea un viaje y añade lugares para ver tu mapa del mundo', - 'atlas.lastTrip': 'Último viaje', - 'atlas.nextTrip': 'Próximo viaje', - 'atlas.daysLeft': 'días restantes', - 'atlas.streak': 'Racha', - 'atlas.year': 'año', - 'atlas.years': 'años', - 'atlas.yearInRow': 'año seguido', - 'atlas.yearsInRow': 'años seguidos', - 'atlas.tripIn': 'viaje en', - 'atlas.tripsIn': 'viajes en', - 'atlas.since': 'desde', - 'atlas.europe': 'Europa', - 'atlas.asia': 'Asia', - 'atlas.northAmerica': 'América del Norte', - 'atlas.southAmerica': 'América del Sur', - 'atlas.africa': 'África', - 'atlas.oceania': 'Oceanía', - 'atlas.other': 'Otros', - 'atlas.firstVisit': 'Primer viaje', - 'atlas.lastVisitLabel': 'Último viaje', - 'atlas.tripSingular': 'Viaje', - 'atlas.tripPlural': 'Viajes', - 'atlas.placeVisited': 'Lugar visitado', - 'atlas.placesVisited': 'Lugares visitados', - - // Trip Planner - 'trip.tabs.plan': 'Plan', - 'trip.tabs.reservations': 'Reservas', - 'trip.tabs.reservationsShort': 'Reservas', - 'trip.tabs.packing': 'Lista de equipaje', - 'trip.tabs.packingShort': 'Equipaje', - 'trip.tabs.budget': 'Presupuesto', - 'trip.tabs.memories': 'Recuerdos', - 'trip.tabs.files': 'Archivos', - 'trip.loading': 'Cargando viaje...', - 'trip.mobilePlan': 'Plan', - 'trip.mobilePlaces': 'Lugares', - 'trip.toast.placeUpdated': 'Lugar actualizado', - 'trip.toast.placeAdded': 'Lugar añadido', - 'trip.toast.placeDeleted': 'Lugar eliminado', - 'trip.toast.selectDay': 'Selecciona primero un día', - 'trip.toast.assignedToDay': 'Lugar asignado al día', - 'trip.toast.reorderError': 'No se pudo reordenar', - 'trip.toast.reservationUpdated': 'Reserva actualizada', - 'trip.toast.reservationAdded': 'Reserva añadida', - 'trip.toast.deleted': 'Eliminado', - 'trip.confirm.deletePlace': '¿Seguro que quieres eliminar este lugar?', - - // Day Plan Sidebar - 'dayplan.emptyDay': 'No hay lugares planificados para este día', - 'dayplan.addNote': 'Añadir nota', - 'dayplan.editNote': 'Editar nota', - 'dayplan.noteAdd': 'Añadir nota', - 'dayplan.noteEdit': 'Editar nota', - 'dayplan.noteTitle': 'Nota', - 'dayplan.noteSubtitle': 'Nota diaria', - 'dayplan.totalCost': 'Coste total', - 'dayplan.days': 'Días', - 'dayplan.dayN': 'Día {n}', - 'dayplan.calculating': 'Calculando...', - 'dayplan.route': 'Ruta', - 'dayplan.optimize': 'Optimizar', - 'dayplan.optimized': 'Ruta optimizada', - 'dayplan.routeError': 'No se pudo calcular la ruta', - 'dayplan.toast.needTwoPlaces': 'Se necesitan al menos dos lugares para optimizar la ruta', - 'dayplan.toast.routeOptimized': 'Ruta optimizada', - 'dayplan.toast.noGeoPlaces': 'No se encontraron lugares con coordenadas para calcular la ruta', - 'dayplan.confirmed': 'Confirmado', - 'dayplan.pendingRes': 'Pendiente', - 'dayplan.pdf': 'PDF', - 'dayplan.pdfTooltip': 'Exportar plan diario como PDF', - 'dayplan.pdfError': 'No se pudo exportar el PDF', - - // Places Sidebar - 'places.addPlace': 'Añadir lugar/actividad', - 'places.assignToDay': '¿A qué día añadirlo?', - 'places.all': 'Todo', - 'places.unplanned': 'Sin planificar', - 'places.search': 'Buscar lugares...', - 'places.allCategories': 'Todas las categorías', - 'places.count': '{count} lugares', - 'places.countSingular': '1 lugar', - 'places.allPlanned': 'Todos los lugares están planificados', - 'places.noneFound': 'No se encontraron lugares', - 'places.editPlace': 'Editar lugar', - 'places.formName': 'Nombre', - 'places.formNamePlaceholder': 'p. ej. Torre Eiffel', - 'places.formDescription': 'Descripción', - 'places.formDescriptionPlaceholder': 'Descripción breve...', - 'places.formAddress': 'Dirección', - 'places.formAddressPlaceholder': 'Calle, ciudad, país', - 'places.formLat': 'Latitud (p. ej. 48.8566)', - 'places.formLng': 'Longitud (p. ej. 2.3522)', - 'places.formCategory': 'Categoría', - 'places.noCategory': 'Sin categoría', - 'places.categoryNamePlaceholder': 'Nombre de la categoría', - 'places.formTime': 'Hora', - 'places.startTime': 'Inicio', - 'places.endTime': 'Fin', - 'places.endTimeBeforeStart': 'La hora de fin es anterior a la de inicio', - 'places.timeCollision': 'Solapamiento horario con:', - 'places.formWebsite': 'Página web', - 'places.formNotesPlaceholder': 'Notas personales...', - 'places.formReservation': 'Reserva', - 'places.reservationNotesPlaceholder': 'Notas de reserva, número de confirmación...', - 'places.mapsSearchPlaceholder': 'Buscar lugares...', - 'places.mapsSearchError': 'La búsqueda de lugares falló.', - 'places.osmHint': 'Usando búsqueda con OpenStreetMap (sin fotos, horarios ni valoraciones). Añade una clave API de Google en Ajustes para obtener todos los detalles.', - 'places.osmActive': 'Búsqueda mediante OpenStreetMap (sin fotos, valoraciones ni horarios). Añade una clave API de Google en Ajustes para datos ampliados.', - 'places.categoryCreateError': 'No se pudo crear la categoría', - 'places.nameRequired': 'Introduce un nombre', - 'places.saveError': 'No se pudo guardar', - - // Place Inspector - 'inspector.opened': 'Abierto', - 'inspector.closed': 'Cerrado', - 'inspector.openingHours': 'Horario de apertura', - 'inspector.showHours': 'Mostrar horario', - 'inspector.files': 'Archivos', - 'inspector.filesCount': '{count} archivos', - 'inspector.removeFromDay': 'Quitar del día', - 'inspector.addToDay': 'Añadir al día', - 'inspector.confirmedRes': 'Reserva confirmada', - 'inspector.pendingRes': 'Reserva pendiente', - 'inspector.google': 'Abrir en Google Maps', - 'inspector.website': 'Abrir la web', - 'inspector.addRes': 'Reserva', - 'inspector.editRes': 'Editar reserva', - 'inspector.participants': 'Participantes', - - // Reservations - 'reservations.title': 'Reservas', - 'reservations.empty': 'Aún no hay reservas', - 'reservations.emptyHint': 'Añade reservas de vuelos, hoteles y más', - 'reservations.add': 'Añadir reserva', - 'reservations.addManual': 'Reserva manual', - 'reservations.placeHint': 'Consejo: es mejor crear las reservas directamente desde un lugar para vincularlas con el plan del día.', - 'reservations.confirmed': 'Confirmada', - 'reservations.pending': 'Pendiente', - 'reservations.summary': '{confirmed} confirmadas, {pending} pendientes', - 'reservations.fromPlan': 'Del plan', - 'reservations.showFiles': 'Mostrar archivos', - 'reservations.editTitle': 'Editar reserva', - 'reservations.status': 'Estado', - 'reservations.datetime': 'Fecha y hora', - 'reservations.startTime': 'Hora de inicio', - 'reservations.endTime': 'Hora de fin', - 'reservations.date': 'Fecha', - 'reservations.time': 'Hora', - 'reservations.timeAlt': 'Hora (alternativa, p. ej. 19:30)', - 'reservations.notes': 'Notas', - 'reservations.notesPlaceholder': 'Notas adicionales...', - 'reservations.type.flight': 'Vuelo', - 'reservations.type.hotel': 'Hotel', - 'reservations.type.restaurant': 'Restaurante', - 'reservations.type.train': 'Tren', - 'reservations.type.car': 'Coche de alquiler', - 'reservations.type.cruise': 'Crucero', - 'reservations.type.event': 'Evento', - 'reservations.type.tour': 'Tour', - 'reservations.type.other': 'Otro', - 'reservations.confirm.delete': '¿Seguro que quieres eliminar la reserva "{name}"?', - 'reservations.toast.updated': 'Reserva actualizada', - 'reservations.toast.removed': 'Reserva eliminada', - 'reservations.toast.fileUploaded': 'Archivo subido', - 'reservations.toast.uploadError': 'No se pudo subir', - 'reservations.newTitle': 'Nueva reserva', - 'reservations.bookingType': 'Tipo de reserva', - 'reservations.titleLabel': 'Título', - 'reservations.titlePlaceholder': 'p. ej. Lufthansa LH123, Hotel Adlon, ...', - 'reservations.locationAddress': 'Ubicación / dirección', - 'reservations.locationPlaceholder': 'Dirección, aeropuerto, hotel...', - 'reservations.confirmationCode': 'Código de reserva', - 'reservations.confirmationPlaceholder': 'p. ej. ABC12345', - 'reservations.day': 'Día', - 'reservations.noDay': 'Sin día', - 'reservations.place': 'Lugar', - 'reservations.noPlace': 'Sin lugar', - 'reservations.pendingSave': 'se guardará…', - 'reservations.uploading': 'Subiendo...', - 'reservations.attachFile': 'Adjuntar archivo', - 'reservations.toast.saveError': 'No se pudo guardar', - 'reservations.toast.updateError': 'No se pudo actualizar', - 'reservations.toast.deleteError': 'No se pudo eliminar', - 'reservations.confirm.remove': '¿Eliminar la reserva de "{name}"?', - 'reservations.linkAssignment': 'Vincular a una asignación del día', - 'reservations.pickAssignment': 'Selecciona una asignación de tu plan...', - 'reservations.noAssignment': 'Sin vínculo (independiente)', - - // Budget - 'budget.title': 'Presupuesto', - 'budget.emptyTitle': 'Aún no se ha creado ningún presupuesto', - 'budget.emptyText': 'Crea categorías y entradas para planificar el presupuesto de tu viaje', - 'budget.emptyPlaceholder': 'Introduce el nombre de la categoría...', - 'budget.createCategory': 'Crear categoría', - 'budget.category': 'Categoría', - 'budget.categoryName': 'Nombre de la categoría', - 'budget.table.name': 'Nombre', - 'budget.table.total': 'Total', - 'budget.table.persons': 'Personas', - 'budget.table.days': 'Días', - 'budget.table.perPerson': 'Por persona', - 'budget.table.perDay': 'Por día', - 'budget.table.perPersonDay': 'Por pers. / día', - 'budget.table.note': 'Nota', - 'budget.newEntry': 'Nueva entrada', - 'budget.defaultEntry': 'Nueva entrada', - 'budget.defaultCategory': 'Nueva categoría', - 'budget.total': 'Total', - 'budget.totalBudget': 'Presupuesto total', - 'budget.byCategory': 'Por categoría', - 'budget.editTooltip': 'Haz clic para editar', - 'budget.confirm.deleteCategory': '¿Seguro que quieres eliminar la categoría "{name}" con {count} entradas?', - 'budget.deleteCategory': 'Eliminar categoría', - 'budget.perPerson': 'Por persona', - 'budget.paid': 'Pagado', - 'budget.open': 'Abrir', - 'budget.noMembers': 'No hay miembros asignados', - - // Files - 'files.title': 'Archivos', - 'files.count': '{count} archivos', - 'files.countSingular': '1 archivo', - 'files.uploaded': '{count} archivos subidos', - 'files.uploadError': 'La subida falló', - 'files.dropzone': 'Arrastra aquí los archivos', - 'files.dropzoneHint': 'o haz clic para explorar', - 'files.allowedTypes': 'Imágenes, PDF, DOC, DOCX, XLS, XLSX, TXT, CSV · Máx. 50 MB', - 'files.uploading': 'Subiendo...', - 'files.filterAll': 'Todo', - 'files.filterPdf': 'PDF', - 'files.filterImages': 'Imágenes', - 'files.filterDocs': 'Documentos', - 'files.filterCollab': 'Notas de colaboración', - 'files.sourceCollab': 'Desde notas de colaboración', - 'files.empty': 'Aún no hay archivos', - 'files.emptyHint': 'Sube archivos para adjuntarlos a tu viaje', - 'files.openTab': 'Abrir en una pestaña nueva', - 'files.confirm.delete': '¿Seguro que quieres eliminar este archivo?', - 'files.toast.deleted': 'Archivo eliminado', - 'files.toast.deleteError': 'No se pudo eliminar el archivo', - 'files.sourcePlan': 'Plan diario', - 'files.sourceBooking': 'Reserva', - 'files.attach': 'Adjuntar', - 'files.pasteHint': 'También puedes pegar imágenes desde el portapapeles (Ctrl+V)', - - // Packing - 'packing.title': 'Lista de equipaje', - 'packing.empty': 'La lista de equipaje está vacía', - 'packing.progress': '{packed} de {total} preparados ({percent}%)', - 'packing.clearChecked': 'Eliminar {count} marcados', - 'packing.clearCheckedShort': 'Eliminar {count}', - 'packing.suggestions': 'Sugerencias', - 'packing.suggestionsTitle': 'Añadir sugerencias', - 'packing.allSuggested': 'Todas las sugerencias añadidas', - 'packing.allPacked': '¡Todo preparado!', - 'packing.addPlaceholder': 'Añadir nuevo elemento...', - 'packing.categoryPlaceholder': 'Categoría...', - 'packing.filterAll': 'Todo', - 'packing.filterOpen': 'Pendientes', - 'packing.filterDone': 'Hecho', - 'packing.emptyTitle': 'La lista de equipaje está vacía', - 'packing.emptyHint': 'Añade elementos o usa las sugerencias', - 'packing.emptyFiltered': 'Ningún elemento coincide con este filtro', - 'packing.menuRename': 'Renombrar', - 'packing.menuCheckAll': 'Marcar todo', - 'packing.menuUncheckAll': 'Desmarcar todo', - 'packing.menuDeleteCat': 'Eliminar categoría', - 'packing.changeCategory': 'Cambiar categoría', - 'packing.confirm.clearChecked': '¿Seguro que quieres eliminar {count} elementos marcados?', - 'packing.confirm.deleteCat': '¿Seguro que quieres eliminar la categoría "{name}" con {count} elementos?', - 'packing.defaultCategory': 'Otros', - 'packing.toast.saveError': 'No se pudo guardar', - 'packing.toast.deleteError': 'No se pudo eliminar', - 'packing.toast.renameError': 'No se pudo renombrar', - 'packing.toast.addError': 'No se pudo añadir', - - // Packing suggestions - 'packing.suggestions.items': [ - { name: 'Pasaporte', category: 'Documentos' }, - { name: 'Documento de identidad', category: 'Documentos' }, - { name: 'Seguro de viaje', category: 'Documentos' }, - { name: 'Billetes de vuelo', category: 'Documentos' }, - { name: 'Tarjeta de crédito', category: 'Finanzas' }, - { name: 'Efectivo', category: 'Finanzas' }, - { name: 'Visado', category: 'Documentos' }, - { name: 'Camisetas', category: 'Ropa' }, - { name: 'Pantalones', category: 'Ropa' }, - { name: 'Ropa interior', category: 'Ropa' }, - { name: 'Calcetines', category: 'Ropa' }, - { name: 'Chaqueta', category: 'Ropa' }, - { name: 'Pijama', category: 'Ropa' }, - { name: 'Ropa de baño', category: 'Ropa' }, - { name: 'Impermeable', category: 'Ropa' }, - { name: 'Zapatos cómodos', category: 'Ropa' }, - { name: 'Cepillo de dientes', category: 'Aseo' }, - { name: 'Pasta de dientes', category: 'Aseo' }, - { name: 'Champú', category: 'Aseo' }, - { name: 'Desodorante', category: 'Aseo' }, - { name: 'Protector solar', category: 'Aseo' }, - { name: 'Maquinilla de afeitar', category: 'Aseo' }, - { name: 'Cargador', category: 'Electrónica' }, - { name: 'Batería externa', category: 'Electrónica' }, - { name: 'Auriculares', category: 'Electrónica' }, - { name: 'Adaptador de viaje', category: 'Electrónica' }, - { name: 'Cámara', category: 'Electrónica' }, - { name: 'Analgésicos', category: 'Salud' }, - { name: 'Tiritas', category: 'Salud' }, - { name: 'Desinfectante', category: 'Salud' }, - ], - - // Members / Sharing - 'members.shareTrip': 'Compartir viaje', - 'members.inviteUser': 'Invitar usuario', - 'members.selectUser': 'Seleccionar usuario…', - 'members.invite': 'Invitar', - 'members.allHaveAccess': 'Todos los usuarios ya tienen acceso.', - 'members.access': 'Acceso', - 'members.person': 'persona', - 'members.persons': 'personas', - 'members.you': 'tú', - 'members.owner': 'Propietario', - 'members.leaveTrip': 'Abandonar viaje', - 'members.removeAccess': 'Quitar acceso', - 'members.confirmLeave': '¿Abandonar el viaje? Perderás el acceso.', - 'members.confirmRemove': '¿Quitar el acceso de este usuario?', - 'members.loadError': 'No se pudieron cargar los miembros', - 'members.added': 'añadido', - 'members.addError': 'No se pudo añadir', - 'members.removed': 'Miembro eliminado', - 'members.removeError': 'No se pudo eliminar', - - // Categories (Admin) - 'categories.title': 'Categorías', - 'categories.subtitle': 'Gestiona categorías para lugares', - 'categories.new': 'Nueva categoría', - 'categories.empty': 'Aún no hay categorías', - 'categories.namePlaceholder': 'Nombre de la categoría', - 'categories.icon': 'Icono', - 'categories.color': 'Color', - 'categories.customColor': 'Elegir color personalizado', - 'categories.preview': 'Vista previa', - 'categories.defaultName': 'Categoría', - 'categories.update': 'Actualizar', - 'categories.create': 'Crear', - 'categories.confirm.delete': '¿Eliminar la categoría? Los lugares de esta categoría no se eliminarán.', - 'categories.toast.loadError': 'No se pudieron cargar las categorías', - 'categories.toast.nameRequired': 'Introduce un nombre', - 'categories.toast.updated': 'Categoría actualizada', - 'categories.toast.created': 'Categoría creada', - 'categories.toast.saveError': 'No se pudo guardar', - 'categories.toast.deleted': 'Categoría eliminada', - 'categories.toast.deleteError': 'No se pudo eliminar', - - // Backup (Admin) - 'backup.title': 'Copia de seguridad de datos', - 'backup.subtitle': 'Base de datos y todos los archivos subidos', - 'backup.refresh': 'Actualizar', - 'backup.upload': 'Subir copia de seguridad', - 'backup.uploading': 'Subiendo…', - 'backup.create': 'Crear copia', - 'backup.creating': 'Creando…', - 'backup.empty': 'Aún no hay copias', - 'backup.createFirst': 'Crear la primera copia', - 'backup.download': 'Descargar', - 'backup.restore': 'Restaurar', - 'backup.confirm.restore': '¿Restaurar la copia "{name}"?\n\nTodos los datos actuales serán reemplazados por la copia.', - 'backup.confirm.uploadRestore': '¿Subir y restaurar el archivo de copia "{name}"?\n\nTodos los datos actuales se sobrescribirán.', - 'backup.confirm.delete': '¿Eliminar la copia "{name}"?', - 'backup.toast.loadError': 'No se pudieron cargar las copias', - 'backup.toast.created': 'Copia de seguridad creada correctamente', - 'backup.toast.createError': 'No se pudo crear la copia', - 'backup.toast.restored': 'Copia restaurada. La página se recargará…', - 'backup.toast.restoreError': 'No se pudo restaurar', - 'backup.toast.uploadError': 'No se pudo subir', - 'backup.toast.deleted': 'Copia eliminada', - 'backup.toast.deleteError': 'No se pudo eliminar', - 'backup.toast.downloadError': 'La descarga falló', - 'backup.toast.settingsSaved': 'Ajustes de copia automática guardados', - 'backup.toast.settingsError': 'No se pudieron guardar los ajustes', - 'backup.auto.title': 'Copia automática', - 'backup.auto.subtitle': 'Copia de seguridad automática según una programación', - 'backup.auto.enable': 'Activar copia automática', - 'backup.auto.enableHint': 'Se crearán copias automáticamente según la frecuencia elegida', - 'backup.auto.interval': 'Intervalo', - 'backup.auto.keepLabel': 'Eliminar copias antiguas después de', - 'backup.interval.hourly': 'Cada hora', - 'backup.interval.daily': 'Diaria', - 'backup.interval.weekly': 'Semanal', - 'backup.interval.monthly': 'Mensual', - 'backup.keep.1day': '1 día', - 'backup.keep.3days': '3 días', - 'backup.keep.7days': '7 días', - 'backup.keep.14days': '14 días', - 'backup.keep.30days': '30 días', - 'backup.keep.forever': 'Conservar para siempre', - - // Photos - 'photos.allDays': 'Todos los días', - 'photos.title': 'Recuerdos', - 'photos.noPhotos': 'Aún no hay fotos', - 'photos.uploadHint': 'Sube y organiza las fotos compartidas de este viaje', - 'photos.clickToSelect': 'o haz clic para seleccionar', - 'photos.dropHere': 'Suelta aquí las fotos...', - 'photos.dropTitle': 'Suelta aquí las fotos', - 'photos.fileHint': 'JPG, PNG, GIF, WebP · máx. 10 MB · hasta 30 fotos', - 'photos.selectedCount': '{count} foto(s) seleccionada(s)', - 'photos.sharedAlbum': '{count} recuerdos en este álbum compartido', - 'photos.sharedAlbumFor': '{count} recuerdos en {trip}', - 'photos.allPlaces': 'Todos los lugares', - 'photos.view.grid': 'Cuadrícula', - 'photos.view.day': 'Por día', - 'photos.view.place': 'Por lugar', - 'photos.stats.total': 'Fotos', - 'photos.stats.days': 'Días', - 'photos.stats.places': 'Lugares', - 'photos.stats.latest': 'Última subida', - 'photos.sectionCount': '{count} foto(s)', - 'photos.ungrouped': 'Sin clasificar', - 'photos.featured': 'Recuerdo destacado', - 'photos.coverFallback': 'Portada del álbum compartido', - 'photos.coverHint': 'Una imagen destacada para este álbum de viaje', - 'photos.mapTitle': 'Mapa de recuerdos', - 'photos.mapHint': 'Explora los lugares vinculados en el mismo mapa que usamos en el plan', - 'photos.mapEmpty': 'Vincula tus fotos a lugares para verlas ubicadas en el mapa.', - 'photos.linkDay': 'Vincular día', - 'photos.noDay': 'Sin día', - 'photos.linkPlace': 'Vincular lugar', - 'photos.noPlace': 'Sin lugar', - 'photos.captionLabel': 'Pie de foto (para todas)', - 'photos.captionPlaceholder': 'Pie de foto opcional...', - 'photos.addCaption': 'Añadir un pie de foto...', - 'photos.uploadN': 'Subida de {n} foto(s)', - 'admin.addons.catalog.memories.name': 'Recuerdos', - 'admin.addons.catalog.memories.description': 'Álbumes de fotos compartidos para cada viaje', - 'admin.addons.catalog.packing.name': 'Equipaje', - 'admin.addons.catalog.packing.description': 'Prepara tu equipaje con listas de comprobación para cada viaje', - 'admin.addons.catalog.budget.name': 'Presupuesto', - 'admin.addons.catalog.budget.description': 'Controla los gastos y planifica el presupuesto del viaje', - 'admin.addons.catalog.documents.name': 'Documentos', - 'admin.addons.catalog.documents.description': 'Guarda y gestiona la documentación del viaje', - 'admin.addons.catalog.vacay.name': 'Vacaciones', - 'admin.addons.catalog.vacay.description': 'Planificador personal de vacaciones con vista de calendario', - 'admin.addons.catalog.atlas.name': 'Atlas', - 'admin.addons.catalog.atlas.description': 'Mapa del mundo con los países visitados y estadísticas de viaje', - 'admin.addons.catalog.collab.name': 'Colaboración', - 'admin.addons.catalog.collab.description': 'Notas, encuestas y chat en tiempo real para organizar el viaje', - - // Backup restore modal - 'backup.restoreConfirmTitle': '¿Restaurar copia?', - 'backup.restoreWarning': 'Todos los datos actuales (viajes, lugares, usuarios, subidas) serán reemplazados permanentemente por la copia. Esta acción no se puede deshacer.', - 'backup.restoreTip': 'Consejo: crea una copia del estado actual antes de restaurar.', - 'backup.restoreConfirm': 'Sí, restaurar', - - // PDF - 'pdf.travelPlan': 'Plan de viaje', - 'pdf.planned': 'Planificado', - 'pdf.costLabel': 'Coste EUR', - 'pdf.preview': 'Vista previa PDF', - 'pdf.saveAsPdf': 'Guardar como PDF', - - // Planner - 'planner.places': 'Lugares', - 'planner.bookings': 'Reservas', - 'planner.packingList': 'Lista de equipaje', - 'planner.documents': 'Documentos', - 'planner.dayPlan': 'Plan por días', - 'planner.reservations': 'Reservas', - 'planner.minTwoPlaces': 'Se necesitan al menos 2 lugares con coordenadas', - 'planner.noGeoPlaces': 'No hay lugares con coordenadas disponibles', - 'planner.routeCalculated': 'Ruta calculada', - 'planner.routeCalcFailed': 'No se pudo calcular la ruta', - 'planner.routeError': 'Error al calcular la ruta', - 'planner.routeOptimized': 'Ruta optimizada', - 'planner.reservationUpdated': 'Reserva actualizada', - 'planner.reservationAdded': 'Reserva añadida', - 'planner.confirmDeleteReservation': '¿Eliminar reserva?', - 'planner.reservationDeleted': 'Reserva eliminada', - 'planner.days': 'Días', - 'planner.allPlaces': 'Todos los lugares', - 'planner.totalPlaces': '{n} lugares en total', - 'planner.noDaysPlanned': 'Aún no hay días planificados', - 'planner.editTrip': 'Editar viaje →', - 'planner.placeOne': '1 lugar', - 'planner.placeN': '{n} lugares', - 'planner.addNote': 'Añadir nota', - 'planner.noEntries': 'No hay entradas para este día', - 'planner.addPlace': 'Añadir lugar/actividad', - 'planner.addPlaceShort': '+ Añadir lugar/actividad', - 'planner.resPending': 'Reserva pendiente · ', - 'planner.resConfirmed': 'Reserva confirmada · ', - 'planner.notePlaceholder': 'Nota…', - 'planner.noteTimePlaceholder': 'Hora (opcional)', - 'planner.noteExamplePlaceholder': 'p. ej. S3 a las 14:30 desde la estación central, ferry desde el muelle 7, pausa para comer…', - 'planner.totalCost': 'Coste total', - 'planner.searchPlaces': 'Buscar lugares…', - 'planner.allCategories': 'Todas las categorías', - 'planner.noPlacesFound': 'No se encontraron lugares', - 'planner.addFirstPlace': 'Añadir el primer lugar', - 'planner.noReservations': 'Sin reservas', - 'planner.addFirstReservation': 'Añadir la primera reserva', - 'planner.new': 'Nuevo', - 'planner.addToDay': '+ Día', - 'planner.calculating': 'Calculando…', - 'planner.route': 'Ruta', - 'planner.optimize': 'Optimizar', - 'planner.openGoogleMaps': 'Abrir en Google Maps', - 'planner.selectDayHint': 'Selecciona un día de la lista izquierda para ver su plan', - 'planner.noPlacesForDay': 'Aún no hay lugares para este día', - 'planner.addPlacesLink': 'Añadir lugares →', - 'planner.minTotal': 'min en total', - 'planner.noReservation': 'Sin reserva', - 'planner.removeFromDay': 'Quitar del día', - 'planner.addToThisDay': 'Añadir al día', - 'planner.overview': 'Vista general', - 'planner.noDays': 'No hay días todavía', - 'planner.editTripToAddDays': 'Edita el viaje para añadir días', - 'planner.dayCount': '{n} días', - 'planner.clickToUnlock': 'Haz clic para desbloquear', - 'planner.keepPosition': 'Mantener posición durante la optimización de ruta', - 'planner.dayDetails': 'Detalles del día', - 'planner.dayN': 'Día {n}', - 'planner.notes': 'Notas', - 'planner.addDayNote': 'Añadir notas para este día...', - - // Dashboard Stats - 'stats.countries': 'Países', - 'stats.cities': 'Ciudades', - 'stats.trips': 'Viajes', - 'stats.places': 'Lugares', - 'stats.worldProgress': 'Progreso mundial', - 'stats.visited': 'visitados', - 'stats.remaining': 'restantes', - 'stats.visitedCountries': 'Países visitados', - - // Day Detail Panel - 'day.precipProb': 'Probabilidad de lluvia', - 'day.precipitation': 'Precipitación', - 'day.wind': 'Viento', - 'day.sunrise': 'Amanecer', - 'day.sunset': 'Atardecer', - 'day.hourlyForecast': 'Pronóstico por horas', - 'day.climateHint': 'Promedios históricos: el pronóstico real está disponible dentro de los 16 días previos a la fecha.', - 'day.noWeather': 'No hay datos meteorológicos disponibles. Añade un lugar con coordenadas.', - 'day.overview': 'Resumen diario', - 'day.accommodation': 'Alojamiento', - 'day.addAccommodation': 'Añadir alojamiento', - 'day.hotelDayRange': 'Aplicar a los días', - 'day.noPlacesForHotel': 'Añade primero lugares al viaje', - 'day.allDays': 'Todos', - 'day.checkIn': 'Check-in', - 'day.checkOut': 'Check-out', - 'day.confirmation': 'Confirmación', - 'day.editAccommodation': 'Editar alojamiento', - 'day.reservations': 'Reservas', - - // Collab Addon - 'collab.tabs.chat': 'Mensajes', - 'collab.tabs.notes': 'Notas', - 'collab.tabs.polls': 'Encuestas', - 'collab.whatsNext.title': 'Qué viene ahora', - 'collab.whatsNext.today': 'Hoy', - 'collab.whatsNext.tomorrow': 'Mañana', - 'collab.whatsNext.empty': 'No hay actividades próximas', - 'collab.whatsNext.until': 'hasta', - 'collab.whatsNext.emptyHint': 'Las actividades con hora aparecerán aquí', - 'collab.chat.send': 'Enviar', - 'collab.chat.placeholder': 'Escribe un mensaje...', - 'collab.chat.empty': 'Empieza la conversación', - 'collab.chat.emptyHint': 'Los mensajes se comparten con todos los miembros del viaje', - 'collab.chat.emptyDesc': 'Comparte ideas, planes y novedades con tu grupo de viaje', - 'collab.chat.today': 'Hoy', - 'collab.chat.yesterday': 'Ayer', - 'collab.chat.deletedMessage': 'eliminó un mensaje', - 'collab.chat.loadMore': 'Cargar mensajes anteriores', - 'collab.chat.justNow': 'justo ahora', - 'collab.chat.minutesAgo': 'hace {n} min', - 'collab.chat.hoursAgo': 'hace {n} h', - 'collab.notes.title': 'Notas', - 'collab.notes.new': 'Nueva nota', - 'collab.notes.empty': 'Aún no hay notas', - 'collab.notes.emptyHint': 'Empieza a capturar ideas y planes', - 'collab.notes.all': 'Todas', - 'collab.notes.titlePlaceholder': 'Título de la nota', - 'collab.notes.contentPlaceholder': 'Escribe algo...', - 'collab.notes.categoryPlaceholder': 'Categoría', - 'collab.notes.newCategory': 'Nueva categoría...', - 'collab.notes.category': 'Categoría', - 'collab.notes.noCategory': 'Sin categoría', - 'collab.notes.color': 'Color', - 'collab.notes.save': 'Guardar', - 'collab.notes.cancel': 'Cancelar', - 'collab.notes.edit': 'Editar', - 'collab.notes.delete': 'Eliminar', - 'collab.notes.pin': 'Fijar', - 'collab.notes.unpin': 'Desfijar', - 'collab.notes.daysAgo': 'hace {n} d', - 'collab.notes.categorySettings': 'Gestionar categorías', - 'collab.notes.create': 'Crear', - 'collab.notes.website': 'Sitio web', - 'collab.notes.websitePlaceholder': 'https://...', - 'collab.notes.attachFiles': 'Adjuntar archivos', - 'collab.notes.noCategoriesYet': 'Aún no hay categorías', - 'collab.notes.emptyDesc': 'Crea una nota para empezar', - 'collab.polls.title': 'Encuestas', - 'collab.polls.new': 'Nueva encuesta', - 'collab.polls.empty': 'Aún no hay encuestas', - 'collab.polls.emptyHint': 'Pregunta al grupo y votad juntos', - 'collab.polls.question': 'Pregunta', - 'collab.polls.questionPlaceholder': '¿Qué deberíamos hacer?', - 'collab.polls.addOption': '+ Añadir opción', - 'collab.polls.optionPlaceholder': 'Opción {n}', - 'collab.polls.create': 'Crear encuesta', - 'collab.polls.close': 'Cerrar', - 'collab.polls.closed': 'Cerrada', - 'collab.polls.votes': '{n} votos', - 'collab.polls.vote': '{n} voto', - 'collab.polls.multipleChoice': 'Selección múltiple', - 'collab.polls.multiChoice': 'Selección múltiple', - 'collab.polls.deadline': 'Fecha límite', - 'collab.polls.option': 'Opción', - 'collab.polls.options': 'Opciones', - 'collab.polls.delete': 'Eliminar', - 'collab.polls.closedSection': 'Cerradas', -} - -export default es diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 9e11b4e..132ac0b 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -1 +1,1113 @@ -export { default } from './es.js' +const es: Record = { + // Common + 'common.save': 'Guardar', + 'common.cancel': 'Cancelar', + 'common.delete': 'Eliminar', + 'common.edit': 'Editar', + 'common.add': 'Añadir', + 'common.loading': 'Cargando...', + 'common.error': 'Error', + 'common.back': 'Atrás', + 'common.all': 'Todo', + 'common.close': 'Cerrar', + 'common.open': 'Abrir', + 'common.upload': 'Subir', + 'common.search': 'Buscar', + 'common.confirm': 'Confirmar', + 'common.ok': 'Aceptar', + 'common.yes': 'Sí', + 'common.no': 'No', + 'common.or': 'o', + 'common.none': 'Ninguno', + 'common.date': 'Fecha', + 'common.rename': 'Renombrar', + 'common.name': 'Nombre', + 'common.email': 'Correo', + 'common.password': 'Contraseña', + 'common.saving': 'Guardando...', + 'common.update': 'Actualizar', + 'common.change': 'Cambiar', + 'common.uploading': 'Subiendo…', + 'common.backToPlanning': 'Volver a la planificación', + 'common.reset': 'Restablecer', + + // Navbar + 'nav.trip': 'Viaje', + 'nav.share': 'Compartir', + 'nav.settings': 'Ajustes', + 'nav.admin': 'Administración', + 'nav.logout': 'Cerrar sesión', + 'nav.lightMode': 'Modo claro', + 'nav.darkMode': 'Modo oscuro', + 'nav.autoMode': 'Modo automático', + 'nav.administrator': 'Administrador', + 'nav.myTrips': 'Mis viajes', + + // Dashboard + 'dashboard.title': 'Mis viajes', + 'dashboard.subtitle.loading': 'Cargando viajes...', + 'dashboard.subtitle.trips': '{count} viajes ({archived} archivados)', + 'dashboard.subtitle.empty': 'Empieza tu primer viaje', + 'dashboard.subtitle.activeOne': '{count} viaje activo', + 'dashboard.subtitle.activeMany': '{count} viajes activos', + 'dashboard.subtitle.archivedSuffix': ' · {count} archivados', + 'dashboard.newTrip': 'Nuevo viaje', + 'dashboard.currency': 'Divisa', + 'dashboard.timezone': 'Zonas horarias', + 'dashboard.localTime': 'Hora local', + 'dashboard.emptyTitle': 'Aún no hay viajes', + 'dashboard.emptyText': 'Crea tu primer viaje y empieza a planificar', + 'dashboard.emptyButton': 'Crear primer viaje', + 'dashboard.nextTrip': 'Próximo viaje', + 'dashboard.shared': 'Compartido', + 'dashboard.sharedBy': 'Compartido por {name}', + 'dashboard.days': 'Días', + 'dashboard.places': 'Lugares', + 'dashboard.archive': 'Archivar', + 'dashboard.restore': 'Restaurar', + 'dashboard.archived': 'Archivado', + 'dashboard.status.ongoing': 'En curso', + 'dashboard.status.today': 'Hoy', + 'dashboard.status.tomorrow': 'Mañana', + 'dashboard.status.past': 'Pasado', + 'dashboard.status.daysLeft': 'Quedan {count} días', + 'dashboard.toast.loadError': 'No se pudieron cargar los viajes', + 'dashboard.toast.created': '¡Viaje creado correctamente!', + 'dashboard.toast.createError': 'No se pudo crear el viaje', + 'dashboard.toast.updated': '¡Viaje actualizado!', + 'dashboard.toast.updateError': 'No se pudo actualizar el viaje', + 'dashboard.toast.deleted': 'Viaje eliminado', + 'dashboard.toast.deleteError': 'No se pudo eliminar el viaje', + 'dashboard.toast.archived': 'Viaje archivado', + 'dashboard.toast.archiveError': 'No se pudo archivar el viaje', + 'dashboard.toast.restored': 'Viaje restaurado', + 'dashboard.toast.restoreError': 'No se pudo restaurar el viaje', + 'dashboard.confirm.delete': '¿Eliminar el viaje "{title}"? Todos los lugares y planes se borrarán permanentemente.', + 'dashboard.editTrip': 'Editar viaje', + 'dashboard.createTrip': 'Crear nuevo viaje', + 'dashboard.tripTitle': 'Título', + 'dashboard.tripTitlePlaceholder': 'p. ej. Verano en Japón', + 'dashboard.tripDescription': 'Descripción', + 'dashboard.tripDescriptionPlaceholder': '¿De qué trata este viaje?', + 'dashboard.startDate': 'Fecha de inicio', + 'dashboard.endDate': 'Fecha de fin', + 'dashboard.noDateHint': 'Sin fecha definida: se crearán 7 días por defecto. Puedes cambiarlo cuando quieras.', + 'dashboard.coverImage': 'Imagen de portada', + 'dashboard.addCoverImage': 'Añadir imagen de portada', + 'dashboard.coverSaved': 'Imagen de portada guardada', + 'dashboard.coverUploadError': 'Error al subir la imagen', + 'dashboard.coverRemoveError': 'Error al eliminar la imagen', + 'dashboard.titleRequired': 'El título es obligatorio', + 'dashboard.endDateError': 'La fecha de fin debe ser posterior a la de inicio', + + // Settings + 'settings.title': 'Ajustes', + 'settings.subtitle': 'Configura tus ajustes personales', + 'settings.map': 'Mapa', + 'settings.mapTemplate': 'Plantilla del mapa', + 'settings.mapTemplatePlaceholder.select': 'Seleccionar plantilla...', + 'settings.mapDefaultHint': 'Déjalo vacío para OpenStreetMap (por defecto)', + 'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + 'settings.mapHint': 'Plantilla de URL para los mosaicos del mapa', + 'settings.latitude': 'Latitud', + 'settings.longitude': 'Longitud', + 'settings.saveMap': 'Guardar mapa', + 'settings.apiKeys': 'Claves API', + 'settings.mapsKey': 'Clave API de Google Maps', + 'settings.mapsKeyHint': 'Necesaria para buscar lugares. Consíguela en console.cloud.google.com', + 'settings.weatherKey': 'Clave API de OpenWeatherMap', + 'settings.weatherKeyHint': 'Para datos meteorológicos. Gratis en openweathermap.org/api', + 'settings.keyPlaceholder': 'Introduce la clave...', + 'settings.configured': 'Configurado', + 'settings.saveKeys': 'Guardar claves', + 'settings.display': 'Visualización', + 'settings.colorMode': 'Modo de color', + 'settings.light': 'Claro', + 'settings.dark': 'Oscuro', + 'settings.auto': 'Automático', + 'settings.language': 'Idioma', + 'settings.temperature': 'Unidad de temperatura', + 'settings.timeFormat': 'Formato de hora', + 'settings.routeCalculation': 'Cálculo de ruta', + 'settings.on': 'Activado', + 'settings.off': 'Desactivado', + 'settings.account': 'Cuenta', + 'settings.username': 'Usuario', + 'settings.email': 'Correo', + 'settings.role': 'Rol', + 'settings.roleAdmin': 'Administrador', + 'settings.oidcLinked': 'Vinculado con', + 'settings.changePassword': 'Cambiar contraseña', + 'settings.currentPassword': 'Contraseña actual', + 'settings.newPassword': 'Nueva contraseña', + 'settings.confirmPassword': 'Confirmar nueva contraseña', + 'settings.updatePassword': 'Actualizar contraseña', + 'settings.passwordRequired': 'Introduce la contraseña actual y la nueva', + 'settings.passwordTooShort': 'La contraseña debe tener al menos 8 caracteres', + 'settings.passwordMismatch': 'Las contraseñas no coinciden', + 'settings.passwordChanged': 'Contraseña cambiada correctamente', + 'settings.deleteAccount': 'Eliminar cuenta', + 'settings.deleteAccountTitle': '¿Eliminar tu cuenta?', + 'settings.deleteAccountWarning': 'Tu cuenta y todos tus viajes, lugares y archivos se eliminarán permanentemente. Esta acción no se puede deshacer.', + 'settings.deleteAccountConfirm': 'Eliminar permanentemente', + 'settings.deleteBlockedTitle': 'No es posible eliminarla', + 'settings.deleteBlockedMessage': 'Eres el único administrador. Asciende a otro usuario a administrador antes de eliminar tu cuenta.', + 'settings.roleUser': 'Usuario', + 'settings.saveProfile': 'Guardar perfil', + 'settings.toast.mapSaved': 'Ajustes del mapa guardados', + 'settings.toast.keysSaved': 'Claves API guardadas', + 'settings.toast.displaySaved': 'Ajustes de visualización guardados', + 'settings.toast.profileSaved': 'Perfil guardado', + 'settings.uploadAvatar': 'Subir foto de perfil', + 'settings.removeAvatar': 'Eliminar foto de perfil', + 'settings.avatarUploaded': 'Foto de perfil actualizada', + 'settings.avatarRemoved': 'Foto de perfil eliminada', + 'settings.avatarError': 'Falló la subida', + + // Login + 'login.error': 'Inicio de sesión fallido. Revisa tus credenciales.', + 'login.tagline': 'Tus viajes.\nTu plan.', + 'login.description': 'Planifica viajes en colaboración con mapas interactivos, presupuestos y sincronización en tiempo real.', + 'login.features.maps': 'Mapas interactivos', + 'login.features.mapsDesc': 'Google Places, rutas y agrupación', + 'login.features.realtime': 'Sincronización en tiempo real', + 'login.features.realtimeDesc': 'Planificad juntos mediante WebSocket', + 'login.features.budget': 'Control de presupuesto', + 'login.features.budgetDesc': 'Categorías, gráficos y costes por persona', + 'login.features.collab': 'Colaboración', + 'login.features.collabDesc': 'Multiusuario con viajes compartidos', + 'login.features.packing': 'Listas de equipaje', + 'login.features.packingDesc': 'Categorías, progreso y sugerencias', + 'login.features.bookings': 'Reservas', + 'login.features.bookingsDesc': 'Vuelos, hoteles, restaurantes y más', + 'login.features.files': 'Documentos', + 'login.features.filesDesc': 'Sube y gestiona documentos', + 'login.features.routes': 'Rutas inteligentes', + 'login.features.routesDesc': 'Optimización automática y exportación a Google Maps', + 'login.selfHosted': 'Autoalojado · Código abierto · Tus datos siguen siendo tuyos', + 'login.title': 'Iniciar sesión', + 'login.subtitle': 'Bienvenido de nuevo', + 'login.signingIn': 'Iniciando sesión…', + 'login.signIn': 'Entrar', + 'login.createAdmin': 'Crear cuenta de administrador', + 'login.createAdminHint': 'Configura la primera cuenta administradora de NOMAD.', + 'login.createAccount': 'Crear cuenta', + 'login.createAccountHint': 'Crea una cuenta nueva.', + 'login.creating': 'Creando…', + 'login.noAccount': '¿No tienes cuenta?', + 'login.hasAccount': '¿Ya tienes cuenta?', + 'login.register': 'Registrarse', + 'login.emailPlaceholder': 'tu@correo.com', + 'login.username': 'Usuario', + 'login.oidc.registrationDisabled': 'El registro está desactivado. Contacta con tu administrador.', + 'login.oidc.noEmail': 'No se recibió ningún correo del proveedor.', + 'login.oidc.tokenFailed': 'La autenticación falló.', + 'login.oidc.invalidState': 'Sesión no válida. Inténtalo de nuevo.', + 'login.demoFailed': 'Falló el acceso a la demo', + 'login.oidcSignIn': 'Entrar con {name}', + 'login.demoHint': 'Prueba la demo: no necesitas registrarte', + + // Register + 'register.passwordMismatch': 'Las contraseñas no coinciden', + 'register.passwordTooShort': 'La contraseña debe tener al menos 6 caracteres', + 'register.failed': 'Falló el registro', + 'register.getStarted': 'Empezar', + 'register.subtitle': 'Crea una cuenta y empieza a planificar tus viajes.', + 'register.feature1': 'Planes de viaje ilimitados', + 'register.feature2': 'Vista de mapa interactiva', + 'register.feature3': 'Gestiona lugares y categorías', + 'register.feature4': 'Haz seguimiento de las reservas', + 'register.feature5': 'Crea listas de equipaje', + 'register.feature6': 'Guarda fotos y archivos', + 'register.createAccount': 'Crear cuenta', + 'register.startPlanning': 'Empieza a planificar tu viaje', + 'register.minChars': 'Mín. 6 caracteres', + 'register.confirmPassword': 'Confirmar contraseña', + 'register.repeatPassword': 'Repetir contraseña', + 'register.registering': 'Registrando...', + 'register.register': 'Registrarse', + 'register.hasAccount': '¿Ya tienes cuenta?', + 'register.signIn': 'Iniciar sesión', + + // Admin + 'admin.title': 'Administración', + 'admin.subtitle': 'Gestión de usuarios y ajustes del sistema', + 'admin.tabs.users': 'Usuarios', + 'admin.tabs.categories': 'Categorías', + 'admin.tabs.backup': 'Copia de seguridad', + 'admin.stats.users': 'Usuarios', + 'admin.stats.trips': 'Viajes', + 'admin.stats.places': 'Lugares', + 'admin.stats.photos': 'Fotos', + 'admin.stats.files': 'Archivos', + 'admin.table.user': 'Usuario', + 'admin.table.email': 'Correo', + 'admin.table.role': 'Rol', + 'admin.table.created': 'Creado', + 'admin.table.lastLogin': 'Último acceso', + 'admin.table.actions': 'Acciones', + 'admin.you': '(Tú)', + 'admin.editUser': 'Editar usuario', + 'admin.newPassword': 'Nueva contraseña', + 'admin.newPasswordHint': 'Déjalo vacío para mantener la contraseña actual', + 'admin.deleteUser': '¿Eliminar al usuario "{name}"? Todos sus viajes se borrarán permanentemente.', + 'admin.deleteUserTitle': 'Eliminar usuario', + 'admin.newPasswordPlaceholder': 'Introduce una nueva contraseña…', + 'admin.toast.loadError': 'No se pudieron cargar los datos de administración', + 'admin.toast.userUpdated': 'Usuario actualizado', + 'admin.toast.updateError': 'No se pudo actualizar', + 'admin.toast.userDeleted': 'Usuario eliminado', + 'admin.toast.deleteError': 'No se pudo eliminar', + 'admin.toast.cannotDeleteSelf': 'No puedes eliminar tu propia cuenta', + 'admin.toast.userCreated': 'Usuario creado', + 'admin.toast.createError': 'No se pudo crear el usuario', + 'admin.toast.fieldsRequired': 'Usuario, correo y contraseña son obligatorios', + 'admin.createUser': 'Crear usuario', + 'admin.tabs.settings': 'Ajustes', + 'admin.allowRegistration': 'Permitir el registro', + 'admin.allowRegistrationHint': 'Los nuevos usuarios pueden registrarse por sí mismos', + 'admin.apiKeys': 'Claves API', + 'admin.apiKeysHint': 'Opcional. Activa datos ampliados de lugares, como fotos y previsión del tiempo.', + 'admin.mapsKey': 'Clave API de Google Maps', + 'admin.mapsKeyHint': 'Obligatoria para buscar lugares. Consíguela en console.cloud.google.com', + 'admin.mapsKeyHintLong': 'Sin una clave API, la búsqueda de lugares usa OpenStreetMap. Con una clave de Google también se pueden cargar fotos, valoraciones y horarios de apertura. Consíguela en console.cloud.google.com.', + 'admin.recommended': 'Recomendado', + 'admin.weatherKey': 'Clave API de OpenWeatherMap', + 'admin.weatherKeyHint': 'Para datos meteorológicos. Gratis en openweathermap.org', + 'admin.validateKey': 'Probar', + 'admin.keyValid': 'Conectado', + 'admin.keyInvalid': 'No válida', + 'admin.keySaved': 'Claves API guardadas', + 'admin.oidcTitle': 'Inicio de sesión único (OIDC)', + 'admin.oidcSubtitle': 'Permite iniciar sesión mediante proveedores externos como Google, Apple, Authentik o Keycloak.', + 'admin.oidcDisplayName': 'Nombre visible', + 'admin.oidcIssuer': 'URL del emisor', + 'admin.oidcIssuerHint': 'La URL Issuer de OpenID Connect del proveedor. Ej.: https://accounts.google.com', + 'admin.oidcSaved': 'Configuración OIDC guardada', + + // File Types + 'admin.fileTypes': 'Tipos de archivo permitidos', + 'admin.fileTypesHint': 'Configura qué tipos de archivo pueden subir los usuarios.', + 'admin.fileTypesFormat': 'Extensiones separadas por comas (p. ej. jpg,png,pdf,doc). Usa * para permitir todos los tipos.', + 'admin.fileTypesSaved': 'Ajustes de tipos de archivo guardados', + + // Addons + 'admin.tabs.addons': 'Complementos', + 'admin.addons.title': 'Complementos', + 'admin.addons.subtitle': 'Activa o desactiva funciones para personalizar tu experiencia en NOMAD.', + 'admin.addons.subtitleBefore': 'Activa o desactiva funciones para personalizar tu experiencia en ', + 'admin.addons.subtitleAfter': '.', + 'admin.addons.enabled': 'Activo', + 'admin.addons.disabled': 'Desactivado', + 'admin.addons.type.trip': 'Viaje', + 'admin.addons.type.global': 'Global', + 'admin.addons.tripHint': 'Disponible como pestaña dentro de cada viaje', + 'admin.addons.globalHint': 'Disponible como sección independiente en la navegación principal', + 'admin.addons.toast.updated': 'Complemento actualizado', + 'admin.addons.toast.error': 'No se pudo actualizar el complemento', + 'admin.addons.noAddons': 'No hay complementos disponibles', + 'admin.weather.title': 'Datos meteorológicos', + 'admin.weather.badge': 'Desde el 24 de marzo de 2026', + 'admin.weather.description': 'NOMAD utiliza Open-Meteo como fuente de datos meteorológicos. Open-Meteo es un servicio meteorológico gratuito y de código abierto: no requiere clave API.', + 'admin.weather.forecast': 'Pronóstico de 16 días', + 'admin.weather.forecastDesc': 'Antes eran 5 días (OpenWeatherMap)', + 'admin.weather.climate': 'Datos climáticos históricos', + 'admin.weather.climateDesc': 'Promedios de los últimos 85 años para fechas posteriores al pronóstico de 16 días', + 'admin.weather.requests': '10.000 solicitudes / día', + 'admin.weather.requestsDesc': 'Gratis, sin necesidad de clave API', + 'admin.weather.locationHint': 'El tiempo se basa en el primer lugar con coordenadas de cada día. Si no hay ningún lugar asignado a un día, se usa como referencia cualquier lugar de la lista.', + + // GitHub + 'admin.tabs.github': 'GitHub', + 'admin.github.title': 'Historial de versiones', + 'admin.github.subtitle': 'Últimas novedades de {repo}', + 'admin.github.latest': 'Última', + 'admin.github.prerelease': 'Prelanzamiento', + 'admin.github.showDetails': 'Mostrar detalles', + 'admin.github.hideDetails': 'Ocultar detalles', + 'admin.github.loadMore': 'Cargar más', + 'admin.github.loading': 'Cargando...', + 'admin.github.error': 'No se pudieron cargar las versiones', + 'admin.github.by': 'por', + 'admin.update.available': 'Actualización disponible', + 'admin.update.text': 'NOMAD {version} está disponible. Estás usando {current}.', + 'admin.update.button': 'Ver en GitHub', + 'admin.update.install': 'Instalar actualización', + 'admin.update.confirmTitle': '¿Instalar actualización?', + 'admin.update.confirmText': 'NOMAD se actualizará de {current} a {version}. Después, el servidor se reiniciará automáticamente.', + 'admin.update.dataInfo': 'Todos tus datos (viajes, usuarios, claves API, subidas, Vacay, Atlas, presupuestos) se conservarán.', + 'admin.update.warning': 'La app estará brevemente no disponible durante el reinicio.', + 'admin.update.confirm': 'Actualizar ahora', + 'admin.update.installing': 'Actualizando…', + 'admin.update.success': '¡Actualización instalada! El servidor se está reiniciando…', + 'admin.update.failed': 'La actualización falló', + 'admin.update.backupHint': 'Recomendamos crear una copia de seguridad antes de actualizar.', + 'admin.update.backupLink': 'Ir a Copia de seguridad', + 'admin.update.howTo': 'Cómo actualizar', + 'admin.update.dockerText': 'Tu instancia de NOMAD se ejecuta en Docker. Para actualizar a {version}, ejecuta los siguientes comandos en tu servidor:', + 'admin.update.reloadHint': 'Recarga la página en unos segundos.', + + // Vacay addon + 'vacay.subtitle': 'Planifica y gestiona días de vacaciones', + 'vacay.settings': 'Ajustes', + 'vacay.year': 'Año', + 'vacay.addYear': 'Añadir año', + 'vacay.removeYear': 'Eliminar año', + 'vacay.removeYearConfirm': '¿Eliminar {year}?', + 'vacay.removeYearHint': 'Todas las vacaciones y festivos de empresa de este año se borrarán permanentemente.', + 'vacay.remove': 'Eliminar', + 'vacay.persons': 'Personas', + 'vacay.noPersons': 'No se han añadido personas', + 'vacay.addPerson': 'Añadir persona', + 'vacay.editPerson': 'Editar persona', + 'vacay.removePerson': 'Eliminar persona', + 'vacay.removePersonConfirm': '¿Eliminar a {name}?', + 'vacay.removePersonHint': 'Todas las vacaciones de esta persona se borrarán permanentemente.', + 'vacay.personName': 'Nombre', + 'vacay.personNamePlaceholder': 'Introduce un nombre', + 'vacay.color': 'Color', + 'vacay.add': 'Añadir', + 'vacay.legend': 'Leyenda', + 'vacay.publicHoliday': 'Festivo', + 'vacay.companyHoliday': 'Festivo de empresa', + 'vacay.weekend': 'Fin de semana', + 'vacay.modeVacation': 'Vacaciones', + 'vacay.modeCompany': 'Festivo de empresa', + 'vacay.entitlement': 'Derecho', + 'vacay.entitlementDays': 'Días', + 'vacay.used': 'Usados', + 'vacay.remaining': 'Restantes', + 'vacay.carriedOver': 'de {year}', + 'vacay.blockWeekends': 'Bloquear fines de semana', + 'vacay.blockWeekendsHint': 'Impide marcar vacaciones en sábados y domingos', + 'vacay.publicHolidays': 'Festivos', + 'vacay.publicHolidaysHint': 'Marcar festivos en el calendario', + 'vacay.selectCountry': 'Seleccionar país', + 'vacay.selectRegion': 'Seleccionar región (opcional)', + 'vacay.companyHolidays': 'Festivos de empresa', + 'vacay.companyHolidaysHint': 'Permitir marcar días festivos comunes de la empresa', + 'vacay.companyHolidaysNoDeduct': 'Los festivos de empresa no descuentan días de vacaciones.', + 'vacay.carryOver': 'Arrastrar saldo', + 'vacay.carryOverHint': 'Trasladar automáticamente los días restantes al año siguiente', + 'vacay.sharing': 'Compartir', + 'vacay.sharingHint': 'Comparte tu calendario de vacaciones con otros usuarios de NOMAD', + 'vacay.owner': 'Propietario', + 'vacay.shareEmailPlaceholder': 'Correo electrónico del usuario de NOMAD', + 'vacay.shareSuccess': 'Plan compartido correctamente', + 'vacay.shareError': 'No se pudo compartir el plan', + 'vacay.dissolve': 'Deshacer fusión', + 'vacay.dissolveHint': 'Separar de nuevo los calendarios. Tus entradas se conservarán.', + 'vacay.dissolveAction': 'Disolver', + 'vacay.dissolved': 'Calendario separado', + 'vacay.fusedWith': 'Fusionado con', + 'vacay.you': 'tú', + 'vacay.noData': 'Sin datos', + 'vacay.changeColor': 'Cambiar color', + 'vacay.inviteUser': 'Invitar usuario', + 'vacay.inviteHint': 'Invita a otro usuario de NOMAD a compartir un calendario combinado de vacaciones.', + 'vacay.selectUser': 'Seleccionar usuario', + 'vacay.sendInvite': 'Enviar invitación', + 'vacay.inviteSent': 'Invitación enviada', + 'vacay.inviteError': 'No se pudo enviar la invitación', + 'vacay.pending': 'pendiente', + 'vacay.noUsersAvailable': 'No hay usuarios disponibles', + 'vacay.accept': 'Aceptar', + 'vacay.decline': 'Rechazar', + 'vacay.acceptFusion': 'Aceptar y fusionar', + 'vacay.inviteTitle': 'Solicitud de fusión', + 'vacay.inviteWantsToFuse': 'quiere compartir un calendario de vacaciones contigo.', + 'vacay.fuseInfo1': 'Ambos veréis todas las entradas de vacaciones en un único calendario compartido.', + 'vacay.fuseInfo2': 'Ambas partes pueden crear y editar entradas mutuamente.', + 'vacay.fuseInfo3': 'Ambas partes pueden borrar entradas y cambiar el número de días de vacaciones disponibles.', + 'vacay.fuseInfo4': 'Ajustes como festivos y festivos de empresa se comparten.', + 'vacay.fuseInfo5': 'La fusión puede disolverse en cualquier momento por cualquiera de las partes. Tus entradas se conservarán.', + + // Atlas addon + 'atlas.subtitle': 'Tu huella viajera por el mundo', + 'atlas.countries': 'Países', + 'atlas.trips': 'Viajes', + 'atlas.places': 'Lugares', + 'atlas.days': 'Días', + 'atlas.visitedCountries': 'Países visitados', + 'atlas.cities': 'Ciudades', + 'atlas.noData': 'Aún no hay datos de viaje', + 'atlas.noDataHint': 'Crea un viaje y añade lugares para ver tu mapa del mundo', + 'atlas.lastTrip': 'Último viaje', + 'atlas.nextTrip': 'Próximo viaje', + 'atlas.daysLeft': 'días restantes', + 'atlas.streak': 'Racha', + 'atlas.year': 'año', + 'atlas.years': 'años', + 'atlas.yearInRow': 'año seguido', + 'atlas.yearsInRow': 'años seguidos', + 'atlas.tripIn': 'viaje en', + 'atlas.tripsIn': 'viajes en', + 'atlas.since': 'desde', + 'atlas.europe': 'Europa', + 'atlas.asia': 'Asia', + 'atlas.northAmerica': 'América del Norte', + 'atlas.southAmerica': 'América del Sur', + 'atlas.africa': 'África', + 'atlas.oceania': 'Oceanía', + 'atlas.other': 'Otros', + 'atlas.firstVisit': 'Primer viaje', + 'atlas.lastVisitLabel': 'Último viaje', + 'atlas.tripSingular': 'Viaje', + 'atlas.tripPlural': 'Viajes', + 'atlas.placeVisited': 'Lugar visitado', + 'atlas.placesVisited': 'Lugares visitados', + + // Trip Planner + 'trip.tabs.plan': 'Plan', + 'trip.tabs.reservations': 'Reservas', + 'trip.tabs.reservationsShort': 'Reservas', + 'trip.tabs.packing': 'Lista de equipaje', + 'trip.tabs.packingShort': 'Equipaje', + 'trip.tabs.budget': 'Presupuesto', + 'trip.tabs.memories': 'Recuerdos', + 'trip.tabs.files': 'Archivos', + 'trip.loading': 'Cargando viaje...', + 'trip.mobilePlan': 'Plan', + 'trip.mobilePlaces': 'Lugares', + 'trip.toast.placeUpdated': 'Lugar actualizado', + 'trip.toast.placeAdded': 'Lugar añadido', + 'trip.toast.placeDeleted': 'Lugar eliminado', + 'trip.toast.selectDay': 'Selecciona primero un día', + 'trip.toast.assignedToDay': 'Lugar asignado al día', + 'trip.toast.reorderError': 'No se pudo reordenar', + 'trip.toast.reservationUpdated': 'Reserva actualizada', + 'trip.toast.reservationAdded': 'Reserva añadida', + 'trip.toast.deleted': 'Eliminado', + 'trip.confirm.deletePlace': '¿Seguro que quieres eliminar este lugar?', + + // Day Plan Sidebar + 'dayplan.emptyDay': 'No hay lugares planificados para este día', + 'dayplan.addNote': 'Añadir nota', + 'dayplan.editNote': 'Editar nota', + 'dayplan.noteAdd': 'Añadir nota', + 'dayplan.noteEdit': 'Editar nota', + 'dayplan.noteTitle': 'Nota', + 'dayplan.noteSubtitle': 'Nota diaria', + 'dayplan.totalCost': 'Coste total', + 'dayplan.days': 'Días', + 'dayplan.dayN': 'Día {n}', + 'dayplan.calculating': 'Calculando...', + 'dayplan.route': 'Ruta', + 'dayplan.optimize': 'Optimizar', + 'dayplan.optimized': 'Ruta optimizada', + 'dayplan.routeError': 'No se pudo calcular la ruta', + 'dayplan.toast.needTwoPlaces': 'Se necesitan al menos dos lugares para optimizar la ruta', + 'dayplan.toast.routeOptimized': 'Ruta optimizada', + 'dayplan.toast.noGeoPlaces': 'No se encontraron lugares con coordenadas para calcular la ruta', + 'dayplan.confirmed': 'Confirmado', + 'dayplan.pendingRes': 'Pendiente', + 'dayplan.pdf': 'PDF', + 'dayplan.pdfTooltip': 'Exportar plan diario como PDF', + 'dayplan.pdfError': 'No se pudo exportar el PDF', + + // Places Sidebar + 'places.addPlace': 'Añadir lugar/actividad', + 'places.assignToDay': '¿A qué día añadirlo?', + 'places.all': 'Todo', + 'places.unplanned': 'Sin planificar', + 'places.search': 'Buscar lugares...', + 'places.allCategories': 'Todas las categorías', + 'places.count': '{count} lugares', + 'places.countSingular': '1 lugar', + 'places.allPlanned': 'Todos los lugares están planificados', + 'places.noneFound': 'No se encontraron lugares', + 'places.editPlace': 'Editar lugar', + 'places.formName': 'Nombre', + 'places.formNamePlaceholder': 'p. ej. Torre Eiffel', + 'places.formDescription': 'Descripción', + 'places.formDescriptionPlaceholder': 'Descripción breve...', + 'places.formAddress': 'Dirección', + 'places.formAddressPlaceholder': 'Calle, ciudad, país', + 'places.formLat': 'Latitud (p. ej. 48.8566)', + 'places.formLng': 'Longitud (p. ej. 2.3522)', + 'places.formCategory': 'Categoría', + 'places.noCategory': 'Sin categoría', + 'places.categoryNamePlaceholder': 'Nombre de la categoría', + 'places.formTime': 'Hora', + 'places.startTime': 'Inicio', + 'places.endTime': 'Fin', + 'places.endTimeBeforeStart': 'La hora de fin es anterior a la de inicio', + 'places.timeCollision': 'Solapamiento horario con:', + 'places.formWebsite': 'Página web', + 'places.formNotesPlaceholder': 'Notas personales...', + 'places.formReservation': 'Reserva', + 'places.reservationNotesPlaceholder': 'Notas de reserva, número de confirmación...', + 'places.mapsSearchPlaceholder': 'Buscar lugares...', + 'places.mapsSearchError': 'La búsqueda de lugares falló.', + 'places.osmHint': 'Usando búsqueda con OpenStreetMap (sin fotos, horarios ni valoraciones). Añade una clave API de Google en Ajustes para obtener todos los detalles.', + 'places.osmActive': 'Búsqueda mediante OpenStreetMap (sin fotos, valoraciones ni horarios). Añade una clave API de Google en Ajustes para datos ampliados.', + 'places.categoryCreateError': 'No se pudo crear la categoría', + 'places.nameRequired': 'Introduce un nombre', + 'places.saveError': 'No se pudo guardar', + + // Place Inspector + 'inspector.opened': 'Abierto', + 'inspector.closed': 'Cerrado', + 'inspector.openingHours': 'Horario de apertura', + 'inspector.showHours': 'Mostrar horario', + 'inspector.files': 'Archivos', + 'inspector.filesCount': '{count} archivos', + 'inspector.removeFromDay': 'Quitar del día', + 'inspector.addToDay': 'Añadir al día', + 'inspector.confirmedRes': 'Reserva confirmada', + 'inspector.pendingRes': 'Reserva pendiente', + 'inspector.google': 'Abrir en Google Maps', + 'inspector.website': 'Abrir la web', + 'inspector.addRes': 'Reserva', + 'inspector.editRes': 'Editar reserva', + 'inspector.participants': 'Participantes', + + // Reservations + 'reservations.title': 'Reservas', + 'reservations.empty': 'Aún no hay reservas', + 'reservations.emptyHint': 'Añade reservas de vuelos, hoteles y más', + 'reservations.add': 'Añadir reserva', + 'reservations.addManual': 'Reserva manual', + 'reservations.placeHint': 'Consejo: es mejor crear las reservas directamente desde un lugar para vincularlas con el plan del día.', + 'reservations.confirmed': 'Confirmada', + 'reservations.pending': 'Pendiente', + 'reservations.summary': '{confirmed} confirmadas, {pending} pendientes', + 'reservations.fromPlan': 'Del plan', + 'reservations.showFiles': 'Mostrar archivos', + 'reservations.editTitle': 'Editar reserva', + 'reservations.status': 'Estado', + 'reservations.datetime': 'Fecha y hora', + 'reservations.startTime': 'Hora de inicio', + 'reservations.endTime': 'Hora de fin', + 'reservations.date': 'Fecha', + 'reservations.time': 'Hora', + 'reservations.timeAlt': 'Hora (alternativa, p. ej. 19:30)', + 'reservations.notes': 'Notas', + 'reservations.notesPlaceholder': 'Notas adicionales...', + 'reservations.type.flight': 'Vuelo', + 'reservations.type.hotel': 'Hotel', + 'reservations.type.restaurant': 'Restaurante', + 'reservations.type.train': 'Tren', + 'reservations.type.car': 'Coche de alquiler', + 'reservations.type.cruise': 'Crucero', + 'reservations.type.event': 'Evento', + 'reservations.type.tour': 'Tour', + 'reservations.type.other': 'Otro', + 'reservations.confirm.delete': '¿Seguro que quieres eliminar la reserva "{name}"?', + 'reservations.toast.updated': 'Reserva actualizada', + 'reservations.toast.removed': 'Reserva eliminada', + 'reservations.toast.fileUploaded': 'Archivo subido', + 'reservations.toast.uploadError': 'No se pudo subir', + 'reservations.newTitle': 'Nueva reserva', + 'reservations.bookingType': 'Tipo de reserva', + 'reservations.titleLabel': 'Título', + 'reservations.titlePlaceholder': 'p. ej. Lufthansa LH123, Hotel Adlon, ...', + 'reservations.locationAddress': 'Ubicación / dirección', + 'reservations.locationPlaceholder': 'Dirección, aeropuerto, hotel...', + 'reservations.confirmationCode': 'Código de reserva', + 'reservations.confirmationPlaceholder': 'p. ej. ABC12345', + 'reservations.day': 'Día', + 'reservations.noDay': 'Sin día', + 'reservations.place': 'Lugar', + 'reservations.noPlace': 'Sin lugar', + 'reservations.pendingSave': 'se guardará…', + 'reservations.uploading': 'Subiendo...', + 'reservations.attachFile': 'Adjuntar archivo', + 'reservations.toast.saveError': 'No se pudo guardar', + 'reservations.toast.updateError': 'No se pudo actualizar', + 'reservations.toast.deleteError': 'No se pudo eliminar', + 'reservations.confirm.remove': '¿Eliminar la reserva de "{name}"?', + 'reservations.linkAssignment': 'Vincular a una asignación del día', + 'reservations.pickAssignment': 'Selecciona una asignación de tu plan...', + 'reservations.noAssignment': 'Sin vínculo (independiente)', + + // Budget + 'budget.title': 'Presupuesto', + 'budget.emptyTitle': 'Aún no se ha creado ningún presupuesto', + 'budget.emptyText': 'Crea categorías y entradas para planificar el presupuesto de tu viaje', + 'budget.emptyPlaceholder': 'Introduce el nombre de la categoría...', + 'budget.createCategory': 'Crear categoría', + 'budget.category': 'Categoría', + 'budget.categoryName': 'Nombre de la categoría', + 'budget.table.name': 'Nombre', + 'budget.table.total': 'Total', + 'budget.table.persons': 'Personas', + 'budget.table.days': 'Días', + 'budget.table.perPerson': 'Por persona', + 'budget.table.perDay': 'Por día', + 'budget.table.perPersonDay': 'Por pers. / día', + 'budget.table.note': 'Nota', + 'budget.newEntry': 'Nueva entrada', + 'budget.defaultEntry': 'Nueva entrada', + 'budget.defaultCategory': 'Nueva categoría', + 'budget.total': 'Total', + 'budget.totalBudget': 'Presupuesto total', + 'budget.byCategory': 'Por categoría', + 'budget.editTooltip': 'Haz clic para editar', + 'budget.confirm.deleteCategory': '¿Seguro que quieres eliminar la categoría "{name}" con {count} entradas?', + 'budget.deleteCategory': 'Eliminar categoría', + 'budget.perPerson': 'Por persona', + 'budget.paid': 'Pagado', + 'budget.open': 'Abrir', + 'budget.noMembers': 'No hay miembros asignados', + + // Files + 'files.title': 'Archivos', + 'files.count': '{count} archivos', + 'files.countSingular': '1 archivo', + 'files.uploaded': '{count} archivos subidos', + 'files.uploadError': 'La subida falló', + 'files.dropzone': 'Arrastra aquí los archivos', + 'files.dropzoneHint': 'o haz clic para explorar', + 'files.allowedTypes': 'Imágenes, PDF, DOC, DOCX, XLS, XLSX, TXT, CSV · Máx. 50 MB', + 'files.uploading': 'Subiendo...', + 'files.filterAll': 'Todo', + 'files.filterPdf': 'PDF', + 'files.filterImages': 'Imágenes', + 'files.filterDocs': 'Documentos', + 'files.filterCollab': 'Notas de colaboración', + 'files.sourceCollab': 'Desde notas de colaboración', + 'files.empty': 'Aún no hay archivos', + 'files.emptyHint': 'Sube archivos para adjuntarlos a tu viaje', + 'files.openTab': 'Abrir en una pestaña nueva', + 'files.confirm.delete': '¿Seguro que quieres eliminar este archivo?', + 'files.toast.deleted': 'Archivo eliminado', + 'files.toast.deleteError': 'No se pudo eliminar el archivo', + 'files.sourcePlan': 'Plan diario', + 'files.sourceBooking': 'Reserva', + 'files.attach': 'Adjuntar', + 'files.pasteHint': 'También puedes pegar imágenes desde el portapapeles (Ctrl+V)', + + // Packing + 'packing.title': 'Lista de equipaje', + 'packing.empty': 'La lista de equipaje está vacía', + 'packing.progress': '{packed} de {total} preparados ({percent}%)', + 'packing.clearChecked': 'Eliminar {count} marcados', + 'packing.clearCheckedShort': 'Eliminar {count}', + 'packing.suggestions': 'Sugerencias', + 'packing.suggestionsTitle': 'Añadir sugerencias', + 'packing.allSuggested': 'Todas las sugerencias añadidas', + 'packing.allPacked': '¡Todo preparado!', + 'packing.addPlaceholder': 'Añadir nuevo elemento...', + 'packing.categoryPlaceholder': 'Categoría...', + 'packing.filterAll': 'Todo', + 'packing.filterOpen': 'Pendientes', + 'packing.filterDone': 'Hecho', + 'packing.emptyTitle': 'La lista de equipaje está vacía', + 'packing.emptyHint': 'Añade elementos o usa las sugerencias', + 'packing.emptyFiltered': 'Ningún elemento coincide con este filtro', + 'packing.menuRename': 'Renombrar', + 'packing.menuCheckAll': 'Marcar todo', + 'packing.menuUncheckAll': 'Desmarcar todo', + 'packing.menuDeleteCat': 'Eliminar categoría', + 'packing.changeCategory': 'Cambiar categoría', + 'packing.confirm.clearChecked': '¿Seguro que quieres eliminar {count} elementos marcados?', + 'packing.confirm.deleteCat': '¿Seguro que quieres eliminar la categoría "{name}" con {count} elementos?', + 'packing.defaultCategory': 'Otros', + 'packing.toast.saveError': 'No se pudo guardar', + 'packing.toast.deleteError': 'No se pudo eliminar', + 'packing.toast.renameError': 'No se pudo renombrar', + 'packing.toast.addError': 'No se pudo añadir', + + // Packing suggestions + 'packing.suggestions.items': [ + { name: 'Pasaporte', category: 'Documentos' }, + { name: 'Documento de identidad', category: 'Documentos' }, + { name: 'Seguro de viaje', category: 'Documentos' }, + { name: 'Billetes de vuelo', category: 'Documentos' }, + { name: 'Tarjeta de crédito', category: 'Finanzas' }, + { name: 'Efectivo', category: 'Finanzas' }, + { name: 'Visado', category: 'Documentos' }, + { name: 'Camisetas', category: 'Ropa' }, + { name: 'Pantalones', category: 'Ropa' }, + { name: 'Ropa interior', category: 'Ropa' }, + { name: 'Calcetines', category: 'Ropa' }, + { name: 'Chaqueta', category: 'Ropa' }, + { name: 'Pijama', category: 'Ropa' }, + { name: 'Ropa de baño', category: 'Ropa' }, + { name: 'Impermeable', category: 'Ropa' }, + { name: 'Zapatos cómodos', category: 'Ropa' }, + { name: 'Cepillo de dientes', category: 'Aseo' }, + { name: 'Pasta de dientes', category: 'Aseo' }, + { name: 'Champú', category: 'Aseo' }, + { name: 'Desodorante', category: 'Aseo' }, + { name: 'Protector solar', category: 'Aseo' }, + { name: 'Maquinilla de afeitar', category: 'Aseo' }, + { name: 'Cargador', category: 'Electrónica' }, + { name: 'Batería externa', category: 'Electrónica' }, + { name: 'Auriculares', category: 'Electrónica' }, + { name: 'Adaptador de viaje', category: 'Electrónica' }, + { name: 'Cámara', category: 'Electrónica' }, + { name: 'Analgésicos', category: 'Salud' }, + { name: 'Tiritas', category: 'Salud' }, + { name: 'Desinfectante', category: 'Salud' }, + ], + + // Members / Sharing + 'members.shareTrip': 'Compartir viaje', + 'members.inviteUser': 'Invitar usuario', + 'members.selectUser': 'Seleccionar usuario…', + 'members.invite': 'Invitar', + 'members.allHaveAccess': 'Todos los usuarios ya tienen acceso.', + 'members.access': 'Acceso', + 'members.person': 'persona', + 'members.persons': 'personas', + 'members.you': 'tú', + 'members.owner': 'Propietario', + 'members.leaveTrip': 'Abandonar viaje', + 'members.removeAccess': 'Quitar acceso', + 'members.confirmLeave': '¿Abandonar el viaje? Perderás el acceso.', + 'members.confirmRemove': '¿Quitar el acceso de este usuario?', + 'members.loadError': 'No se pudieron cargar los miembros', + 'members.added': 'añadido', + 'members.addError': 'No se pudo añadir', + 'members.removed': 'Miembro eliminado', + 'members.removeError': 'No se pudo eliminar', + + // Categories (Admin) + 'categories.title': 'Categorías', + 'categories.subtitle': 'Gestiona categorías para lugares', + 'categories.new': 'Nueva categoría', + 'categories.empty': 'Aún no hay categorías', + 'categories.namePlaceholder': 'Nombre de la categoría', + 'categories.icon': 'Icono', + 'categories.color': 'Color', + 'categories.customColor': 'Elegir color personalizado', + 'categories.preview': 'Vista previa', + 'categories.defaultName': 'Categoría', + 'categories.update': 'Actualizar', + 'categories.create': 'Crear', + 'categories.confirm.delete': '¿Eliminar la categoría? Los lugares de esta categoría no se eliminarán.', + 'categories.toast.loadError': 'No se pudieron cargar las categorías', + 'categories.toast.nameRequired': 'Introduce un nombre', + 'categories.toast.updated': 'Categoría actualizada', + 'categories.toast.created': 'Categoría creada', + 'categories.toast.saveError': 'No se pudo guardar', + 'categories.toast.deleted': 'Categoría eliminada', + 'categories.toast.deleteError': 'No se pudo eliminar', + + // Backup (Admin) + 'backup.title': 'Copia de seguridad de datos', + 'backup.subtitle': 'Base de datos y todos los archivos subidos', + 'backup.refresh': 'Actualizar', + 'backup.upload': 'Subir copia de seguridad', + 'backup.uploading': 'Subiendo…', + 'backup.create': 'Crear copia', + 'backup.creating': 'Creando…', + 'backup.empty': 'Aún no hay copias', + 'backup.createFirst': 'Crear la primera copia', + 'backup.download': 'Descargar', + 'backup.restore': 'Restaurar', + 'backup.confirm.restore': '¿Restaurar la copia "{name}"?\n\nTodos los datos actuales serán reemplazados por la copia.', + 'backup.confirm.uploadRestore': '¿Subir y restaurar el archivo de copia "{name}"?\n\nTodos los datos actuales se sobrescribirán.', + 'backup.confirm.delete': '¿Eliminar la copia "{name}"?', + 'backup.toast.loadError': 'No se pudieron cargar las copias', + 'backup.toast.created': 'Copia de seguridad creada correctamente', + 'backup.toast.createError': 'No se pudo crear la copia', + 'backup.toast.restored': 'Copia restaurada. La página se recargará…', + 'backup.toast.restoreError': 'No se pudo restaurar', + 'backup.toast.uploadError': 'No se pudo subir', + 'backup.toast.deleted': 'Copia eliminada', + 'backup.toast.deleteError': 'No se pudo eliminar', + 'backup.toast.downloadError': 'La descarga falló', + 'backup.toast.settingsSaved': 'Ajustes de copia automática guardados', + 'backup.toast.settingsError': 'No se pudieron guardar los ajustes', + 'backup.auto.title': 'Copia automática', + 'backup.auto.subtitle': 'Copia de seguridad automática según una programación', + 'backup.auto.enable': 'Activar copia automática', + 'backup.auto.enableHint': 'Se crearán copias automáticamente según la frecuencia elegida', + 'backup.auto.interval': 'Intervalo', + 'backup.auto.keepLabel': 'Eliminar copias antiguas después de', + 'backup.interval.hourly': 'Cada hora', + 'backup.interval.daily': 'Diaria', + 'backup.interval.weekly': 'Semanal', + 'backup.interval.monthly': 'Mensual', + 'backup.keep.1day': '1 día', + 'backup.keep.3days': '3 días', + 'backup.keep.7days': '7 días', + 'backup.keep.14days': '14 días', + 'backup.keep.30days': '30 días', + 'backup.keep.forever': 'Conservar para siempre', + + // Photos + 'photos.allDays': 'Todos los días', + 'photos.title': 'Recuerdos', + 'photos.noPhotos': 'Aún no hay fotos', + 'photos.uploadHint': 'Sube y organiza las fotos compartidas de este viaje', + 'photos.clickToSelect': 'o haz clic para seleccionar', + 'photos.dropHere': 'Suelta aquí las fotos...', + 'photos.dropTitle': 'Suelta aquí las fotos', + 'photos.fileHint': 'JPG, PNG, GIF, WebP · máx. 10 MB · hasta 30 fotos', + 'photos.selectedCount': '{count} foto(s) seleccionada(s)', + 'photos.sharedAlbum': '{count} recuerdos en este álbum compartido', + 'photos.sharedAlbumFor': '{count} recuerdos en {trip}', + 'photos.allPlaces': 'Todos los lugares', + 'photos.view.grid': 'Cuadrícula', + 'photos.view.day': 'Por día', + 'photos.view.place': 'Por lugar', + 'photos.stats.total': 'Fotos', + 'photos.stats.days': 'Días', + 'photos.stats.places': 'Lugares', + 'photos.stats.latest': 'Última subida', + 'photos.sectionCount': '{count} foto(s)', + 'photos.ungrouped': 'Sin clasificar', + 'photos.featured': 'Recuerdo destacado', + 'photos.coverFallback': 'Portada del álbum compartido', + 'photos.coverHint': 'Una imagen destacada para este álbum de viaje', + 'photos.mapTitle': 'Mapa de recuerdos', + 'photos.mapHint': 'Explora los lugares vinculados en el mismo mapa que usamos en el plan', + 'photos.mapEmpty': 'Vincula tus fotos a lugares para verlas ubicadas en el mapa.', + 'photos.linkDay': 'Vincular día', + 'photos.noDay': 'Sin día', + 'photos.linkPlace': 'Vincular lugar', + 'photos.noPlace': 'Sin lugar', + 'photos.captionLabel': 'Pie de foto (para todas)', + 'photos.captionPlaceholder': 'Pie de foto opcional...', + 'photos.addCaption': 'Añadir un pie de foto...', + 'photos.uploadN': 'Subida de {n} foto(s)', + 'admin.addons.catalog.memories.name': 'Recuerdos', + 'admin.addons.catalog.memories.description': 'Álbumes de fotos compartidos para cada viaje', + 'admin.addons.catalog.packing.name': 'Equipaje', + 'admin.addons.catalog.packing.description': 'Prepara tu equipaje con listas de comprobación para cada viaje', + 'admin.addons.catalog.budget.name': 'Presupuesto', + 'admin.addons.catalog.budget.description': 'Controla los gastos y planifica el presupuesto del viaje', + 'admin.addons.catalog.documents.name': 'Documentos', + 'admin.addons.catalog.documents.description': 'Guarda y gestiona la documentación del viaje', + 'admin.addons.catalog.vacay.name': 'Vacaciones', + 'admin.addons.catalog.vacay.description': 'Planificador personal de vacaciones con vista de calendario', + 'admin.addons.catalog.atlas.name': 'Atlas', + 'admin.addons.catalog.atlas.description': 'Mapa del mundo con los países visitados y estadísticas de viaje', + 'admin.addons.catalog.collab.name': 'Colaboración', + 'admin.addons.catalog.collab.description': 'Notas, encuestas y chat en tiempo real para organizar el viaje', + + // Backup restore modal + 'backup.restoreConfirmTitle': '¿Restaurar copia?', + 'backup.restoreWarning': 'Todos los datos actuales (viajes, lugares, usuarios, subidas) serán reemplazados permanentemente por la copia. Esta acción no se puede deshacer.', + 'backup.restoreTip': 'Consejo: crea una copia del estado actual antes de restaurar.', + 'backup.restoreConfirm': 'Sí, restaurar', + + // PDF + 'pdf.travelPlan': 'Plan de viaje', + 'pdf.planned': 'Planificado', + 'pdf.costLabel': 'Coste EUR', + 'pdf.preview': 'Vista previa PDF', + 'pdf.saveAsPdf': 'Guardar como PDF', + + // Planner + 'planner.places': 'Lugares', + 'planner.bookings': 'Reservas', + 'planner.packingList': 'Lista de equipaje', + 'planner.documents': 'Documentos', + 'planner.dayPlan': 'Plan por días', + 'planner.reservations': 'Reservas', + 'planner.minTwoPlaces': 'Se necesitan al menos 2 lugares con coordenadas', + 'planner.noGeoPlaces': 'No hay lugares con coordenadas disponibles', + 'planner.routeCalculated': 'Ruta calculada', + 'planner.routeCalcFailed': 'No se pudo calcular la ruta', + 'planner.routeError': 'Error al calcular la ruta', + 'planner.routeOptimized': 'Ruta optimizada', + 'planner.reservationUpdated': 'Reserva actualizada', + 'planner.reservationAdded': 'Reserva añadida', + 'planner.confirmDeleteReservation': '¿Eliminar reserva?', + 'planner.reservationDeleted': 'Reserva eliminada', + 'planner.days': 'Días', + 'planner.allPlaces': 'Todos los lugares', + 'planner.totalPlaces': '{n} lugares en total', + 'planner.noDaysPlanned': 'Aún no hay días planificados', + 'planner.editTrip': 'Editar viaje →', + 'planner.placeOne': '1 lugar', + 'planner.placeN': '{n} lugares', + 'planner.addNote': 'Añadir nota', + 'planner.noEntries': 'No hay entradas para este día', + 'planner.addPlace': 'Añadir lugar/actividad', + 'planner.addPlaceShort': '+ Añadir lugar/actividad', + 'planner.resPending': 'Reserva pendiente · ', + 'planner.resConfirmed': 'Reserva confirmada · ', + 'planner.notePlaceholder': 'Nota…', + 'planner.noteTimePlaceholder': 'Hora (opcional)', + 'planner.noteExamplePlaceholder': 'p. ej. S3 a las 14:30 desde la estación central, ferry desde el muelle 7, pausa para comer…', + 'planner.totalCost': 'Coste total', + 'planner.searchPlaces': 'Buscar lugares…', + 'planner.allCategories': 'Todas las categorías', + 'planner.noPlacesFound': 'No se encontraron lugares', + 'planner.addFirstPlace': 'Añadir el primer lugar', + 'planner.noReservations': 'Sin reservas', + 'planner.addFirstReservation': 'Añadir la primera reserva', + 'planner.new': 'Nuevo', + 'planner.addToDay': '+ Día', + 'planner.calculating': 'Calculando…', + 'planner.route': 'Ruta', + 'planner.optimize': 'Optimizar', + 'planner.openGoogleMaps': 'Abrir en Google Maps', + 'planner.selectDayHint': 'Selecciona un día de la lista izquierda para ver su plan', + 'planner.noPlacesForDay': 'Aún no hay lugares para este día', + 'planner.addPlacesLink': 'Añadir lugares →', + 'planner.minTotal': 'min en total', + 'planner.noReservation': 'Sin reserva', + 'planner.removeFromDay': 'Quitar del día', + 'planner.addToThisDay': 'Añadir al día', + 'planner.overview': 'Vista general', + 'planner.noDays': 'No hay días todavía', + 'planner.editTripToAddDays': 'Edita el viaje para añadir días', + 'planner.dayCount': '{n} días', + 'planner.clickToUnlock': 'Haz clic para desbloquear', + 'planner.keepPosition': 'Mantener posición durante la optimización de ruta', + 'planner.dayDetails': 'Detalles del día', + 'planner.dayN': 'Día {n}', + 'planner.notes': 'Notas', + 'planner.addDayNote': 'Añadir notas para este día...', + + // Dashboard Stats + 'stats.countries': 'Países', + 'stats.cities': 'Ciudades', + 'stats.trips': 'Viajes', + 'stats.places': 'Lugares', + 'stats.worldProgress': 'Progreso mundial', + 'stats.visited': 'visitados', + 'stats.remaining': 'restantes', + 'stats.visitedCountries': 'Países visitados', + + // Day Detail Panel + 'day.precipProb': 'Probabilidad de lluvia', + 'day.precipitation': 'Precipitación', + 'day.wind': 'Viento', + 'day.sunrise': 'Amanecer', + 'day.sunset': 'Atardecer', + 'day.hourlyForecast': 'Pronóstico por horas', + 'day.climateHint': 'Promedios históricos: el pronóstico real está disponible dentro de los 16 días previos a la fecha.', + 'day.noWeather': 'No hay datos meteorológicos disponibles. Añade un lugar con coordenadas.', + 'day.overview': 'Resumen diario', + 'day.accommodation': 'Alojamiento', + 'day.addAccommodation': 'Añadir alojamiento', + 'day.hotelDayRange': 'Aplicar a los días', + 'day.noPlacesForHotel': 'Añade primero lugares al viaje', + 'day.allDays': 'Todos', + 'day.checkIn': 'Check-in', + 'day.checkOut': 'Check-out', + 'day.confirmation': 'Confirmación', + 'day.editAccommodation': 'Editar alojamiento', + 'day.reservations': 'Reservas', + + // Collab Addon + 'collab.tabs.chat': 'Mensajes', + 'collab.tabs.notes': 'Notas', + 'collab.tabs.polls': 'Encuestas', + 'collab.whatsNext.title': 'Qué viene ahora', + 'collab.whatsNext.today': 'Hoy', + 'collab.whatsNext.tomorrow': 'Mañana', + 'collab.whatsNext.empty': 'No hay actividades próximas', + 'collab.whatsNext.until': 'hasta', + 'collab.whatsNext.emptyHint': 'Las actividades con hora aparecerán aquí', + 'collab.chat.send': 'Enviar', + 'collab.chat.placeholder': 'Escribe un mensaje...', + 'collab.chat.empty': 'Empieza la conversación', + 'collab.chat.emptyHint': 'Los mensajes se comparten con todos los miembros del viaje', + 'collab.chat.emptyDesc': 'Comparte ideas, planes y novedades con tu grupo de viaje', + 'collab.chat.today': 'Hoy', + 'collab.chat.yesterday': 'Ayer', + 'collab.chat.deletedMessage': 'eliminó un mensaje', + 'collab.chat.loadMore': 'Cargar mensajes anteriores', + 'collab.chat.justNow': 'justo ahora', + 'collab.chat.minutesAgo': 'hace {n} min', + 'collab.chat.hoursAgo': 'hace {n} h', + 'collab.notes.title': 'Notas', + 'collab.notes.new': 'Nueva nota', + 'collab.notes.empty': 'Aún no hay notas', + 'collab.notes.emptyHint': 'Empieza a capturar ideas y planes', + 'collab.notes.all': 'Todas', + 'collab.notes.titlePlaceholder': 'Título de la nota', + 'collab.notes.contentPlaceholder': 'Escribe algo...', + 'collab.notes.categoryPlaceholder': 'Categoría', + 'collab.notes.newCategory': 'Nueva categoría...', + 'collab.notes.category': 'Categoría', + 'collab.notes.noCategory': 'Sin categoría', + 'collab.notes.color': 'Color', + 'collab.notes.save': 'Guardar', + 'collab.notes.cancel': 'Cancelar', + 'collab.notes.edit': 'Editar', + 'collab.notes.delete': 'Eliminar', + 'collab.notes.pin': 'Fijar', + 'collab.notes.unpin': 'Desfijar', + 'collab.notes.daysAgo': 'hace {n} d', + 'collab.notes.categorySettings': 'Gestionar categorías', + 'collab.notes.create': 'Crear', + 'collab.notes.website': 'Sitio web', + 'collab.notes.websitePlaceholder': 'https://...', + 'collab.notes.attachFiles': 'Adjuntar archivos', + 'collab.notes.noCategoriesYet': 'Aún no hay categorías', + 'collab.notes.emptyDesc': 'Crea una nota para empezar', + 'collab.polls.title': 'Encuestas', + 'collab.polls.new': 'Nueva encuesta', + 'collab.polls.empty': 'Aún no hay encuestas', + 'collab.polls.emptyHint': 'Pregunta al grupo y votad juntos', + 'collab.polls.question': 'Pregunta', + 'collab.polls.questionPlaceholder': '¿Qué deberíamos hacer?', + 'collab.polls.addOption': '+ Añadir opción', + 'collab.polls.optionPlaceholder': 'Opción {n}', + 'collab.polls.create': 'Crear encuesta', + 'collab.polls.close': 'Cerrar', + 'collab.polls.closed': 'Cerrada', + 'collab.polls.votes': '{n} votos', + 'collab.polls.vote': '{n} voto', + 'collab.polls.multipleChoice': 'Selección múltiple', + 'collab.polls.multiChoice': 'Selección múltiple', + 'collab.polls.deadline': 'Fecha límite', + 'collab.polls.option': 'Opción', + 'collab.polls.options': 'Opciones', + 'collab.polls.delete': 'Eliminar', + 'collab.polls.closedSection': 'Cerradas', + + // Files management (2.6.2) + 'files.trash': 'Papelera', + 'files.trashEmpty': 'La papelera está vacía', + 'files.emptyTrash': 'Vaciar papelera', + 'files.restore': 'Restaurar', + 'files.star': 'Destacar', + 'files.unstar': 'Quitar destacado', + 'files.assign': 'Asignar', + 'files.assignTitle': 'Asignar archivo', + 'files.assignPlace': 'Lugar', + 'files.assignBooking': 'Reserva', + 'files.unassigned': 'Sin asignar', + 'files.unlink': 'Eliminar vínculo', + 'files.noteLabel': 'Nota', + 'files.notePlaceholder': 'Añadir una nota...', + 'files.toast.trashed': 'Movido a la papelera', + 'files.toast.restored': 'Archivo restaurado', + 'files.toast.trashEmptied': 'Papelera vaciada', + 'files.toast.assigned': 'Archivo asignado', + 'files.toast.assignError': 'Error al asignar', + 'files.toast.restoreError': 'Error al restaurar', + 'files.confirm.permanentDelete': 'Eliminar este archivo permanentemente? No se puede deshacer.', + 'files.confirm.emptyTrash': 'Eliminar todos los archivos de la papelera? No se puede deshacer.', + + // Reservation metadata (2.6.2) + 'reservations.meta.airline': 'Aerolínea', + 'reservations.meta.flightNumber': 'N° de vuelo', + 'reservations.meta.from': 'Desde', + 'reservations.meta.to': 'Hasta', + 'reservations.meta.trainNumber': 'N° de tren', + 'reservations.meta.platform': 'Andén', + 'reservations.meta.seat': 'Asiento', + 'reservations.meta.checkIn': 'Check-in', + 'reservations.meta.checkOut': 'Check-out', + 'reservations.meta.linkAccommodation': 'Alojamiento', + 'reservations.meta.pickAccommodation': 'Vincular con alojamiento', + 'reservations.meta.noAccommodation': 'Ninguno', + 'reservations.meta.hotelPlace': 'Hotel', + 'reservations.meta.pickHotel': 'Seleccionar hotel', + 'reservations.meta.fromDay': 'Desde', + 'reservations.meta.toDay': 'Hasta', + 'reservations.meta.selectDay': 'Seleccionar día', + + // OIDC-only mode (2.6.2) + 'admin.oidcOnlyMode': 'Desactivar autenticación por contraseña', + 'admin.oidcOnlyModeHint': 'Si está activado, solo se permite el inicio de sesión con SSO. El inicio de sesión y registro con contraseña se bloquean.', + 'login.oidcOnly': 'La autenticación por contraseña está desactivada. Por favor, inicia sesión con tu proveedor SSO.', + + // Settings (2.6.2) + 'settings.currentPasswordRequired': 'La contraseña actual es obligatoria', + 'settings.passwordWeak': 'La contraseña debe contener mayúsculas, minúsculas y números', +} + +export default es From 31124a604a01666aa998371f8bf83734df2310d8 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 28 Mar 2026 23:11:47 +0100 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20auto-split=20pasted=20lat,lng=20coo?= =?UTF-8?q?rdinates=20in=20place=20form=20=E2=80=94=20closes=20#22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/Planner/PlaceFormModal.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/src/components/Planner/PlaceFormModal.tsx b/client/src/components/Planner/PlaceFormModal.tsx index 6f48961..ef4ec5f 100644 --- a/client/src/components/Planner/PlaceFormModal.tsx +++ b/client/src/components/Planner/PlaceFormModal.tsx @@ -281,6 +281,15 @@ export default function PlaceFormModal({ step="any" value={form.lat} onChange={e => handleChange('lat', e.target.value)} + onPaste={e => { + const text = e.clipboardData.getData('text').trim() + const match = text.match(/^(-?\d+\.?\d*)\s*[,;\s]\s*(-?\d+\.?\d*)$/) + if (match) { + e.preventDefault() + handleChange('lat', match[1]) + handleChange('lng', match[2]) + } + }} placeholder={t('places.formLat')} className="form-input" /> From 83d256ebacba5df012d007f94153d752360f5623 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 28 Mar 2026 23:23:52 +0100 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20custom=20timezones=20in=20timezone?= =?UTF-8?q?=20widget=20=E2=80=94=20closes=20#21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Dashboard/TimezoneWidget.tsx | 41 ++++++++++++++++++- client/src/i18n/translations/de.ts | 7 ++++ client/src/i18n/translations/en.ts | 7 ++++ client/src/i18n/translations/es.ts | 7 ++++ 4 files changed, 61 insertions(+), 1 deletion(-) diff --git a/client/src/components/Dashboard/TimezoneWidget.tsx b/client/src/components/Dashboard/TimezoneWidget.tsx index 5ee97e5..087937a 100644 --- a/client/src/components/Dashboard/TimezoneWidget.tsx +++ b/client/src/components/Dashboard/TimezoneWidget.tsx @@ -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]) @@ -108,7 +125,29 @@ export default function TimezoneWidget() { {/* Add zone dropdown */} {showAdd && ( -
+
+ {/* Custom timezone */} +
+

{t('dashboard.timezoneCustomTitle')}

+
+ setCustomLabel(e.target.value)} + placeholder={t('dashboard.timezoneCustomLabelPlaceholder')} + className="w-full px-2 py-1.5 rounded-lg text-xs outline-none" + style={{ background: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border-secondary)' }} /> + { setCustomTz(e.target.value); setCustomError('') }} + placeholder={t('dashboard.timezoneCustomTzPlaceholder')} + className="w-full px-2 py-1.5 rounded-lg text-xs outline-none" + style={{ background: 'var(--bg-secondary)', color: 'var(--text-primary)', border: `1px solid ${customError ? '#ef4444' : 'var(--border-secondary)'}` }} + onKeyDown={e => { if (e.key === 'Enter') addCustomZone() }} /> + {customError &&

{customError}

} + +
+
+ {/* Popular zones */} {POPULAR_ZONES.filter(z => !zones.find(existing => existing.tz === z.tz)).map(z => (