i18n implemented

This commit is contained in:
2026-10-03 23:45:04 +02:00
parent 2ccea34bb9
commit d95ed40053
55 changed files with 2952 additions and 301 deletions
+46 -29
View File
@@ -36,6 +36,7 @@ import {
const { user, refresh } = useAuth()
const { invalidate } = useUserSummaries()
const { t, locale, setLocale } = useI18n()
const color = ref(user.value?.color ?? '#6366f1')
const cardEffect = ref<string>(user.value?.cardEffect ?? 'none')
@@ -121,11 +122,11 @@ async function save() {
if (user.value) invalidate(user.value.id)
await refreshNuxtData()
avatar.value = null
toast.success('Einstellungen gespeichert')
toast.success(t('settings.saved'))
}
catch (err) {
const e = err as { data?: { statusMessage?: string } }
toast.error(e.data?.statusMessage ?? 'Speichern fehlgeschlagen')
toast.error(e.data?.statusMessage ?? t('settings.saveFailed'))
}
finally {
saving.value = false
@@ -137,34 +138,50 @@ async function save() {
<div class="mx-auto w-full max-w-2xl px-4 lg:px-6">
<Card v-if="user">
<CardHeader>
<CardTitle>Einstellungen</CardTitle>
<CardDescription>Name-Farbe und Avatar anpassen.</CardDescription>
<CardTitle>{{ $t('settings.title') }}</CardTitle>
<CardDescription>{{ $t('settings.desc') }}</CardDescription>
</CardHeader>
<CardContent>
<form class="space-y-4" @submit.prevent="save">
<div class="space-y-1.5">
<Label>Name-Farbe</Label>
<Label>{{ $t('settings.language') }}</Label>
<Select :model-value="locale" @update:model-value="(v) => setLocale(v as string)">
<SelectTrigger class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="de">
Deutsch
</SelectItem>
<SelectItem value="en">
English
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-1.5">
<Label>{{ $t('settings.nameColor') }}</Label>
<div class="flex items-center gap-3">
<ColorPicker :value="color" type="hex" @value-change="(val: any) => color = val.hex">
<ColorPicker :value="(color as `#${string}`)" type="hex" @value-change="(val: any) => color = val.hex">
<button
type="button"
class="h-9 w-14 cursor-pointer rounded-md border border-input shadow-sm"
:style="{ backgroundColor: color }"
aria-label="Farbe auswählen"
:aria-label="$t('settings.pickColor')"
/>
</ColorPicker>
<span class="text-sm font-medium" :style="{ color }">{{ user.name }}</span>
</div>
</div>
<div class="space-y-1.5">
<Label>Hover-Card-Effekt</Label>
<Label>{{ $t('settings.cardEffect') }}</Label>
<Select v-model="cardEffect">
<SelectTrigger class="w-full">
<SelectValue placeholder="Kein Effekt" />
<SelectValue :placeholder="$t('settings.noEffectPlaceholder')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">
Keiner
{{ $t('settings.noEffect') }}
</SelectItem>
<SelectItem v-for="e in CARD_EFFECTS" :key="e.key" :value="e.key">
{{ e.label }}
@@ -174,31 +191,31 @@ async function save() {
<div v-if="cardEffect !== 'none'" class="flex flex-wrap items-center gap-x-5 gap-y-2 pt-1">
<label class="flex cursor-pointer items-center gap-2 text-sm text-muted-foreground">
<Checkbox v-model="cardEffectColorEnabled" />
Eigene Effekt-Farbe
{{ $t('settings.ownEffectColor') }}
</label>
<ColorPicker v-if="cardEffectColorEnabled" :value="cardEffectColor" type="hex" @value-change="(val: any) => cardEffectColor = val.hex">
<ColorPicker v-if="cardEffectColorEnabled" :value="(cardEffectColor as `#${string}`)" type="hex" @value-change="(val: any) => cardEffectColor = val.hex">
<button
type="button"
class="h-7 w-12 cursor-pointer rounded-md border border-input shadow-sm"
:style="{ backgroundColor: cardEffectColor }"
aria-label="Effekt-Farbe auswählen"
:aria-label="$t('settings.pickEffectColor')"
/>
</ColorPicker>
<label class="flex cursor-pointer items-center gap-2 text-sm text-muted-foreground">
<Checkbox v-model="cardVignette" />
Vignette (Text lesbar halten)
{{ $t('settings.vignette') }}
</label>
</div>
</div>
<div class="space-y-1.5">
<Label>Hover-Card-Border</Label>
<Label>{{ $t('settings.cardBorder') }}</Label>
<Select v-model="cardBorderEffect">
<SelectTrigger class="w-full">
<SelectValue placeholder="Kein Effekt" />
<SelectValue :placeholder="$t('settings.noEffectPlaceholder')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">
Keiner
{{ $t('settings.noEffect') }}
</SelectItem>
<SelectItem v-for="e in CARD_BORDER_EFFECTS" :key="e.key" :value="e.key">
{{ e.label }}
@@ -208,20 +225,20 @@ async function save() {
<div v-if="cardBorderEffect !== 'none'" class="flex items-center gap-3 pt-1">
<label class="flex cursor-pointer items-center gap-2 text-sm text-muted-foreground">
<Checkbox v-model="cardBorderEffectColorEnabled" />
Eigene Border-Farbe
{{ $t('settings.ownBorderColor') }}
</label>
<ColorPicker v-if="cardBorderEffectColorEnabled" :value="cardBorderEffectColor" type="hex" @value-change="(val: any) => cardBorderEffectColor = val.hex">
<ColorPicker v-if="cardBorderEffectColorEnabled" :value="(cardBorderEffectColor as `#${string}`)" type="hex" @value-change="(val: any) => cardBorderEffectColor = val.hex">
<button
type="button"
class="h-7 w-12 cursor-pointer rounded-md border border-input shadow-sm"
:style="{ backgroundColor: cardBorderEffectColor }"
aria-label="Border-Farbe auswählen"
:aria-label="$t('settings.pickBorderColor')"
/>
</ColorPicker>
</div>
</div>
<div v-if="showPreview" class="space-y-1.5">
<Label>Vorschau</Label>
<Label>{{ $t('settings.preview') }}</Label>
<div class="relative h-24 overflow-hidden rounded-md border bg-popover">
<component :is="previewEffect" v-if="previewEffect" :color="previewEffectColor" />
<div v-if="previewEffect" class="absolute inset-0 bg-popover/60" />
@@ -232,34 +249,34 @@ async function save() {
<CardBorderEffect v-if="previewBorder" :effect="previewBorder" :color="previewBorderColor" />
</div>
<p class="text-xs text-muted-foreground">
So sieht deine Hover-Card für andere aus.
{{ $t('settings.previewHint') }}
</p>
</div>
<div class="space-y-1.5">
<Label>Avatar</Label>
<Label>{{ $t('settings.avatar') }}</Label>
<div v-if="user.avatarUrl && !avatar" class="mb-2 flex items-center gap-2">
<NuxtImg
:src="user.avatarUrl"
alt="Aktueller Avatar"
:alt="$t('settings.currentAvatar')"
width="48"
height="48"
fit="cover"
class="size-12 rounded-lg"
/>
<span class="text-xs text-muted-foreground">Aktueller Avatar</span>
<span class="text-xs text-muted-foreground">{{ $t('settings.currentAvatar') }}</span>
</div>
<FileDropzone v-model="avatar" accept="image/*" hint="Bilder, max. 5 MB" />
<FileDropzone v-model="avatar" accept="image/*" :hint="t('upload.avatarHint')" />
</div>
<Button type="submit" class="w-full" :disabled="saving">
{{ saving ? 'Speichert…' : 'Speichern' }}
{{ saving ? $t('common.saving') : $t('common.save') }}
</Button>
</form>
</CardContent>
</Card>
<p v-else class="text-center text-muted-foreground">
<NuxtLink to="/login" class="text-primary underline">
Melde dich an
</NuxtLink>, um deine Einstellungen zu ändern.
{{ $t('common.loginLink') }}
</NuxtLink>{{ $t('settings.loginToChange') }}
</p>
</div>
</template>