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 "./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 (
<html lang="de">
<body className={inter.className}>
<PWARegistration />
<OfflineIndicator />
<UploadQueue />
{children}
<I18nProvider>
<PWARegistration />
<OfflineIndicator />
<UploadQueue />
{children}
</I18nProvider>
</body>
</html>
);

View File

@@ -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<any>(null);
const [fetchError, setFetchError] = useState<string | null>(null);
const { t } = useI18n();
useEffect(() => {
// Check session
@@ -111,7 +114,12 @@ export default function Home() {
<h1 className="text-5xl font-black text-zinc-900 dark:text-white tracking-tighter mb-4">
WHISKY<span className="text-amber-600">VAULT</span>
</h1>
<p className="text-zinc-500 max-w-sm mx-auto">Scanne deine Flaschen, tracke deine Tastings und verwalte deinen Keller.</p>
<p className="text-zinc-500 max-w-sm mx-auto">
{t('home.searchPlaceholder').replace('...', '')}
</p>
<div className="mt-8">
<LanguageSwitcher />
</div>
</div>
<AuthForm />
</main>
@@ -126,12 +134,13 @@ export default function Home() {
WHISKY<span className="text-amber-600">VAULT</span>
</h1>
<div className="flex items-center gap-4">
<LanguageSwitcher />
<DramOfTheDay bottles={bottles} />
<button
onClick={handleLogout}
className="text-sm font-medium text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors"
>
Abmelden
{t('home.logout')}
</button>
</div>
</header>
@@ -152,7 +161,7 @@ export default function Home() {
<div className="w-full mt-12">
<h2 className="text-2xl font-bold mb-6 text-zinc-800 dark:text-zinc-100 flex items-center gap-3">
Deine Sammlung
{t('home.collection')}
<span className="text-sm font-normal text-zinc-500 bg-zinc-100 dark:bg-zinc-800 px-3 py-1 rounded-full">
{bottles.length}
</span>
@@ -164,13 +173,13 @@ export default function Home() {
</div>
) : fetchError ? (
<div className="p-8 bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-800 rounded-3xl text-center">
<p className="text-zinc-800 dark:text-zinc-200 font-bold mb-2">Die Sammlung konnte nicht geladen werden</p>
<p className="text-zinc-500 text-sm italic mb-4">Ein technischer Fehler ist aufgetreten.</p>
<p className="text-zinc-800 dark:text-zinc-200 font-bold mb-2">{t('common.error')}</p>
<p className="text-zinc-500 text-sm italic mb-4">{fetchError}</p>
<button
onClick={fetchCollection}
className="px-6 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-xl text-xs font-bold uppercase tracking-widest transition-all"
>
Erneut versuchen
{t('home.reTry')}
</button>
</div>
) : (