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
+29 -28
View File
@@ -34,6 +34,7 @@ interface AdminUser {
}
const { user, can } = useAuth()
const { t } = useI18n()
const { data: rolesList, refresh: refreshRoles } = await useFetch<Role[]>('/api/admin/roles', { default: () => [] })
const { data: usersList, refresh: refreshUsers } = await useFetch<AdminUser[]>('/api/admin/users', { default: () => [] })
@@ -47,12 +48,12 @@ const selectedRole = ref<Record<number, number | undefined>>({})
const editedPermissions = ref<Record<number, string[]>>({})
const savingRole = ref<number | null>(null)
const userColumns: ColumnDef<DataTableFeatures, AdminUser>[] = [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'E-Mail', cell: ({ getValue }) => getValue() ?? '—' },
{ id: 'roles', header: 'Rollen', enableSorting: false, enableGlobalFilter: false },
{ id: 'actions', header: 'Aktion', enableSorting: false, enableGlobalFilter: false },
]
const userColumns = computed<ColumnDef<DataTableFeatures, AdminUser>[]>(() => [
{ accessorKey: 'name', header: t('common.name') },
{ accessorKey: 'email', header: t('common.email'), cell: ({ getValue }) => getValue() ?? '—' },
{ id: 'roles', header: t('admin.rolesCol'), enableSorting: false, enableGlobalFilter: false },
{ id: 'actions', header: t('admin.actionCol'), enableSorting: false, enableGlobalFilter: false },
])
function userSearch(u: AdminUser, query: string) {
const q = query.toLowerCase()
@@ -61,7 +62,7 @@ function userSearch(u: AdminUser, query: string) {
function errText(err: unknown) {
const e = err as { data?: { statusMessage?: string, message?: string } }
return e.data?.statusMessage ?? e.data?.message ?? 'Aktion fehlgeschlagen'
return e.data?.statusMessage ?? e.data?.message ?? t('admin.actionFailed')
}
async function refreshAll() {
@@ -178,15 +179,15 @@ async function saveMotd() {
<div class="grid gap-6 px-4 lg:grid-cols-2 lg:px-6">
<Card>
<CardHeader>
<CardTitle>Rollen</CardTitle>
<CardDescription>Bestehende Rollen und neue anlegen (z. B. moderator). Die admin-Rolle hat immer alle Berechtigungen.</CardDescription>
<CardTitle>{{ $t('admin.roles') }}</CardTitle>
<CardDescription>{{ $t('admin.rolesDesc') }}</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<ul class="space-y-2">
<li v-for="role in rolesList" :key="role.id" class="rounded-md border px-3 py-2">
<div class="flex items-center gap-3">
<span class="font-medium">{{ role.name }}</span>
<span class="text-sm text-muted-foreground">{{ role.memberCount }} Nutzer</span>
<span class="text-sm text-muted-foreground">{{ $t('admin.memberCount', { n: role.memberCount }) }}</span>
<Button
v-if="!SYSTEM_ROLES.includes(role.name as 'admin' | 'moderator') && can('roles.manage')"
variant="ghost"
@@ -195,7 +196,7 @@ async function saveMotd() {
@click="deleteRole(role.id)"
>
<Icon name="ph:trash-fill" class="size-4" />
<span class="sr-only">Rolle löschen</span>
<span class="sr-only">{{ $t('admin.deleteRole') }}</span>
</Button>
</div>
<div class="mt-2 flex flex-wrap gap-x-4 gap-y-1">
@@ -209,7 +210,7 @@ async function saveMotd() {
:disabled="role.name === 'admin' || !can('roles.manage') || savingRole === role.id"
@update:model-value="togglePermission(role, perm.key, $event)"
/>
{{ perm.label }}
{{ $t(`admin.perms.${perm.key}`) }}
</label>
</div>
<div v-if="isDirty(role)" class="mt-2 flex items-center gap-2">
@@ -218,7 +219,7 @@ async function saveMotd() {
:disabled="savingRole === role.id"
@click="savePermissions(role)"
>
{{ savingRole === role.id ? 'Speichern…' : 'Speichern' }}
{{ savingRole === role.id ? $t('common.saving') : $t('common.save') }}
</Button>
<Button
size="sm"
@@ -226,15 +227,15 @@ async function saveMotd() {
:disabled="savingRole === role.id"
@click="resetPermissions(role)"
>
Zurücksetzen
{{ $t('common.reset') }}
</Button>
</div>
</li>
</ul>
<form v-if="can('roles.manage')" class="flex gap-2" @submit.prevent="createRole">
<Input v-model="newRoleName" placeholder="Neuer Rollenname" maxlength="50" />
<Input v-model="newRoleName" :placeholder="$t('admin.newRole')" maxlength="50" />
<Button type="submit" :disabled="!newRoleName.trim()">
Anlegen
{{ $t('admin.create') }}
</Button>
</form>
</CardContent>
@@ -242,14 +243,14 @@ async function saveMotd() {
<Card v-if="can('users.manage')">
<CardHeader>
<CardTitle>Benutzer</CardTitle>
<CardDescription>Rollen zuweisen oder entfernen.</CardDescription>
<CardTitle>{{ $t('admin.users') }}</CardTitle>
<CardDescription>{{ $t('admin.usersDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<DataTable
:columns="userColumns"
:data="usersList"
search-placeholder="Name oder E-Mail suchen…"
:search-placeholder="$t('admin.searchUsers')"
:search-fn="userSearch"
>
<template #cell-name="{ row }">
@@ -266,10 +267,10 @@ async function saveMotd() {
@click="removeRole(row.original.id, r.id)"
>
<Icon name="ph:x-fill" class="size-3" />
<span class="sr-only">Rolle {{ r.name }} entfernen</span>
<span class="sr-only">{{ $t('admin.removeRole', { name: r.name }) }}</span>
</button>
</Badge>
<span v-if="!row.original.roles.length" class="text-xs text-muted-foreground">keine Rollen</span>
<span v-if="!row.original.roles.length" class="text-xs text-muted-foreground">{{ $t('admin.noRoles') }}</span>
</div>
</template>
<template #cell-actions="{ row }">
@@ -279,8 +280,8 @@ async function saveMotd() {
:disabled="!rolesList.some(r => !row.original.roles.some(ur => ur.id === r.id))"
@update:model-value="selectedRole[row.original.id] = $event === undefined ? undefined : Number($event)"
>
<SelectTrigger size="sm" class="w-40" aria-label="Rolle wählen">
<SelectValue placeholder="Rolle wählen…" />
<SelectTrigger size="sm" class="w-40" :aria-label="$t('admin.chooseRole')">
<SelectValue :placeholder="$t('admin.chooseRole')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
@@ -300,7 +301,7 @@ async function saveMotd() {
:disabled="!selectedRole[row.original.id]"
@click="addRole(row.original.id)"
>
Zuweisen
{{ $t('admin.assign') }}
</Button>
</div>
</template>
@@ -310,19 +311,19 @@ async function saveMotd() {
<Card v-if="can('site.manage')" class="lg:col-span-2">
<CardHeader>
<CardTitle>Ansage</CardTitle>
<CardDescription>Wird allen Besuchern direkt unter der Kopfzeile angezeigt und bei Änderung sofort aktualisiert. Leer lassen, um sie zu entfernen.</CardDescription>
<CardTitle>{{ $t('admin.motd') }}</CardTitle>
<CardDescription>{{ $t('admin.motdDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<form class="flex flex-col gap-2 sm:flex-row sm:items-end" @submit.prevent="saveMotd">
<Textarea
v-model="motdDraft"
placeholder="Ansagetext…"
:placeholder="$t('admin.motdPlaceholder')"
maxlength="500"
class="min-h-16"
/>
<Button type="submit" class="shrink-0" :disabled="motdSaving">
{{ motdSaving ? 'Speichern…' : 'Speichern' }}
{{ motdSaving ? $t('common.saving') : $t('common.save') }}
</Button>
</form>
</CardContent>