Übersetzung Teil 1

This commit is contained in:
Ashikagi
2026-03-23 23:02:51 +01:00
parent cd77f88d96
commit 46d468575a
21 changed files with 1605 additions and 333 deletions

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { Check, ChevronDown } from 'lucide-react';
import { countryOptions, getCountryLabel, getCountryOption } from '../data/countries';
import CountryFlag from './CountryFlag';
import { useTranslation } from '../i18n/LanguageContext';
const emptyStatus = {
message: '',
@@ -17,6 +18,7 @@ export default function ProfileTab({
onDelete,
onSessionChange
}) {
const t = useTranslation();
const [formValues, setFormValues] = useState(profile);
const [status, setStatus] = useState(emptyStatus);
const [saving, setSaving] = useState(false);
@@ -87,14 +89,14 @@ export default function ProfileTab({
setStatus({
message: result.emailUpdateRequested
? 'Profil gespeichert. Die neue Mailadresse muss ggf. noch per E-Mail bestätigt werden.'
: 'Profil gespeichert.',
? t('profile.saved_email')
: t('profile.saved'),
error: ''
});
} else {
setStatus({
message: '',
error: result?.error?.message || 'Profil konnte nicht gespeichert werden.'
error: result?.error?.message || t('profile.error_save')
});
}
@@ -102,9 +104,7 @@ export default function ProfileTab({
};
const handleDelete = async () => {
const confirmed = window.confirm(
'Profil wirklich löschen?\n\nDabei werden alle Tests dieses Benutzers sowie vorhandene Analysepläne dauerhaft entfernt.'
);
const confirmed = window.confirm(t('profile.delete_confirm'));
if (!confirmed) return;
@@ -115,13 +115,13 @@ export default function ProfileTab({
if (result?.ok) {
setStatus({
message: 'Profil und alle Tests wurden gelöscht. Das Konto bleibt bestehen und kann neu eingerichtet werden.',
message: t('profile.deleted'),
error: ''
});
} else {
setStatus({
message: '',
error: result?.error?.message || 'Profil konnte nicht gelöscht werden.'
error: result?.error?.message || t('profile.error_delete')
});
}
@@ -133,44 +133,44 @@ export default function ProfileTab({
<div className="max-w-4xl mx-auto">
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 overflow-hidden">
<div className="px-6 py-5 border-b border-gray-200 dark:border-gray-800">
<p className="text-xs uppercase tracking-[0.25em] text-sky-700 dark:text-sky-300">Benutzerprofil</p>
<h1 className="text-3xl font-bold mt-2">Profil verwalten</h1>
<p className="text-xs uppercase tracking-[0.25em] text-sky-700 dark:text-sky-300">{t('profile.title_label')}</p>
<h1 className="text-3xl font-bold mt-2">{t('profile.title')}</h1>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-300">
Name, Vorname, Mailadresse, Land und die Sichtbarkeit deiner Testfreigaben werden hier gepflegt.
{t('profile.description')}
</p>
</div>
{loading ? (
<div className="px-6 py-10 text-sm text-gray-600 dark:text-gray-300">
Profil wird geladen...
{t('profile.loading')}
</div>
) : (
<form onSubmit={handleSubmit} className="px-6 py-6 space-y-6">
<div className="grid md:grid-cols-2 gap-5">
<label className="block">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">Vorname</span>
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">{t('profile.first_name')}</span>
<input
type="text"
value={formValues.firstName || ''}
onChange={updateField('firstName')}
className="mt-1 w-full px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-sky-500"
placeholder="Max"
placeholder={t('profile.first_name_placeholder')}
/>
</label>
<label className="block">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">Nachname</span>
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">{t('profile.last_name')}</span>
<input
type="text"
value={formValues.lastName || ''}
onChange={updateField('lastName')}
className="mt-1 w-full px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-sky-500"
placeholder="Mustermann"
placeholder={t('profile.last_name_placeholder')}
/>
</label>
<label className="block md:col-span-2">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">Mailadresse</span>
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">{t('profile.email')}</span>
<input
type="email"
value={formValues.email || session?.user?.email || ''}
@@ -180,12 +180,12 @@ export default function ProfileTab({
placeholder="you@example.com"
/>
<span className="mt-2 block text-xs text-gray-500 dark:text-gray-400">
Wenn du die Mailadresse änderst, wird dir eine Bestätigung an die neue Adresse gesendet.
{t('profile.email_hint')}
</span>
</label>
<label className="block">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">Land</span>
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">{t('profile.country')}</span>
<div ref={countryDropdownRef} className="relative mt-1">
<button
type="button"
@@ -195,11 +195,11 @@ export default function ProfileTab({
<span className="inline-flex items-center gap-3">
<CountryFlag
countryCode={selectedCountry?.code || formValues.country}
title={selectedCountry?.name || getCountryLabel(formValues.country) || 'Land auswählen'}
title={selectedCountry?.name || getCountryLabel(formValues.country) || t('profile.country_none')}
className="h-5 w-7 rounded-[3px] shadow-sm ring-1 ring-black/10"
/>
<span className="font-medium">
{selectedCountry?.name || getCountryLabel(formValues.country) || 'Land auswählen'}
{selectedCountry?.name || getCountryLabel(formValues.country) || t('profile.country_none')}
</span>
</span>
<ChevronDown
@@ -222,10 +222,10 @@ export default function ProfileTab({
<span className="inline-flex items-center gap-3">
<CountryFlag
countryCode=""
title="Kein Land ausgewählt"
title={t('profile.country_none')}
className="h-5 w-7 rounded-[3px] shadow-sm"
/>
<span>Kein Land ausgewählt</span>
<span>{t('profile.country_none')}</span>
</span>
{!formValues.country && <Check className="h-4 w-4" aria-hidden="true" />}
</button>
@@ -278,35 +278,52 @@ export default function ProfileTab({
)}
</div>
<span className="mt-2 block text-xs text-gray-500 dark:text-gray-400">
Die ausgewählte Flagge wird später auch im Kopfbereich neben deiner Mailadresse angezeigt.
{t('profile.country_hint')}
</span>
</label>
<div className="block">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">Ranglistenplatz</span>
<div className="mt-1 rounded-lg border border-dashed border-amber-300 bg-amber-50 px-4 py-3 dark:border-amber-900/70 dark:bg-amber-950/30">
<div className="flex items-center justify-between gap-3">
<span className="text-sm font-medium text-amber-800 dark:text-amber-200">
Platzhalter
</span>
<span className="rounded-full bg-white px-3 py-1 text-sm font-semibold text-amber-700 shadow-sm dark:bg-gray-900 dark:text-amber-300">
# -
</span>
</div>
<p className="mt-2 text-xs text-amber-700/80 dark:text-amber-200/80">
Hier kann spater der Ranglistenplatz des Benutzers angezeigt werden.
</p>
<label className="block">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">{t('profile.display')}</span>
<select
value={formValues.language || 'de'}
onChange={updateField('language')}
className="mt-1 w-full px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-sky-500"
>
<option value="de">Deutsch</option>
<option value="en">Englisch</option>
<option value="it">Italienisch</option>
<option value="es">Spanisch</option>
<option value="pt">Portugiesisch</option>
</select>
<span className="mt-2 block text-xs text-gray-500 dark:text-gray-400">
{t('profile.display_hint')}
</span>
</label>
</div>
<div className="block">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">{t('profile.rank')}</span>
<div className="mt-1 rounded-lg border border-dashed border-amber-300 bg-amber-50 px-4 py-3 dark:border-amber-900/70 dark:bg-amber-950/30">
<div className="flex items-center justify-between gap-3">
<span className="text-sm font-medium text-amber-800 dark:text-amber-200">
{t('profile.rank_placeholder')}
</span>
<span className="rounded-full bg-white px-3 py-1 text-sm font-semibold text-amber-700 shadow-sm dark:bg-gray-900 dark:text-amber-300">
# -
</span>
</div>
<p className="mt-2 text-xs text-amber-700/80 dark:text-amber-200/80">
{t('profile.rank_hint')}
</p>
</div>
</div>
<div className="rounded-xl border border-sky-200 dark:border-sky-900/70 bg-sky-50 dark:bg-sky-950/30 p-5">
<div className="flex items-start justify-between gap-4 flex-wrap">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Tests sichtbar</h2>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">{t('profile.tests_visible_title')}</h2>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-300 max-w-2xl">
Ist dieser Schalter aktiv, funktionieren deine Freigabelinks. Wenn du ihn deaktivierst,
bleiben vorhandene Links gespeichert, sind aber nicht mehr öffentlich sichtbar.
{t('profile.tests_visible_desc')}
</p>
</div>
@@ -318,7 +335,7 @@ export default function ProfileTab({
className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500"
/>
<span className="text-sm font-semibold text-gray-800 dark:text-gray-100">
{formValues.testsVisible ? 'Sichtbar' : 'Nicht sichtbar'}
{formValues.testsVisible ? t('profile.visible') : t('profile.not_visible')}
</span>
</label>
</div>
@@ -338,7 +355,7 @@ export default function ProfileTab({
<div className="flex items-center justify-between gap-4 flex-wrap">
<div className="text-sm text-gray-500 dark:text-gray-400">
Konto-ID: <span className="font-mono">{session?.user?.id || '—'}</span>
{t('profile.account_id')}: <span className="font-mono">{session?.user?.id || '—'}</span>
</div>
<div className="flex items-center gap-3 flex-wrap">
@@ -348,7 +365,7 @@ export default function ProfileTab({
disabled={deleting || saving}
className="px-5 py-3 rounded-lg border border-rose-300 bg-white text-rose-700 font-semibold hover:bg-rose-50 transition disabled:opacity-60 disabled:cursor-not-allowed dark:border-rose-800 dark:bg-gray-900 dark:text-rose-300 dark:hover:bg-rose-950/40"
>
{deleting ? 'Löscht...' : 'Profil löschen'}
{deleting ? t('profile.deleting') : t('profile.delete')}
</button>
<button
@@ -356,7 +373,7 @@ export default function ProfileTab({
disabled={saving || deleting}
className="px-5 py-3 rounded-lg bg-sky-600 text-white font-semibold hover:bg-sky-700 transition disabled:opacity-60 disabled:cursor-not-allowed"
>
{saving ? 'Speichert...' : 'Profil speichern'}
{saving ? t('profile.saving') : t('profile.save')}
</button>
</div>
</div>