diff --git a/src/app/layout.tsx b/src/app/layout.tsx index eacc6cb..a616043 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import PWARegistration from "@/components/PWARegistration"; import OfflineIndicator from "@/components/OfflineIndicator"; import UploadQueue from "@/components/UploadQueue"; +import { I18nProvider } from "@/i18n/I18nContext"; const inter = Inter({ subsets: ["latin"] }); @@ -40,10 +41,12 @@ export default function RootLayout({ return ( - - - - {children} + + + + + {children} + ); diff --git a/src/app/page.tsx b/src/app/page.tsx index ea96301..e4f7583 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -9,6 +9,8 @@ import BuddyList from "@/components/BuddyList"; import SessionList from "@/components/SessionList"; import StatsDashboard from "@/components/StatsDashboard"; import DramOfTheDay from "@/components/DramOfTheDay"; +import LanguageSwitcher from "@/components/LanguageSwitcher"; +import { useI18n } from "@/i18n/I18nContext"; export default function Home() { const supabase = createClientComponentClient(); @@ -16,6 +18,7 @@ export default function Home() { const [isLoading, setIsLoading] = useState(true); const [user, setUser] = useState(null); const [fetchError, setFetchError] = useState(null); + const { t } = useI18n(); useEffect(() => { // Check session @@ -111,7 +114,12 @@ export default function Home() {

WHISKYVAULT

-

Scanne deine Flaschen, tracke deine Tastings und verwalte deinen Keller.

+

+ {t('home.searchPlaceholder').replace('...', '')} +

+
+ +
@@ -126,12 +134,13 @@ export default function Home() { WHISKYVAULT
+
@@ -152,7 +161,7 @@ export default function Home() {

- Deine Sammlung + {t('home.collection')} {bottles.length} @@ -164,13 +173,13 @@ export default function Home() {

) : fetchError ? (
-

Die Sammlung konnte nicht geladen werden

-

Ein technischer Fehler ist aufgetreten.

+

{t('common.error')}

+

{fetchError}

) : ( diff --git a/src/components/BottleGrid.tsx b/src/components/BottleGrid.tsx index 0d1f35d..b7561f1 100644 --- a/src/components/BottleGrid.tsx +++ b/src/components/BottleGrid.tsx @@ -6,6 +6,7 @@ import { Search, Filter, X, Calendar, Clock, Package, Lock, Unlock, Ghost, Flask import { getStorageUrl } from '@/lib/supabase'; import { useSearchParams } from 'next/navigation'; import { validateSession } from '@/services/validate-session'; +import { useI18n } from '@/i18n/I18nContext'; interface Bottle { id: string; @@ -29,11 +30,12 @@ interface BottleCardProps { } function BottleCard({ bottle, sessionId }: BottleCardProps) { + const { t, locale } = useI18n(); const statusConfig = { - open: { icon: Unlock, color: 'bg-amber-500/80 border-amber-400/50', label: 'Offen' }, - sampled: { icon: FlaskConical, color: 'bg-purple-500/80 border-purple-400/50', label: 'Sample' }, - empty: { icon: Ghost, color: 'bg-zinc-500/80 border-zinc-400/50', label: 'Leer' }, - sealed: { icon: Lock, color: 'bg-blue-600/80 border-blue-400/50', label: 'Versiegelt' }, + open: { icon: Unlock, color: 'bg-amber-500/80 border-amber-400/50', label: t('bottle.status.open') }, + sampled: { icon: FlaskConical, color: 'bg-purple-500/80 border-purple-400/50', label: t('bottle.status.sampled') }, + empty: { icon: Ghost, color: 'bg-zinc-500/80 border-zinc-400/50', label: t('bottle.status.empty') }, + sealed: { icon: Lock, color: 'bg-blue-600/80 border-blue-400/50', label: t('bottle.status.sealed') }, }; const StatusIcon = statusConfig[bottle.status as keyof typeof statusConfig]?.icon || Lock; @@ -56,14 +58,14 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) { {sessionId && (
- ZU SESSION HINZUFÜGEN + {t('grid.addSession')}
)} {bottle.last_tasted && (
- {new Date(bottle.last_tasted).toLocaleDateString('de-DE')} + {new Date(bottle.last_tasted).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US')}
)} @@ -80,13 +82,13 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) { {(bottle.is_whisky === false || (bottle.confidence && bottle.confidence < 70)) && (
- REVIEW + {t('grid.reviewRequired')}
)}

- {bottle.name || 'Unbekannte Flasche'} + {bottle.name || t('grid.unknownBottle')}

@@ -101,8 +103,10 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) {
- Hinzugefügt am - {new Date(bottle.created_at).toLocaleDateString('de-DE')} + {t('grid.addedOn')} + + {new Date(bottle.created_at).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US')} +
@@ -115,6 +119,7 @@ interface BottleGridProps { } export default function BottleGrid({ bottles }: BottleGridProps) { + const { t } = useI18n(); const searchParams = useSearchParams(); const sessionId = searchParams.get('session_id'); const [validatedSessionId, setValidatedSessionId] = useState(null); @@ -186,7 +191,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) { if (!bottles || bottles.length === 0) { return (
-

Noch keine Flaschen im Vault. Zeit für den ersten Scan! 🥃

+

{t('home.noBottles')}

); } @@ -200,7 +205,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) { setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-3 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl focus:ring-2 focus:ring-amber-500 outline-none transition-all" @@ -220,16 +225,16 @@ export default function BottleGrid({ bottles }: BottleGridProps) { onChange={(e) => setSortBy(e.target.value as any)} className="px-4 py-3 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl text-sm font-medium focus:ring-2 focus:ring-amber-500 outline-none cursor-pointer" > - - - + + +
{/* Category Filter */}
- Kategorie + {t('grid.filter.category')}
{categories.map((cat) => ( {distilleries.map((dist) => ( ))}
@@ -313,7 +318,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
) : (
-

Keine Flaschen gefunden, die deinen Filtern entsprechen. 🔎

+

{t('grid.noResults')}

)}
diff --git a/src/components/BuddyList.tsx b/src/components/BuddyList.tsx index 175eaf3..8614adf 100644 --- a/src/components/BuddyList.tsx +++ b/src/components/BuddyList.tsx @@ -3,6 +3,7 @@ import React, { useState, useEffect } from 'react'; import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; import { Users, UserPlus, Trash2, User, Loader2 } from 'lucide-react'; +import { useI18n } from '@/i18n/I18nContext'; interface Buddy { id: string; @@ -11,6 +12,7 @@ interface Buddy { } export default function BuddyList() { + const { t } = useI18n(); const supabase = createClientComponentClient(); const [buddies, setBuddies] = useState([]); const [newName, setNewName] = useState(''); @@ -77,7 +79,7 @@ export default function BuddyList() {

- Deine Buddies + {t('buddy.title')}

@@ -85,7 +87,7 @@ export default function BuddyList() { type="text" value={newName} onChange={(e) => setNewName(e.target.value)} - placeholder="Buddy Name..." + placeholder={t('buddy.placeholder')} className="flex-1 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" /> @@ -282,21 +284,21 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS className="w-full py-3 px-6 bg-amber-50 dark:bg-amber-900/20 text-amber-600 rounded-xl font-bold flex items-center justify-center gap-2 border border-amber-200 dark:border-amber-800/50 hover:bg-amber-100 transition-all text-sm" > - Whiskybase-Link suchen + {t('camera.whiskybaseSearch')} )} {isDiscovering && (
- Suche auf Whiskybase... + {t('camera.searchingWb')}
)} {wbDiscovery && (
- Treffer gefunden + {t('camera.wbMatchFound')}

{wbDiscovery.title} @@ -306,7 +308,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS onClick={handleLinkWb} className="flex-1 py-2.5 bg-amber-600 text-white text-[10px] font-black uppercase rounded-lg hover:bg-amber-700 transition-colors" > - Verknüpfen + {t('common.link')} - Prüfen + {t('common.check')}

@@ -328,7 +330,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS }} className="w-full py-3 px-6 text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200 font-bold transition-colors" > - Später (Zurück zur Liste) + {t('camera.later')} ) : matchingBottle ? ( @@ -337,7 +339,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS className="w-full py-4 px-6 bg-amber-600 hover:bg-amber-700 text-white rounded-xl font-semibold flex items-center justify-center gap-2 transition-all active:scale-[0.98] shadow-lg shadow-amber-600/20" > - Zum Whisky im Vault + {t('camera.toVault')} ) : ( @@ -384,7 +386,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS {isQueued && (
- Offline! Foto wurde gemerkt – wird automatisch analysiert, sobald du wieder Netz hast. 📡 + {t('camera.offlineNotice')}
)} @@ -392,16 +394,16 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
- Bereits im Vault! + {t('camera.alreadyInVault')}

- Du hast diesen Whisky bereits in deiner Sammlung. Willst du direkt zur Flasche gehen? + {t('camera.alreadyInVaultDesc')}

)} @@ -410,30 +412,30 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
- Bild erfolgreich analysiert + {t('camera.analysisSuccess')}
{analysisResult && (
- Ergebnisse + {t('camera.results')}
- Name: + {t('bottle.nameLabel')}: {analysisResult.name || '-'}
- Distille: + {t('bottle.distilleryLabel')}: {analysisResult.distillery || '-'}
- Kategorie: + {t('bottle.categoryLabel')}: {analysisResult.category || '-'}
- ABV: + {t('bottle.abvLabel')}: {analysisResult.abv ? `${analysisResult.abv}%` : '-'}
diff --git a/src/components/DramOfTheDay.tsx b/src/components/DramOfTheDay.tsx index 0341b83..dfbd3ba 100644 --- a/src/components/DramOfTheDay.tsx +++ b/src/components/DramOfTheDay.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { Sparkles, GlassWater, Dices, X } from 'lucide-react'; +import { useI18n } from '@/i18n/I18nContext'; import Link from 'next/link'; interface Bottle { @@ -16,6 +17,7 @@ interface DramOfTheDayProps { } export default function DramOfTheDay({ bottles }: DramOfTheDayProps) { + const { t } = useI18n(); const [suggestion, setSuggestion] = useState(null); const [isRolling, setIsRolling] = useState(false); @@ -24,7 +26,7 @@ export default function DramOfTheDay({ bottles }: DramOfTheDayProps) { const openBottles = bottles.filter(b => b.status === 'open' || b.status === 'sampled'); if (openBottles.length === 0) { - alert('Keine offenen Flaschen gefunden! Vielleicht Zeit für ein neues Tasting? 🥃'); + alert(t('home.dramOfDay.noOpenBottles')); setIsRolling(false); return; } @@ -49,7 +51,7 @@ export default function DramOfTheDay({ bottles }: DramOfTheDayProps) { ) : ( )} - Dram of the Day + {t('home.dramOfDay.button')} {suggestion && ( @@ -68,7 +70,7 @@ export default function DramOfTheDay({ bottles }: DramOfTheDayProps) {
-

Dein heutiger Dram

+

{t('home.dramOfDay.title')}

{suggestion.name}

@@ -83,13 +85,13 @@ export default function DramOfTheDay({ bottles }: DramOfTheDayProps) { onClick={() => setSuggestion(null)} className="block w-full py-4 bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 rounded-2xl font-black uppercase tracking-widest text-xs hover:bg-amber-600 dark:hover:bg-amber-600 hover:text-white transition-all shadow-xl" > - Flasche anschauen + {t('home.dramOfDay.viewBottle')}
diff --git a/src/components/EditBottleForm.tsx b/src/components/EditBottleForm.tsx index 7ee3ca9..f1758da 100644 --- a/src/components/EditBottleForm.tsx +++ b/src/components/EditBottleForm.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; import { Edit2, Save, X, Info, Tag, FlaskConical, CircleDollarSign, Search, Loader2, ExternalLink } from 'lucide-react'; import { updateBottle } from '@/services/update-bottle'; import { discoverWhiskybaseId } from '@/services/discover-whiskybase'; +import { useI18n } from '@/i18n/I18nContext'; interface EditBottleFormProps { bottle: { @@ -23,6 +24,7 @@ interface EditBottleFormProps { } export default function EditBottleForm({ bottle, onComplete }: EditBottleFormProps) { + const { t, locale } = useI18n(); const [isEditing, setIsEditing] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isSearching, setIsSearching] = useState(false); @@ -60,7 +62,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro if (result.success && result.id) { setDiscoveryResult({ id: result.id!, url: result.url!, title: result.title! }); } else { - setError(result.error || 'Keinen Treffer gefunden.'); + setError(result.error || t('bottle.noMatchFound')); } setIsSearching(false); }; @@ -91,10 +93,10 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro setIsEditing(false); if (onComplete) onComplete(); } else { - setError(response.error || 'Fehler beim Speichern'); + setError(response.error || t('common.error')); } } catch (err) { - setError('Etwas ist schiefgelaufen.'); + setError(t('common.error')); } finally { setIsSaving(false); } @@ -108,12 +110,12 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro className="flex items-center gap-2 px-4 py-2 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 text-zinc-600 dark:text-zinc-400 rounded-xl text-sm font-bold transition-all w-fit" > - Details bearbeiten + {t('bottle.editDetails')} {bottle.purchase_price && (
- Kaufpreis: {parseFloat(bottle.purchase_price.toString()).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} + {t('bottle.priceLabel')}: {parseFloat(bottle.purchase_price.toString()).toLocaleString(locale === 'de' ? 'de-DE' : 'en-US', { style: 'currency', currency: 'EUR' })}
)} @@ -124,7 +126,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro

- Details korrigieren + {t('bottle.editTitle')}

- ID Übernehmen + {t('bottle.applyId')} - Prüfen + {t('common.check')}
)}
- +
- +
- +
- + {isSaving ?
: } - Änderungen speichern + {t('bottle.saveChanges')}
); diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..0c0943b --- /dev/null +++ b/src/components/LanguageSwitcher.tsx @@ -0,0 +1,35 @@ +'use client'; + +import React from 'react'; +import { useI18n } from '@/i18n/I18nContext'; + +const LanguageSwitcher = () => { + const { locale, setLocale } = useI18n(); + + return ( +
+ + +
+ ); +}; + +export default LanguageSwitcher; diff --git a/src/components/SessionList.tsx b/src/components/SessionList.tsx index 76702aa..e608569 100644 --- a/src/components/SessionList.tsx +++ b/src/components/SessionList.tsx @@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react'; import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; import { Calendar, Plus, GlassWater, Loader2, ChevronRight, Users } from 'lucide-react'; import Link from 'next/link'; +import { useI18n } from '@/i18n/I18nContext'; interface Session { id: string; @@ -13,6 +14,7 @@ interface Session { } export default function SessionList() { + const { t, locale } = useI18n(); const supabase = createClientComponentClient(); const [sessions, setSessions] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -70,7 +72,7 @@ export default function SessionList() {

- Tasting Sessions + {t('session.title')}

@@ -78,7 +80,7 @@ export default function SessionList() { type="text" value={newName} onChange={(e) => setNewName(e.target.value)} - placeholder="Event Name (z.B. Islay Night)..." + placeholder={t('session.sessionName')} className="flex-1 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50" />