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
This commit is contained in:
Joaquin
2026-03-28 22:56:17 +01:00
committed by GitHub
parent 1f9ae8e4b5
commit 706548c45d
22 changed files with 1185 additions and 55 deletions
+3 -4
View File
@@ -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 = `
<div style="display:flex;flex-direction:column;gap:8px;min-width:160px">
<div style="font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.1em;padding-bottom:6px;border-bottom:1px solid ${dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)'}">${name}</div>
@@ -515,4 +515,3 @@ function SidebarContent({ data, stats, countries, selectedCountry, countryDetail
</div>
)
}
+2 -2
View File
@@ -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' })
}
+8 -3
View File
@@ -270,9 +270,14 @@ export default function LoginPage(): React.ReactElement {
return (
<div style={{ minHeight: '100vh', display: 'flex', fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif", position: 'relative' }}>
{/* Sprach-Toggle oben rechts */}
{/* Language toggle */}
<button
onClick={() => setLanguageLocal(language === 'en' ? 'de' : 'en')}
onClick={() => {
const languages = ['en', 'es', 'de']
const currentIndex = languages.indexOf(language)
const nextLanguage = languages[(currentIndex + 1) % languages.length]
setLanguageLocal(nextLanguage)
}}
style={{
position: 'absolute', top: 16, right: 16, zIndex: 10,
display: 'flex', alignItems: 'center', gap: 6,
@@ -286,7 +291,7 @@ export default function LoginPage(): React.ReactElement {
onMouseLeave={(e: React.MouseEvent<HTMLButtonElement>) => e.currentTarget.style.background = 'rgba(0,0,0,0.06)'}
>
<Globe size={14} />
{language === 'en' ? 'EN' : 'DE'}
{language.toUpperCase()}
</button>
{/* Left — branding */}
+1
View File
@@ -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 => (
<button
key={opt.value}
+1 -1
View File
@@ -67,7 +67,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
...(enabledAddons.packing ? [{ id: 'packliste', label: t('trip.tabs.packing'), shortLabel: t('trip.tabs.packingShort') }] : []),
...(enabledAddons.budget ? [{ id: 'finanzplan', label: t('trip.tabs.budget') }] : []),
...(enabledAddons.documents ? [{ id: 'dateien', label: t('trip.tabs.files') }] : []),
...(enabledAddons.collab ? [{ id: 'collab', label: 'Collab' }] : []),
...(enabledAddons.collab ? [{ id: 'collab', label: t('admin.addons.catalog.collab.name') }] : []),
]
const [activeTab, setActiveTab] = useState<string>(() => {
+1 -1
View File
@@ -128,7 +128,7 @@ export default function VacayPage(): React.ReactElement {
<CalendarDays size={18} style={{ color: 'var(--text-primary)' }} />
</div>
<div>
<h1 className="text-lg sm:text-xl font-bold" style={{ color: 'var(--text-primary)' }}>Vacay</h1>
<h1 className="text-lg sm:text-xl font-bold" style={{ color: 'var(--text-primary)' }}>{t('admin.addons.catalog.vacay.name')}</h1>
<p className="text-xs hidden sm:block" style={{ color: 'var(--text-muted)' }}>{t('vacay.subtitle')}</p>
</div>
</div>