feat: implement comprehensive i18n system with German and English support

- Created type-safe i18n system with TranslationKeys interface
- Added German (de) and English (en) translations with 160+ keys
- Implemented I18nContext provider and useI18n hook
- Added LanguageSwitcher component for language selection
- Refactored all major components to use translations:
  * Home page, StatsDashboard, DramOfTheDay
  * BottleGrid, EditBottleForm, CameraCapture
  * BuddyList, SessionList, TastingNoteForm
  * StatusSwitcher and bottle management features
- Implemented locale-aware currency formatting (EUR)
- Implemented locale-aware date formatting
- Added localStorage persistence for language preference
- Added automatic browser language detection
- Organized translations into 8 main categories
- System is extensible for additional languages
This commit is contained in:
2025-12-18 13:44:48 +01:00
parent acf02a78dd
commit 334bece471
16 changed files with 741 additions and 120 deletions

View File

@@ -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"
>
<Edit2 size={16} />
Details bearbeiten
{t('bottle.editDetails')}
</button>
{bottle.purchase_price && (
<div className="flex items-center gap-2 px-4 py-2 bg-green-50 dark:bg-green-900/10 text-green-700 dark:text-green-400 rounded-xl text-sm font-bold border border-green-100 dark:border-green-900/30 w-fit">
<CircleDollarSign size={16} />
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' })}
</div>
)}
</div>
@@ -124,7 +126,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
<div className="p-6 bg-white dark:bg-zinc-900 border border-amber-500/30 rounded-3xl shadow-xl space-y-4 animate-in zoom-in-95 duration-200">
<div className="flex justify-between items-center mb-2">
<h3 className="text-lg font-black text-amber-600 uppercase tracking-widest flex items-center gap-2">
<Info size={18} /> Details korrigieren
<Info size={18} /> {t('bottle.editTitle')}
</h3>
<button
onClick={() => setIsEditing(false)}
@@ -136,7 +138,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">Name</label>
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">{t('bottle.nameLabel')}</label>
<input
type="text"
value={formData.name}
@@ -145,7 +147,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">Brennerei</label>
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">{t('bottle.distilleryLabel')}</label>
<input
type="text"
value={formData.distillery}
@@ -154,7 +156,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">Kategorie</label>
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">{t('bottle.categoryLabel')}</label>
<input
type="text"
value={formData.category}
@@ -164,7 +166,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">ABV%</label>
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">{t('bottle.abvLabel')}</label>
<input
type="number"
step="0.1"
@@ -174,7 +176,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">Alter</label>
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">{t('bottle.ageLabel')}</label>
<input
type="number"
value={formData.age}
@@ -193,7 +195,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
className="text-amber-600 hover:text-amber-700 flex items-center gap-1 normal-case font-bold"
>
{isSearching ? <Loader2 size={10} className="animate-spin" /> : <Search size={10} />}
Automatisch suchen
{t('bottle.autoSearch')}
</button>
</label>
<input
@@ -212,7 +214,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
onClick={applyDiscovery}
className="px-3 py-1.5 bg-amber-600 text-white text-[10px] font-black uppercase rounded-lg hover:bg-amber-700 transition-colors"
>
ID Übernehmen
{t('bottle.applyId')}
</button>
<a
href={discoveryResult.url}
@@ -220,14 +222,14 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
rel="noopener noreferrer"
className="px-3 py-1.5 bg-zinc-200 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300 text-[10px] font-black uppercase rounded-lg hover:bg-zinc-300 dark:hover:bg-zinc-600 transition-colors flex items-center gap-1"
>
<ExternalLink size={10} /> Prüfen
<ExternalLink size={10} /> {t('common.check')}
</a>
</div>
</div>
)}
</div>
<div className="space-y-1">
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">Kaufpreis ()</label>
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">{t('bottle.priceLabel')} ()</label>
<input
type="number"
step="0.01"
@@ -239,7 +241,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
</div>
<div className="space-y-1">
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">Destilliert</label>
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">{t('bottle.distilledLabel')}</label>
<input
type="text"
placeholder="z.B. 2010"
@@ -250,7 +252,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
</div>
<div className="space-y-1">
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">Abgefüllt</label>
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">{t('bottle.bottledLabel')}</label>
<input
type="text"
placeholder="z.B. 2022"
@@ -261,7 +263,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
</div>
<div className="space-y-1 md:col-span-2">
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">Batch / Code</label>
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">{t('bottle.batchLabel')}</label>
<input
type="text"
placeholder="z.B. Batch 12 oder L-Code"
@@ -280,7 +282,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
className="w-full py-4 bg-amber-600 hover:bg-amber-700 text-white rounded-2xl font-black uppercase tracking-widest transition-all flex items-center justify-center gap-2 shadow-lg shadow-amber-600/20 disabled:opacity-50"
>
{isSaving ? <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div> : <Save size={20} />}
Änderungen speichern
{t('bottle.saveChanges')}
</button>
</div>
);