diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 13e9206..b5d0799 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -59,6 +59,7 @@ interface SearchResult { } const GlobalSearch = () => { + const { t } = useTranslation(); const [query, setQuery] = useState(''); const [options, setOptions] = useState([]); const navigate = useNavigate(); @@ -120,7 +121,7 @@ const GlobalSearch = () => { { {following.length > 0 && ( <> - Following + {t('nav.following')} {following.map(f => ( setDrawerOpen(false)}> @@ -419,10 +420,10 @@ const App: React.FC = () => { elevation={3} sx={{ mt: 1 }} > - setAnchorEl(null)}>Dashboard - setAnchorEl(null)}>Settings + setAnchorEl(null)}>{t('nav.dashboard')} + setAnchorEl(null)}>{t('nav.settings')} - Logout + {t('nav.logout')} ) : ( diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index f5f0d39..fff8407 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -12,7 +12,50 @@ const resources = { "dashboard": "Dashboard", "settings": "Settings", "logout": "Logout", - "login": "Login / Signup" + "login": "Login / Signup", + "following": "Following" + }, + "dashboard": { + "welcome": "Welcome back, {{username}}!", + "shareProfile": "Share Profile:", + "copySuccess": "Link copied to clipboard!", + "myRatings": "My Ratings", + "missing": "Missing", + "noRatings": "You haven't rated any flavors yet.", + "exploreFlavors": "Go explore flavors", + "allRated": "Amazing! You've rated everything! 🏆" + }, + "settings": { + "subtitle": "Manage your account and preferences", + "appearanceTitle": "Appearance & Language", + "themeLabel": "Theme", + "langLabel": "Language", + "avatarTitle": "Profile Picture", + "avatarButton": "Upload New Avatar", + "avatarHint": "Max size: 2MB. JPG, PNG or WEBP.", + "avatarSuccess": "Avatar updated successfully!", + "infoTitle": "Profile Information", + "usernameLabel": "Username", + "emailLabel": "Email", + "emailHint": "Change will require confirmation", + "updateButton": "Update Profile", + "updateSuccess": "Profile updated successfully!", + "confirmEmailTitle": "Confirm Email Change", + "confirmEmailHint": "Enter the 6-digit code sent to {{email}}", + "confirmButton": "Verify & Update Email", + "passwordTitle": "Change Password", + "oldPasswordLabel": "Old Password", + "newPasswordLabel": "New Password", + "passwordButton": "Change Password", + "passwordSuccess": "Password changed successfully!", + "dangerTitle": "Danger Zone", + "dangerDesc": "Permanently delete your account and all associated data. This action is irreversible.", + "deleteButton": "Delete My Account", + "deletionCodeLabel": "Deletion Code", + "deletionCodeHint": "Enter verification code:", + "confirmDeleteButton": "Confirm Permanent Deletion", + "cancelButton": "Cancel", + "codeSent": "Verification code sent to your email." }, "hero": { "title": "The Ultimate Holy Archive", @@ -43,6 +86,21 @@ const resources = { "reviews": "reviews", "rank": "Rank", "loading": "Loading..." + }, + "about": { + "title": "About This Project", + "mission": "Holy Flavors Archive is a community-driven project dedicated to documenting and rating the vast world of Holy Energy products. Our goal is to create a definitive, high-density catalog of every flavor ever released—including legacy and limited editions.", + "canDoTitle": "What you can do here:", + "featureExplore": "Explore: Find details on any Holy Energy, Iced Tea, or Milkshake product.", + "featureRate": "Rate: Share your honest opinion and score flavors from 1 to 10.", + "featureCommunity": "Community: Follow other fans, see their latest tests, and discover new favorites together.", + "featureProfile": "Taste Profile: Build your own personal tiered leaderboard to share with the world.", + "disclaimer": "This website is a fan project and is not officially affiliated with, authorized, maintained, sponsored, or endorsed by HOLY Energy GmbH. All product names, logos, and brands are property of their respective owners.", + "impressumTitle": "Impressum (Legal Notice)", + "legalInfo": "Information according to § 5 TMG:", + "contact": "Contact", + "responsible": "Responsible for content according to § 55 Abs. 2 RStV:", + "disclaimerTitle": "Legal Disclaimer" } } }, @@ -55,7 +113,50 @@ const resources = { "dashboard": "Dashboard", "settings": "Einstellungen", "logout": "Abmelden", - "login": "Anmelden / Registrieren" + "login": "Anmelden / Registrieren", + "following": "Folge ich" + }, + "dashboard": { + "welcome": "Willkommen zurück, {{username}}!", + "shareProfile": "Profil teilen:", + "copySuccess": "Link in die Zwischenablage kopiert!", + "myRatings": "Meine Bewertungen", + "missing": "Fehlend", + "noRatings": "Du hast noch keine Sorten bewertet.", + "exploreFlavors": "Sorten entdecken", + "allRated": "Wahnsinn! Du hast alles bewertet! 🏆" + }, + "settings": { + "subtitle": "Verwalte dein Konto und deine Einstellungen", + "appearanceTitle": "Erscheinungsbild & Sprache", + "themeLabel": "Design", + "langLabel": "Sprache", + "avatarTitle": "Profilbild", + "avatarButton": "Neues Bild hochladen", + "avatarHint": "Max. Größe: 2MB. JPG, PNG oder WEBP.", + "avatarSuccess": "Profilbild erfolgreich aktualisiert!", + "infoTitle": "Profil-Informationen", + "usernameLabel": "Benutzername", + "emailLabel": "E-Mail", + "emailHint": "Änderung erfordert Bestätigung", + "updateButton": "Profil aktualisieren", + "updateSuccess": "Profil erfolgreich aktualisiert!", + "confirmEmailTitle": "E-Mail-Änderung bestätigen", + "confirmEmailHint": "Gib den 6-stelligen Code ein, der an {{email}} gesendet wurde", + "confirmButton": "E-Mail verifizieren & aktualisieren", + "passwordTitle": "Passwort ändern", + "oldPasswordLabel": "Altes Passwort", + "newPasswordLabel": "Neues Passwort", + "passwordButton": "Passwort ändern", + "passwordSuccess": "Passwort erfolgreich geändert!", + "dangerTitle": "Gefahrenzone", + "dangerDesc": "Lösche dein Konto und alle damit verbundenen Daten dauerhaft. Dies kann nicht rückgängig gemacht werden.", + "deleteButton": "Mein Konto löschen", + "deletionCodeLabel": "Löschcode", + "deletionCodeHint": "Bestätigungscode eingeben:", + "confirmDeleteButton": "Dauerhafte Löschung bestätigen", + "cancelButton": "Abbrechen", + "codeSent": "Bestätigungscode an deine E-Mail gesendet." }, "hero": { "title": "Das ultimative Holy-Archiv", @@ -86,6 +187,21 @@ const resources = { "reviews": "Bewertungen", "rank": "Rang", "loading": "Wird geladen..." + }, + "about": { + "title": "Über dieses Projekt", + "mission": "Das Holy Flavors Archiv ist ein von der Community getriebenes Projekt, das sich der Dokumentation und Bewertung der riesigen Welt der Holy Energy Produkte widmet. Unser Ziel ist es, einen definitiven Katalog jeder jemals veröffentlichten Sorte zu erstellen – einschließlich Legacy- und Limited-Editionen.", + "canDoTitle": "Was du hier tun kannst:", + "featureExplore": "Entdecken: Details zu jedem Holy Energy, Eistee oder Milkshake Produkt finden.", + "featureRate": "Bewerten: Deine ehrliche Meinung teilen und Sorten von 1 bis 10 bewerten.", + "featureCommunity": "Community: Folge anderen Fans, sieh dir ihre neuesten Tests an und entdeckt gemeinsam neue Favoriten.", + "featureProfile": "Geschmacksprofil: Erstelle deine eigene persönliche Bestenliste, um sie mit der Welt zu teilen.", + "disclaimer": "Diese Website ist ein Fan-Projekt und wird nicht von der HOLY Energy GmbH offiziell unterstützt, autorisiert, gepflegt oder gesponselt. Alle Produktnamen, Logos und Marken sind Eigentum ihrer jeweiligen Inhaber.", + "impressumTitle": "Impressum", + "legalInfo": "Angaben gemäß § 5 TMG:", + "contact": "Kontakt", + "responsible": "Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV:", + "disclaimerTitle": "Haftungsausschluss" } } } diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx index 52bb38c..f5babb7 100644 --- a/frontend/src/pages/About.tsx +++ b/frontend/src/pages/About.tsx @@ -2,10 +2,12 @@ import React from 'react'; import { Typography, Box, Container, Paper, Divider, Button } from '@mui/material'; import { Link } from 'react-router-dom'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { useTranslation } from 'react-i18next'; import { useTitle } from '../hooks/useTitle'; const About: React.FC = () => { - useTitle('About & Impressum'); + const { t } = useTranslation(); + useTitle(t('nav.about')); return ( @@ -16,37 +18,34 @@ const About: React.FC = () => { startIcon={} sx={{ mb: 4, textTransform: 'none', borderRadius: 2 }} > - Back to Archive + {t('common.backToArchive')} - About This Project + {t('about.title')} - Holy Flavors Archive is a community-driven project dedicated to documenting and rating - the vast world of Holy Energy products. Our goal is to create a definitive, high-density catalog of every - flavor ever released—including legacy and limited editions. + {t('about.mission')} - What you can do here: + {t('about.canDoTitle')} -
  • Explore: Find details on any Holy Energy, Iced Tea, or Milkshake product.
  • -
  • Rate: Share your honest opinion and score flavors from 1 to 10.
  • -
  • Community: Follow other fans, see their latest tests, and discover new favorites together.
  • -
  • Taste Profile: Build your own personal tiered leaderboard to share with the world.
  • +
  • {t('about.featureExplore')}
  • +
  • {t('about.featureRate')}
  • +
  • {t('about.featureCommunity')}
  • +
  • {t('about.featureProfile')}
  • - This website is a fan project and is not officially affiliated with, authorized, maintained, sponsored, - or endorsed by HOLY Energy GmbH. All product names, logos, and brands are property of their respective owners. + {t('about.disclaimer')} - Impressum + {t('about.impressumTitle')} - Angaben gemäß § 5 TMG:
    + {t('about.legalInfo')}
    {import.meta.env.VITE_IMPRESSUM_NAME || '[Your Name or Project Name]'}
    {import.meta.env.VITE_IMPRESSUM_STREET || '[Your Street Address]'}
    {import.meta.env.VITE_IMPRESSUM_CITY || '[Postal Code and City]'}
    @@ -54,7 +53,7 @@ const About: React.FC = () => {
    - Kontakt: + {t('about.contact')}: E-Mail: {import.meta.env.VITE_IMPRESSUM_EMAIL || '[Your Support Email Address]'}
    {import.meta.env.VITE_IMPRESSUM_PHONE && ( @@ -65,7 +64,7 @@ const About: React.FC = () => {
    - Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV: + {t('about.responsible')} {import.meta.env.VITE_IMPRESSUM_NAME || '[Your Name]'}
    {import.meta.env.VITE_IMPRESSUM_STREET || '[Your Street Address]'}
    @@ -74,7 +73,7 @@ const About: React.FC = () => {
    - Haftungsausschluss (Disclaimer) + {t('about.disclaimerTitle')} Haftung für Inhalte: Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 4d149bb..c804a1a 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -17,6 +17,7 @@ import { import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import api from '../api'; import { useTitle } from '../hooks/useTitle'; @@ -29,7 +30,8 @@ interface DashboardData { } const Dashboard: React.FC = () => { - useTitle('Dashboard'); + const { t } = useTranslation(); + useTitle(t('nav.dashboard')); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState(0); @@ -38,7 +40,7 @@ const Dashboard: React.FC = () => { const copyToClipboard = () => { navigator.clipboard.writeText(shareUrl); - alert('Link copied to clipboard!'); + alert(t('dashboard.copySuccess')); }; useEffect(() => { @@ -78,7 +80,7 @@ const Dashboard: React.FC = () => { startIcon={} sx={{ mb: 4, textTransform: 'none', borderRadius: 2 }} > - Back to Home + {t('common.backToHome')} @@ -87,12 +89,12 @@ const Dashboard: React.FC = () => { {!data.user.avatar && data.user.username.charAt(0).toUpperCase()} - Dashboard - Welcome back, {data.user.username}! + {t('nav.dashboard')} + {t('dashboard.welcome', { username: data.user.username })} - Share Profile: + {t('dashboard.shareProfile')} { setActiveTab(v)} textColor="primary" indicatorColor="primary"> - - + + @@ -117,8 +119,8 @@ const Dashboard: React.FC = () => { {Object.keys(ratedGrouped).length === 0 ? ( - You haven't rated any flavors yet. - + {t('dashboard.noRatings')} + ) : ( Object.entries(ratedGrouped).map(([category, items]: [string, any]) => ( @@ -189,7 +191,7 @@ const Dashboard: React.FC = () => { {Object.keys(missingGrouped).length === 0 ? ( - Amazing! You've rated everything! 🏆 + {t('dashboard.allRated')} ) : ( Object.entries(missingGrouped).map(([category, flavors]: [string, any]) => ( diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index a1ff2eb..f331ab5 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -13,7 +13,8 @@ import { Select, MenuItem, ListSubheader, - Divider + Divider, + Grid } from '@mui/material'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { Link } from 'react-router-dom'; @@ -78,7 +79,7 @@ const Settings: React.FC = ({ themeName, onThemeChange }) => { headers: { 'Content-Type': 'multipart/form-data' } }); setAvatar(res.data.avatar); - setMessage({ type: 'success', text: 'Avatar updated successfully!' }); + setMessage({ type: 'success', text: t('settings.avatarSuccess') }); } catch (err: any) { setMessage({ type: 'error', text: err.response?.data?.error || 'Failed to upload avatar' }); } @@ -100,9 +101,9 @@ const Settings: React.FC = ({ themeName, onThemeChange }) => { const res = await api.patch('users/update_profile/', { username, email }); if (res.data.message) { setPendingConfirmation(true); - setMessage({ type: 'success', text: `Profile updated.${res.data.message}` }); + setMessage({ type: 'success', text: `${t('settings.updateSuccess')} ${res.data.message}` }); } else { - setMessage({ type: 'success', text: 'Profile updated successfully!' }); + setMessage({ type: 'success', text: t('settings.updateSuccess') }); setCurrentEmail(email); } } catch (err: any) { @@ -114,7 +115,7 @@ const Settings: React.FC = ({ themeName, onThemeChange }) => { e.preventDefault(); try { const res = await api.post('users/confirm_email/', { code: confirmationCode }); - setMessage({ type: 'success', text: 'Email confirmed and updated!' }); + setMessage({ type: 'success', text: t('settings.updateSuccess') }); setCurrentEmail(res.data.email); setPendingConfirmation(false); setConfirmationCode(''); @@ -127,7 +128,7 @@ const Settings: React.FC = ({ themeName, onThemeChange }) => { e.preventDefault(); try { await api.post('users/change_password/', { old_password: oldPassword, new_password: newPassword }); - setMessage({ type: 'success', text: 'Password changed successfully!' }); + setMessage({ type: 'success', text: t('settings.passwordSuccess') }); setOldPassword(''); setNewPassword(''); } catch (err: any) { @@ -139,7 +140,7 @@ const Settings: React.FC = ({ themeName, onThemeChange }) => { try { await api.post('users/request_account_deletion/'); setIsDeleting(true); - setMessage({ type: 'success', text: 'Verification code sent to your email.' }); + setMessage({ type: 'success', text: t('settings.codeSent') }); } catch (err: any) { setMessage({ type: 'error', text: 'Failed to request deletion.' }); } @@ -158,7 +159,7 @@ const Settings: React.FC = ({ themeName, onThemeChange }) => { }; return ( - + - + {!avatar && username.charAt(0).toUpperCase()} {username} - Manage your account and preferences + {t('settings.subtitle')} {message && ( - setMessage(null)}> + setMessage(null)}> {message.text} )} - - Appearance & Language - - - Theme - - + + {/* Left Column: Visuals & Language */} + + + {t('settings.appearanceTitle')} + + {t('settings.themeLabel')} + + - - Language - - - - + + {t('settings.langLabel')} + + +
    - - Profile Picture - - - {!avatar && username.charAt(0).toUpperCase()} - - - - - Max size: 2MB. JPG, PNG or WEBP. - - - - - - - Profile Information -
    - setUsername(e.target.value)} - /> - setEmail(e.target.value)} - helperText={email !== currentEmail ? "Change will require confirmation" : ""} - /> - - - - {pendingConfirmation && ( - - Confirm Email Change - Enter the 6-digit code sent to {email} -
    - setConfirmationCode(e.target.value)} - /> - - -
    - )} -
    - - - Change Password -
    - setOldPassword(e.target.value)} - /> - setNewPassword(e.target.value)} - /> - - -
    - - - Danger Zone - - Permanently delete your account and all associated data. This action is irreversible. - - - {!isDeleting ? ( - - ) : ( -
    - Enter verification code: - setDeletionCode(e.target.value)} - sx={{ mb: 2 }} - /> - - - + + {t('settings.avatarTitle')} + + + {!avatar && username.charAt(0).toUpperCase()} + + + + + {t('settings.avatarHint')} + + - - )} - +
    + + + {/* Right Column: Account Info & Security */} + + + {t('settings.infoTitle')} +
    + setUsername(e.target.value)} + InputLabelProps={{ shrink: true }} + /> + setEmail(e.target.value)} + helperText={email !== currentEmail ? t('settings.emailHint') : ""} + InputLabelProps={{ shrink: true }} + /> + + + + {pendingConfirmation && ( + + {t('settings.confirmEmailTitle')} + {t('settings.confirmEmailHint', { email })} +
    + setConfirmationCode(e.target.value)} + InputLabelProps={{ shrink: true }} + /> + + +
    + )} +
    + + + {t('settings.passwordTitle')} +
    + setOldPassword(e.target.value)} + InputLabelProps={{ shrink: true }} + /> + setNewPassword(e.target.value)} + InputLabelProps={{ shrink: true }} + /> + + +
    + + theme.palette.mode === 'dark' ? 'rgba(255,0,0,0.05)' : 'rgba(255,0,0,0.02)', '& .MuiInputLabel-root': { bgcolor: 'background.paper', px: 0.5 } }}> + {t('settings.dangerTitle')} + + {t('settings.dangerDesc')} + + + {!isDeleting ? ( + + ) : ( +
    + {t('settings.deletionCodeHint')} + setDeletionCode(e.target.value)} + sx={{ mb: 2 }} + InputLabelProps={{ shrink: true }} + /> + + + + + + )} +
    +
    +
    ); }; diff --git a/frontend/src/pages/flavors/FlavorDetail.tsx b/frontend/src/pages/flavors/FlavorDetail.tsx index 1f05d7d..914a188 100644 --- a/frontend/src/pages/flavors/FlavorDetail.tsx +++ b/frontend/src/pages/flavors/FlavorDetail.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useParams, Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { Typography, Box, @@ -51,6 +52,7 @@ interface Flavor { } const FlavorDetail: React.FC = () => { + const { t } = useTranslation(); const { id } = useParams<{ id: string }>(); const [flavor, setFlavor] = useState(null); const [loading, setLoading] = useState(true); @@ -88,7 +90,7 @@ const FlavorDetail: React.FC = () => { fetchFlavor(); }, [id]); - useTitle(flavor?.name || 'Flavor Detail'); + useTitle(flavor?.name || t('common.loading')); const handleRatingSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -156,7 +158,7 @@ const FlavorDetail: React.FC = () => { startIcon={} sx={{ mb: 2, textTransform: 'none', borderRadius: 2 }} > - Back to {flavor.category_name} + {t('common.backTo')} {flavor.category_name} @@ -212,7 +214,7 @@ const FlavorDetail: React.FC = () => { {flavor.shop_url && ( )} @@ -248,11 +250,11 @@ const FlavorDetail: React.FC = () => { )} - Community Reviews + {t('home.communityVoice')} {flavor.ratings.length === 0 ? ( - No reviews yet. Be the first to rate it! + {t('dashboard.noRatings')} ) : ( flavor.ratings.map((rating: Rating) => (