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:
@@ -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 && (
|
||||
<div className="absolute top-3 left-3 bg-amber-600 text-white text-[9px] font-black px-2 py-1.5 rounded-xl flex items-center gap-1.5 border border-amber-400 shadow-xl animate-in slide-in-from-left-4 duration-500">
|
||||
<PlusCircle size={12} strokeWidth={3} />
|
||||
ZU SESSION HINZUFÜGEN
|
||||
{t('grid.addSession')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{bottle.last_tasted && (
|
||||
<div className="absolute top-3 right-3 bg-zinc-900/80 backdrop-blur-md text-white text-[9px] font-black px-2 py-1 rounded-lg flex items-center gap-1 border border-white/10 ring-1 ring-black/5">
|
||||
<Clock size={10} />
|
||||
{new Date(bottle.last_tasted).toLocaleDateString('de-DE')}
|
||||
{new Date(bottle.last_tasted).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -80,13 +82,13 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) {
|
||||
{(bottle.is_whisky === false || (bottle.confidence && bottle.confidence < 70)) && (
|
||||
<div className="flex items-center gap-1 text-[8px] font-black bg-red-500 text-white px-1.5 py-0.5 rounded-full animate-pulse">
|
||||
<AlertCircle size={8} />
|
||||
REVIEW
|
||||
{t('grid.reviewRequired')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h3 className={`font-black text-lg md:text-xl leading-tight group-hover:text-amber-600 transition-colors line-clamp-2 min-h-[3rem] md:min-h-[3.5rem] flex items-center ${bottle.is_whisky === false ? 'text-red-600 dark:text-red-400' : 'text-zinc-900 dark:text-zinc-100'
|
||||
}`}>
|
||||
{bottle.name || 'Unbekannte Flasche'}
|
||||
{bottle.name || t('grid.unknownBottle')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -101,8 +103,10 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) {
|
||||
|
||||
<div className="pt-1 md:pt-2 flex items-center gap-2 text-[9px] md:text-[10px] font-bold text-zinc-400 uppercase tracking-wider border-t border-zinc-100 dark:border-zinc-800">
|
||||
<Calendar size={10} className="text-zinc-300" />
|
||||
<span className="opacity-70 text-[8px] md:text-[9px]">Hinzugefügt am</span>
|
||||
<span className="text-zinc-500 dark:text-zinc-300">{new Date(bottle.created_at).toLocaleDateString('de-DE')}</span>
|
||||
<span className="opacity-70 text-[8px] md:text-[9px]">{t('grid.addedOn')}</span>
|
||||
<span className="text-zinc-500 dark:text-zinc-300">
|
||||
{new Date(bottle.created_at).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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<string | null>(null);
|
||||
@@ -186,7 +191,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
||||
if (!bottles || bottles.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12 p-8 bg-zinc-50 dark:bg-zinc-900/50 rounded-3xl border-2 border-dashed border-zinc-200 dark:border-zinc-800">
|
||||
<p className="text-zinc-500">Noch keine Flaschen im Vault. Zeit für den ersten Scan! 🥃</p>
|
||||
<p className="text-zinc-500">{t('home.noBottles')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -200,7 +205,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Suchen nach Name oder Distille..."
|
||||
placeholder={t('grid.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => 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"
|
||||
>
|
||||
<option value="created_at">Neueste zuerst</option>
|
||||
<option value="last_tasted">Zuletzt verkostet</option>
|
||||
<option value="name">Alphabetisch</option>
|
||||
<option value="created_at">{t('grid.sortBy.createdAt')}</option>
|
||||
<option value="last_tasted">{t('grid.sortBy.lastTasted')}</option>
|
||||
<option value="name">{t('grid.sortBy.name')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Category Filter */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-zinc-400 px-1">Kategorie</span>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-zinc-400 px-1">{t('grid.filter.category')}</span>
|
||||
<div className="flex gap-2 overflow-x-auto -mx-4 px-4 pb-2 scrollbar-hide touch-pan-x">
|
||||
<button
|
||||
onClick={() => setSelectedCategory(null)}
|
||||
@@ -238,7 +243,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
||||
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400'
|
||||
}`}
|
||||
>
|
||||
ALLE
|
||||
{t('common.all').toUpperCase()}
|
||||
</button>
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
@@ -257,7 +262,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
||||
|
||||
{/* Distillery Filter */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-zinc-400 px-1">Distillery</span>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-zinc-400 px-1">{t('grid.filter.distillery')}</span>
|
||||
<div className="flex gap-2 overflow-x-auto -mx-4 px-4 pb-2 scrollbar-hide touch-pan-x">
|
||||
<button
|
||||
onClick={() => setSelectedDistillery(null)}
|
||||
@@ -266,7 +271,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
||||
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400'
|
||||
}`}
|
||||
>
|
||||
ALLE
|
||||
{t('common.all').toUpperCase()}
|
||||
</button>
|
||||
{distilleries.map((dist) => (
|
||||
<button
|
||||
@@ -285,7 +290,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
||||
|
||||
{/* Status Filter */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-zinc-400 px-1">Status</span>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-zinc-400 px-1">{t('grid.filter.status')}</span>
|
||||
<div className="flex gap-2 overflow-x-auto -mx-4 px-4 pb-2 scrollbar-hide touch-pan-x">
|
||||
{['sealed', 'open', 'sampled', 'empty'].map((status) => (
|
||||
<button
|
||||
@@ -296,7 +301,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
||||
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400'
|
||||
}`}
|
||||
>
|
||||
{status.toUpperCase()}
|
||||
{t(`bottle.status.${status}`).toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -313,7 +318,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-zinc-500 italic">Keine Flaschen gefunden, die deinen Filtern entsprechen. 🔎</p>
|
||||
<p className="text-zinc-500 italic">{t('grid.noResults')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user