i18n: internationalize all admin pages

Add full i18n coverage for the admin section — locale keys, layout nav,
users, videos, and articles pages (list, new, edit).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 16:49:30 +01:00
parent 95fd9f48fc
commit 7d373b3aa3
10 changed files with 330 additions and 187 deletions

View File

@@ -896,6 +896,142 @@ export default {
head: {
title: "SexyArt | {title}",
},
admin: {
nav: {
back_to_site: "← Back to site",
back_mobile: "← Back",
title: "Admin",
users: "Users",
videos: "Videos",
articles: "Articles",
},
common: {
save_changes: "Save changes",
saving: "Saving…",
creating: "Creating…",
deleting: "Deleting…",
featured: "Featured",
premium: "Premium",
write: "Write",
preview: "Preview",
cover_image: "Cover image",
tags: "Tags",
publish_date: "Publish date",
title_field: "Title *",
slug_field: "Slug *",
title_slug_required: "Title and slug are required",
image_uploaded: "Image uploaded",
image_upload_failed: "Image upload failed",
},
users: {
title: "Users",
total: "{total} total",
search_placeholder: "Search email or name…",
filter_all: "All",
col_user: "User",
col_email: "Email",
col_role: "Role",
col_joined: "Joined",
col_actions: "Actions",
role_viewer: "Viewer",
role_model: "Model",
admin_badge: "Admin",
no_results: "No users found",
showing: "Showing {start}{end} of {total}",
role_updated: "Role updated to {role}",
role_update_failed: "Failed to update role",
delete_title: "Delete user",
delete_description: "Are you sure you want to permanently delete {name}? This cannot be undone.",
delete_success: "User deleted",
delete_error: "Failed to delete user",
},
user_edit: {
first_name: "First name",
last_name: "Last name",
artist_name: "Artist name",
avatar: "Avatar",
banner: "Banner",
is_admin: "Administrator",
is_admin_hint: "Grants full admin access to the dashboard",
photos: "Photo gallery",
no_photos: "No photos yet.",
avatar_uploaded: "Avatar uploaded",
avatar_failed: "Avatar upload failed",
banner_uploaded: "Banner uploaded",
banner_failed: "Banner upload failed",
photos_added: "{count} photos added",
photo_upload_failed: "Failed to upload {name}",
photo_remove_failed: "Failed to remove photo",
save_success: "Saved",
save_error: "Save failed",
},
videos: {
title: "Videos",
new_video: "New video",
col_video: "Video",
col_badges: "Badges",
col_plays: "Plays",
col_likes: "Likes",
no_results: "No videos yet",
delete_title: "Delete video",
delete_description: "Permanently delete {title}? This cannot be undone.",
delete_success: "Video deleted",
delete_error: "Failed to delete video",
},
video_form: {
new_title: "New video",
edit_title: "Edit video",
title_placeholder: "Video title",
slug_placeholder: "video-slug",
description: "Description",
description_placeholder: "Optional description",
video_file: "Video file",
current_file: "Current file: {id}",
models: "Models",
no_models: "No models",
models_selected: "{count} models selected",
cover_uploaded: "Cover image uploaded",
video_uploaded: "Video uploaded",
video_upload_failed: "Video upload failed",
create_success: "Video created",
create_error: "Failed to create video",
update_success: "Video updated",
update_error: "Failed to update video",
create: "Create video",
},
articles: {
title: "Articles",
new_article: "New article",
col_article: "Article",
col_category: "Category",
col_published: "Published",
no_results: "No articles yet",
delete_title: "Delete article",
delete_description: "Permanently delete {title}? This cannot be undone.",
delete_success: "Article deleted",
delete_error: "Failed to delete article",
},
article_form: {
new_title: "New article",
edit_title: "Edit article",
title_placeholder: "Article title",
slug_placeholder: "article-slug",
excerpt: "Excerpt",
excerpt_placeholder: "Short summary…",
content: "Content (Markdown)",
content_placeholder: "Write in Markdown…",
preview_placeholder: "Preview will appear here…",
category: "Category",
category_placeholder: "e.g. news, tutorial…",
author: "Author",
no_author: "No author",
create_success: "Article created",
create_error: "Failed to create article",
update_success: "Article updated",
update_error: "Failed to update article",
create: "Create article",
},
},
gamification: {
leaderboard: "Leaderboard",
leaderboard_description: "Compete with other creators and players for the top spot",

View File

@@ -1,13 +1,14 @@
<script lang="ts">
import { page } from "$app/state";
import { _ } from "svelte-i18n";
const { children } = $props();
const navLinks = [
{ name: "Users", href: "/admin/users", icon: "icon-[ri--team-line]" },
{ name: "Videos", href: "/admin/videos", icon: "icon-[ri--film-line]" },
{ name: "Articles", href: "/admin/articles", icon: "icon-[ri--article-line]" },
];
const navLinks = $derived([
{ name: $_("admin.nav.users"), href: "/admin/users", icon: "icon-[ri--team-line]" },
{ name: $_("admin.nav.videos"), href: "/admin/videos", icon: "icon-[ri--film-line]" },
{ name: $_("admin.nav.articles"), href: "/admin/articles", icon: "icon-[ri--article-line]" },
]);
function isActive(href: string) {
return page.url.pathname.startsWith(href);
@@ -20,7 +21,7 @@
<!-- Mobile top nav -->
<div class="lg:hidden flex items-center gap-2 py-3 border-b border-border/40">
<a href="/" class="text-xs text-muted-foreground hover:text-foreground transition-colors shrink-0 mr-2">
← Back
{$_("admin.nav.back_mobile")}
</a>
{#each navLinks as link (link.href)}
<a
@@ -43,9 +44,9 @@
<aside class="hidden lg:flex w-56 shrink-0 flex-col border-r border-border/40">
<div class="px-4 py-5 border-b border-border/40">
<a href="/" class="text-xs text-muted-foreground hover:text-foreground transition-colors">
← Back to site
{$_("admin.nav.back_to_site")}
</a>
<h1 class="mt-2 text-base font-bold text-foreground">Admin</h1>
<h1 class="mt-2 text-base font-bold text-foreground">{$_("admin.nav.title")}</h1>
</div>
<nav class="flex-1 p-3 space-y-1">

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { invalidateAll } from "$app/navigation";
import { toast } from "svelte-sonner";
import { _ } from "svelte-i18n";
import { deleteArticle } from "$lib/services";
import { getAssetUrl } from "$lib/api";
import { Button } from "$lib/components/ui/button";
@@ -26,12 +27,12 @@
deleting = true;
try {
await deleteArticle(deleteTarget.id);
toast.success("Article deleted");
toast.success($_("admin.articles.delete_success"));
deleteOpen = false;
deleteTarget = null;
await invalidateAll();
} catch {
toast.error("Failed to delete article");
toast.error($_("admin.articles.delete_error"));
} finally {
deleting = false;
}
@@ -40,9 +41,9 @@
<div class="p-3 sm:p-6">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">Articles</h1>
<h1 class="text-2xl font-bold">{$_("admin.articles.title")}</h1>
<Button href="/admin/articles/new">
<span class="icon-[ri--add-line] h-4 w-4 mr-1"></span>New article
<span class="icon-[ri--add-line] h-4 w-4 mr-1"></span>{$_("admin.articles.new_article")}
</Button>
</div>
@@ -50,10 +51,10 @@
<table class="w-full text-sm">
<thead class="bg-muted/30">
<tr>
<th class="px-4 py-3 text-left font-medium text-muted-foreground">Article</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell">Category</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell">Published</th>
<th class="px-4 py-3 text-right font-medium text-muted-foreground">Actions</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground">{$_("admin.articles.col_article")}</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell">{$_("admin.articles.col_category")}</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell">{$_("admin.articles.col_published")}</th>
<th class="px-4 py-3 text-right font-medium text-muted-foreground">{$_("admin.users.col_actions")}</th>
</tr>
</thead>
<tbody class="divide-y divide-border/30">
@@ -79,7 +80,7 @@
{#if article.featured}
<span
class="text-xs px-1.5 py-0.5 rounded bg-primary/10 text-primary font-medium"
>Featured</span
>{$_("admin.common.featured")}</span
>
{/if}
</div>
@@ -110,7 +111,7 @@
{#if data.articles.length === 0}
<tr>
<td colspan="4" class="px-4 py-8 text-center text-muted-foreground">
No articles yet
{$_("admin.articles.no_results")}
</td>
</tr>
{/if}
@@ -122,15 +123,15 @@
<Dialog.Root bind:open={deleteOpen}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Delete article</Dialog.Title>
<Dialog.Title>{$_("admin.articles.delete_title")}</Dialog.Title>
<Dialog.Description>
Permanently delete <strong>{deleteTarget?.title}</strong>? This cannot be undone.
{$_("admin.articles.delete_description", { values: { title: deleteTarget?.title } })}
</Dialog.Description>
</Dialog.Header>
<Dialog.Footer>
<Button variant="outline" onclick={() => (deleteOpen = false)}>Cancel</Button>
<Button variant="outline" onclick={() => (deleteOpen = false)}>{$_("common.cancel")}</Button>
<Button variant="destructive" disabled={deleting} onclick={handleDelete}>
{deleting ? "Deleting" : "Delete"}
{deleting ? $_("admin.common.deleting") : $_("common.delete")}
</Button>
</Dialog.Footer>
</Dialog.Content>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { toast } from "svelte-sonner";
import { _ } from "svelte-i18n";
import { updateArticle, uploadFile } from "$lib/services";
import { marked } from "marked";
import { Button } from "$lib/components/ui/button";
@@ -40,9 +41,9 @@
try {
const res = await uploadFile(fd);
imageId = res.id;
toast.success("Image uploaded");
toast.success($_("admin.common.image_uploaded"));
} catch {
toast.error("Image upload failed");
toast.error($_("admin.common.image_upload_failed"));
}
}
@@ -62,10 +63,10 @@
featured,
publishDate: publishDate || undefined,
});
toast.success("Article updated");
toast.success($_("admin.article_form.update_success"));
goto("/admin/articles");
} catch (e: any) {
toast.error(e?.message ?? "Failed to update article");
toast.error(e?.message ?? $_("admin.article_form.update_error"));
} finally {
saving = false;
}
@@ -75,43 +76,43 @@
<div class="p-3 sm:p-6">
<div class="flex items-center gap-4 mb-6">
<Button variant="ghost" href="/admin/articles" size="sm">
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>Back
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
</Button>
<h1 class="text-2xl font-bold">Edit article</h1>
<h1 class="text-2xl font-bold">{$_("admin.article_form.edit_title")}</h1>
</div>
<div class="space-y-5 max-w-4xl">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="title">Title *</Label>
<Label for="title">{$_("admin.common.title_field")}</Label>
<Input id="title" bind:value={title} />
</div>
<div class="space-y-1.5">
<Label for="slug">Slug *</Label>
<Label for="slug">{$_("admin.common.slug_field")}</Label>
<Input id="slug" bind:value={slug} />
</div>
</div>
<div class="space-y-1.5">
<Label for="excerpt">Excerpt</Label>
<Label for="excerpt">{$_("admin.article_form.excerpt")}</Label>
<Textarea id="excerpt" bind:value={excerpt} rows={2} />
</div>
<!-- Markdown editor with live preview -->
<div class="space-y-1.5">
<div class="flex items-center justify-between">
<Label>Content (Markdown)</Label>
<Label>{$_("admin.article_form.content")}</Label>
<div class="flex rounded-lg border border-border/40 overflow-hidden text-xs sm:hidden">
<button
type="button"
class={`px-3 py-1 transition-colors ${editorTab === "write" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "write")}
>Write</button>
>{$_("admin.common.write")}</button>
<button
type="button"
class={`px-3 py-1 transition-colors ${editorTab === "preview" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "preview")}
>Preview</button>
>{$_("admin.common.preview")}</button>
</div>
</div>
<div class="sm:grid sm:grid-cols-2 sm:gap-4 min-h-96">
@@ -125,14 +126,14 @@
{#if preview}
{@html preview}
{:else}
<p class="text-muted-foreground italic text-sm">Preview will appear here…</p>
<p class="text-muted-foreground italic text-sm">{$_("admin.article_form.preview_placeholder")}</p>
{/if}
</div>
</div>
</div>
<div class="space-y-1.5">
<Label>Cover image</Label>
<Label>{$_("admin.common.cover_image")}</Label>
{#if imageId}
<img
src={getAssetUrl(imageId, "thumbnail")}
@@ -145,7 +146,7 @@
<!-- Author -->
<div class="space-y-1.5">
<Label>Author</Label>
<Label>{$_("admin.article_form.author")}</Label>
<Select type="single" bind:value={authorId}>
<SelectTrigger class="w-full">
{#if selectedAuthor}
@@ -154,11 +155,11 @@
{/if}
{selectedAuthor.artist_name}
{:else}
<span class="text-muted-foreground">No author</span>
<span class="text-muted-foreground">{$_("admin.article_form.no_author")}</span>
{/if}
</SelectTrigger>
<SelectContent>
<SelectItem value="">No author</SelectItem>
<SelectItem value="">{$_("admin.article_form.no_author")}</SelectItem>
{#each data.authors as author (author.id)}
<SelectItem value={author.id}>
{#if author.avatar}
@@ -173,30 +174,30 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="category">Category</Label>
<Label for="category">{$_("admin.article_form.category")}</Label>
<Input id="category" bind:value={category} />
</div>
<div class="space-y-1.5">
<Label for="publishDate">Publish date</Label>
<Label for="publishDate">{$_("admin.common.publish_date")}</Label>
<Input id="publishDate" type="datetime-local" bind:value={publishDate} />
</div>
</div>
<div class="space-y-1.5">
<Label>Tags</Label>
<Label>{$_("admin.common.tags")}</Label>
<TagsInput bind:value={tags} />
</div>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={featured} class="rounded" />
<span class="text-sm">Featured</span>
<span class="text-sm">{$_("admin.common.featured")}</span>
</label>
<div class="flex gap-3 pt-2">
<Button onclick={handleSubmit} disabled={saving}>
{saving ? "Saving" : "Save changes"}
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
</Button>
<Button variant="outline" href="/admin/articles">Cancel</Button>
<Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button>
</div>
</div>
</div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { toast } from "svelte-sonner";
import { _ } from "svelte-i18n";
import { createArticle, uploadFile } from "$lib/services";
import { marked } from "marked";
import { Button } from "$lib/components/ui/button";
@@ -39,15 +40,15 @@
try {
const res = await uploadFile(fd);
imageId = res.id;
toast.success("Image uploaded");
toast.success($_("admin.common.image_uploaded"));
} catch {
toast.error("Image upload failed");
toast.error($_("admin.common.image_upload_failed"));
}
}
async function handleSubmit() {
if (!title || !slug) {
toast.error("Title and slug are required");
toast.error($_("admin.common.title_slug_required"));
return;
}
saving = true;
@@ -63,10 +64,10 @@
featured,
publishDate: publishDate || undefined,
});
toast.success("Article created");
toast.success($_("admin.article_form.create_success"));
goto("/admin/articles");
} catch (e: any) {
toast.error(e?.message ?? "Failed to create article");
toast.error(e?.message ?? $_("admin.article_form.create_error"));
} finally {
saving = false;
}
@@ -76,57 +77,57 @@
<div class="p-3 sm:p-6">
<div class="flex items-center gap-4 mb-6">
<Button variant="ghost" href="/admin/articles" size="sm">
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>Back
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
</Button>
<h1 class="text-2xl font-bold">New article</h1>
<h1 class="text-2xl font-bold">{$_("admin.article_form.new_title")}</h1>
</div>
<div class="space-y-5 max-w-4xl">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="title">Title *</Label>
<Label for="title">{$_("admin.common.title_field")}</Label>
<Input
id="title"
bind:value={title}
oninput={() => {
if (!slug) slug = generateSlug(title);
}}
placeholder="Article title"
placeholder={$_("admin.article_form.title_placeholder")}
/>
</div>
<div class="space-y-1.5">
<Label for="slug">Slug *</Label>
<Input id="slug" bind:value={slug} placeholder="article-slug" />
<Label for="slug">{$_("admin.common.slug_field")}</Label>
<Input id="slug" bind:value={slug} placeholder={$_("admin.article_form.slug_placeholder")} />
</div>
</div>
<div class="space-y-1.5">
<Label for="excerpt">Excerpt</Label>
<Textarea id="excerpt" bind:value={excerpt} placeholder="Short summary…" rows={2} />
<Label for="excerpt">{$_("admin.article_form.excerpt")}</Label>
<Textarea id="excerpt" bind:value={excerpt} placeholder={$_("admin.article_form.excerpt_placeholder")} rows={2} />
</div>
<!-- Markdown editor with live preview -->
<div class="space-y-1.5">
<div class="flex items-center justify-between">
<Label>Content (Markdown)</Label>
<Label>{$_("admin.article_form.content")}</Label>
<div class="flex rounded-lg border border-border/40 overflow-hidden text-xs sm:hidden">
<button
type="button"
class={`px-3 py-1 transition-colors ${editorTab === "write" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "write")}
>Write</button>
>{$_("admin.common.write")}</button>
<button
type="button"
class={`px-3 py-1 transition-colors ${editorTab === "preview" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "preview")}
>Preview</button>
>{$_("admin.common.preview")}</button>
</div>
</div>
<!-- Mobile: single pane toggled; Desktop: side by side -->
<div class="sm:grid sm:grid-cols-2 sm:gap-4 min-h-96">
<Textarea
bind:value={content}
placeholder="Write in Markdown…"
placeholder={$_("admin.article_form.content_placeholder")}
class={`h-full min-h-96 font-mono text-sm resize-none ${editorTab === "preview" ? "hidden sm:flex" : ""}`}
/>
<div
@@ -135,44 +136,44 @@
{#if preview}
{@html preview}
{:else}
<p class="text-muted-foreground italic text-sm">Preview will appear here…</p>
<p class="text-muted-foreground italic text-sm">{$_("admin.article_form.preview_placeholder")}</p>
{/if}
</div>
</div>
</div>
<div class="space-y-1.5">
<Label>Cover image</Label>
<Label>{$_("admin.common.cover_image")}</Label>
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
{#if imageId}<p class="text-xs text-green-600 mt-1">Image uploaded ✓</p>{/if}
{#if imageId}<p class="text-xs text-green-600 mt-1">{$_("admin.common.image_uploaded")}</p>{/if}
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="category">Category</Label>
<Input id="category" bind:value={category} placeholder="e.g. news, tutorial…" />
<Label for="category">{$_("admin.article_form.category")}</Label>
<Input id="category" bind:value={category} placeholder={$_("admin.article_form.category_placeholder")} />
</div>
<div class="space-y-1.5">
<Label for="publishDate">Publish date</Label>
<Label for="publishDate">{$_("admin.common.publish_date")}</Label>
<Input id="publishDate" type="datetime-local" bind:value={publishDate} />
</div>
</div>
<div class="space-y-1.5">
<Label>Tags</Label>
<Label>{$_("admin.common.tags")}</Label>
<TagsInput bind:value={tags} />
</div>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={featured} class="rounded" />
<span class="text-sm">Featured</span>
<span class="text-sm">{$_("admin.common.featured")}</span>
</label>
<div class="flex gap-3 pt-2">
<Button onclick={handleSubmit} disabled={saving}>
{saving ? "Creating" : "Create article"}
{saving ? $_("admin.common.creating") : $_("admin.article_form.create")}
</Button>
<Button variant="outline" href="/admin/articles">Cancel</Button>
<Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button>
</div>
</div>
</div>

View File

@@ -3,6 +3,7 @@
import { page } from "$app/state";
import { SvelteURLSearchParams } from "svelte/reactivity";
import { toast } from "svelte-sonner";
import { _ } from "svelte-i18n";
import { adminUpdateUser, adminDeleteUser } from "$lib/services";
import { getAssetUrl } from "$lib/api";
import { Button } from "$lib/components/ui/button";
@@ -48,10 +49,10 @@
updatingId = user.id;
try {
await adminUpdateUser({ userId: user.id, role: newRole });
toast.success(`Role updated to ${newRole}`);
toast.success($_("admin.users.role_updated", { values: { role: newRole } }));
await invalidateAll();
} catch {
toast.error("Failed to update role");
toast.error($_("admin.users.role_update_failed"));
} finally {
updatingId = null;
}
@@ -67,12 +68,12 @@
deleting = true;
try {
await adminDeleteUser(deleteTarget.id);
toast.success("User deleted");
toast.success($_("admin.users.delete_success"));
deleteOpen = false;
deleteTarget = null;
await invalidateAll();
} catch {
toast.error("Failed to delete user");
toast.error($_("admin.users.delete_error"));
} finally {
deleting = false;
}
@@ -85,14 +86,14 @@
<div class="p-3 sm:p-6">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">Users</h1>
<span class="text-sm text-muted-foreground">{data.total} total</span>
<h1 class="text-2xl font-bold">{$_("admin.users.title")}</h1>
<span class="text-sm text-muted-foreground">{$_("admin.users.total", { values: { total: data.total } })}</span>
</div>
<!-- Filters -->
<div class="flex flex-wrap gap-3 mb-4">
<Input
placeholder="Search email or name…"
placeholder={$_("admin.users.search_placeholder")}
class="max-w-xs"
value={searchValue}
oninput={(e) => {
@@ -108,7 +109,7 @@
variant={data.role === role || (!data.role && role === "") ? "default" : "outline"}
onclick={() => setRole(role)}
>
{role || "All"}
{role ? $_(`admin.users.role_${role}`) : $_("admin.users.filter_all")}
</Button>
{/each}
</div>
@@ -119,11 +120,11 @@
<table class="w-full text-sm">
<thead class="bg-muted/30">
<tr>
<th class="px-4 py-3 text-left font-medium text-muted-foreground">User</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell">Email</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground">Role</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden md:table-cell">Joined</th>
<th class="px-4 py-3 text-right font-medium text-muted-foreground">Actions</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground">{$_("admin.users.col_user")}</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell">{$_("admin.users.col_email")}</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground">{$_("admin.users.col_role")}</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden md:table-cell">{$_("admin.users.col_joined")}</th>
<th class="px-4 py-3 text-right font-medium text-muted-foreground">{$_("admin.users.col_actions")}</th>
</tr>
</thead>
<tbody class="divide-y divide-border/30">
@@ -148,7 +149,7 @@
<div class="flex items-center gap-1.5">
<span class="font-medium truncate">{user.artist_name || user.first_name || "—"}</span>
{#if user.is_admin}
<Badge variant="default" class="shrink-0 text-[10px] px-1.5 py-0">Admin</Badge>
<Badge variant="default" class="shrink-0 text-[10px] px-1.5 py-0">{$_("admin.users.admin_badge")}</Badge>
{/if}
</div>
<span class="text-xs text-muted-foreground sm:hidden truncate block">{user.email}</span>
@@ -167,8 +168,8 @@
{user.role}
</SelectTrigger>
<SelectContent>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="model">Model</SelectItem>
<SelectItem value="viewer">{$_("admin.users.role_viewer")}</SelectItem>
<SelectItem value="model">{$_("admin.users.role_model")}</SelectItem>
</SelectContent>
</Select>
</td>
@@ -194,7 +195,7 @@
{#if data.items.length === 0}
<tr>
<td colspan="5" class="px-4 py-8 text-center text-muted-foreground">No users found</td>
<td colspan="5" class="px-4 py-8 text-center text-muted-foreground">{$_("admin.users.no_results")}</td>
</tr>
{/if}
</tbody>
@@ -205,7 +206,7 @@
{#if data.total > data.limit}
<div class="flex items-center justify-between mt-4">
<span class="text-sm text-muted-foreground">
Showing {data.offset + 1}{Math.min(data.offset + data.limit, data.total)} of {data.total}
{$_("admin.users.showing", { values: { start: data.offset + 1, end: Math.min(data.offset + data.limit, data.total), total: data.total } })}
</span>
<div class="flex gap-2">
<Button
@@ -218,7 +219,7 @@
goto(`?${params.toString()}`);
}}
>
Previous
{$_("common.previous")}
</Button>
<Button
size="sm"
@@ -230,7 +231,7 @@
goto(`?${params.toString()}`);
}}
>
Next
{$_("common.next")}
</Button>
</div>
</div>
@@ -241,17 +242,15 @@
<Dialog.Root bind:open={deleteOpen}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Delete user</Dialog.Title>
<Dialog.Title>{$_("admin.users.delete_title")}</Dialog.Title>
<Dialog.Description>
Are you sure you want to permanently delete <strong
>{deleteTarget?.artist_name || deleteTarget?.email}</strong
>? This cannot be undone.
{$_("admin.users.delete_description", { values: { name: deleteTarget?.artist_name || deleteTarget?.email } })}
</Dialog.Description>
</Dialog.Header>
<Dialog.Footer>
<Button variant="outline" onclick={() => (deleteOpen = false)}>Cancel</Button>
<Button variant="outline" onclick={() => (deleteOpen = false)}>{$_("common.cancel")}</Button>
<Button variant="destructive" disabled={deleting} onclick={handleDelete}>
{deleting ? "Deleting" : "Delete"}
{deleting ? $_("admin.common.deleting") : $_("common.delete")}
</Button>
</Dialog.Footer>
</Dialog.Content>

View File

@@ -8,6 +8,7 @@
uploadFile,
} from "$lib/services";
import { getAssetUrl } from "$lib/api";
import { _ } from "svelte-i18n";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
@@ -31,9 +32,9 @@
try {
const res = await uploadFile(fd);
avatarId = res.id;
toast.success("Avatar uploaded");
toast.success($_("admin.user_edit.avatar_uploaded"));
} catch {
toast.error("Avatar upload failed");
toast.error($_("admin.user_edit.avatar_failed"));
}
}
@@ -45,9 +46,9 @@
try {
const res = await uploadFile(fd);
bannerId = res.id;
toast.success("Banner uploaded");
toast.success($_("admin.user_edit.banner_uploaded"));
} catch {
toast.error("Banner upload failed");
toast.error($_("admin.user_edit.banner_failed"));
}
}
@@ -59,21 +60,21 @@
const res = await uploadFile(fd);
await adminAddUserPhoto(data.user.id, res.id);
} catch {
toast.error(`Failed to upload ${file.name}`);
toast.error($_("admin.user_edit.photo_upload_failed", { values: { name: file.name } }));
return;
}
}
toast.success(`${files.length} photo${files.length > 1 ? "s" : ""} added`);
toast.success($_("admin.user_edit.photos_added", { values: { count: files.length } }));
await invalidateAll();
}
async function removePhoto(fileId: string) {
try {
await adminRemoveUserPhoto(data.user.id, fileId);
toast.success("Photo removed");
toast.success($_("admin.user_edit.save_success"));
await invalidateAll();
} catch {
toast.error("Failed to remove photo");
toast.error($_("admin.user_edit.photo_remove_failed"));
}
}
@@ -89,9 +90,9 @@
bannerId: bannerId || undefined,
isAdmin,
});
toast.success("Saved");
toast.success($_("admin.user_edit.save_success"));
} catch (e: any) {
toast.error(e?.message ?? "Save failed");
toast.error(e?.message ?? $_("admin.user_edit.save_error"));
} finally {
saving = false;
}
@@ -101,11 +102,11 @@
<div class="p-3 sm:p-6 max-w-2xl">
<div class="flex items-center gap-4 mb-6">
<Button variant="ghost" href="/admin/users" size="sm">
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>Back
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
</Button>
<div>
<h1 class="text-2xl font-bold">{data.user.artist_name || data.user.email}</h1>
<p class="text-xs text-muted-foreground">{data.user.email} · {data.user.role}{data.user.is_admin ? " · admin" : ""}</p>
<p class="text-xs text-muted-foreground">{data.user.email} · {data.user.role}{data.user.is_admin ? " · " + $_("admin.users.admin_badge").toLowerCase() : ""}</p>
</div>
</div>
@@ -113,23 +114,23 @@
<!-- Basic info -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="firstName">First name</Label>
<Label for="firstName">{$_("admin.user_edit.first_name")}</Label>
<Input id="firstName" bind:value={firstName} />
</div>
<div class="space-y-1.5">
<Label for="lastName">Last name</Label>
<Label for="lastName">{$_("admin.user_edit.last_name")}</Label>
<Input id="lastName" bind:value={lastName} />
</div>
</div>
<div class="space-y-1.5">
<Label for="artistName">Artist name</Label>
<Label for="artistName">{$_("admin.user_edit.artist_name")}</Label>
<Input id="artistName" bind:value={artistName} />
</div>
<!-- Avatar -->
<div class="space-y-1.5">
<Label>Avatar</Label>
<Label>{$_("admin.user_edit.avatar")}</Label>
{#if avatarId}
<img
src={getAssetUrl(avatarId, "thumbnail")}
@@ -142,7 +143,7 @@
<!-- Banner -->
<div class="space-y-1.5">
<Label>Banner</Label>
<Label>{$_("admin.user_edit.banner")}</Label>
{#if bannerId}
<img
src={getAssetUrl(bannerId, "preview")}
@@ -157,20 +158,20 @@
<label class="flex items-center gap-3 rounded-lg border border-border/40 px-4 py-3 cursor-pointer hover:bg-muted/20 transition-colors">
<input type="checkbox" bind:checked={isAdmin} class="h-4 w-4 rounded accent-primary shrink-0" />
<div>
<span class="text-sm font-medium">Administrator</span>
<p class="text-xs text-muted-foreground">Grants full admin access to the dashboard</p>
<span class="text-sm font-medium">{$_("admin.user_edit.is_admin")}</span>
<p class="text-xs text-muted-foreground">{$_("admin.user_edit.is_admin_hint")}</p>
</div>
</label>
<div class="flex gap-3">
<Button onclick={handleSave} disabled={saving}>
{saving ? "Saving" : "Save changes"}
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
</Button>
</div>
<!-- Photo gallery -->
<div class="space-y-3 pt-4 border-t border-border/40">
<Label>Photo gallery</Label>
<Label>{$_("admin.user_edit.photos")}</Label>
{#if data.user.photos && data.user.photos.length > 0}
<div class="grid grid-cols-3 gap-2">
@@ -192,7 +193,7 @@
{/each}
</div>
{:else}
<p class="text-sm text-muted-foreground">No photos yet.</p>
<p class="text-sm text-muted-foreground">{$_("admin.user_edit.no_photos")}</p>
{/if}
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handlePhotoUpload} />

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { invalidateAll } from "$app/navigation";
import { toast } from "svelte-sonner";
import { _ } from "svelte-i18n";
import { deleteVideo } from "$lib/services";
import { getAssetUrl } from "$lib/api";
import { Button } from "$lib/components/ui/button";
@@ -24,12 +25,12 @@
deleting = true;
try {
await deleteVideo(deleteTarget.id);
toast.success("Video deleted");
toast.success($_("admin.videos.delete_success"));
deleteOpen = false;
deleteTarget = null;
await invalidateAll();
} catch {
toast.error("Failed to delete video");
toast.error($_("admin.videos.delete_error"));
} finally {
deleting = false;
}
@@ -38,9 +39,9 @@
<div class="p-3 sm:p-6">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">Videos</h1>
<h1 class="text-2xl font-bold">{$_("admin.videos.title")}</h1>
<Button href="/admin/videos/new">
<span class="icon-[ri--add-line] h-4 w-4 mr-1"></span>New video
<span class="icon-[ri--add-line] h-4 w-4 mr-1"></span>{$_("admin.videos.new_video")}
</Button>
</div>
@@ -48,11 +49,11 @@
<table class="w-full text-sm">
<thead class="bg-muted/30">
<tr>
<th class="px-4 py-3 text-left font-medium text-muted-foreground">Video</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell">Badges</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden md:table-cell">Plays</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden md:table-cell">Likes</th>
<th class="px-4 py-3 text-right font-medium text-muted-foreground">Actions</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground">{$_("admin.videos.col_video")}</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell">{$_("admin.videos.col_badges")}</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden md:table-cell">{$_("admin.videos.col_plays")}</th>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden md:table-cell">{$_("admin.videos.col_likes")}</th>
<th class="px-4 py-3 text-right font-medium text-muted-foreground">{$_("admin.users.col_actions")}</th>
</tr>
</thead>
<tbody class="divide-y divide-border/30">
@@ -82,10 +83,10 @@
<td class="px-4 py-3 hidden sm:table-cell">
<div class="flex gap-1">
{#if video.premium}
<Badge variant="outline" class="text-yellow-600 border-yellow-500/40 bg-yellow-500/10">Premium</Badge>
<Badge variant="outline" class="text-yellow-600 border-yellow-500/40 bg-yellow-500/10">{$_("admin.common.premium")}</Badge>
{/if}
{#if video.featured}
<Badge variant="default">Featured</Badge>
<Badge variant="default">{$_("admin.common.featured")}</Badge>
{/if}
</div>
</td>
@@ -111,7 +112,7 @@
{#if data.videos.length === 0}
<tr>
<td colspan="5" class="px-4 py-8 text-center text-muted-foreground">No videos yet</td>
<td colspan="5" class="px-4 py-8 text-center text-muted-foreground">{$_("admin.videos.no_results")}</td>
</tr>
{/if}
</tbody>
@@ -122,15 +123,15 @@
<Dialog.Root bind:open={deleteOpen}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Delete video</Dialog.Title>
<Dialog.Title>{$_("admin.videos.delete_title")}</Dialog.Title>
<Dialog.Description>
Permanently delete <strong>{deleteTarget?.title}</strong>? This cannot be undone.
{$_("admin.videos.delete_description", { values: { title: deleteTarget?.title } })}
</Dialog.Description>
</Dialog.Header>
<Dialog.Footer>
<Button variant="outline" onclick={() => (deleteOpen = false)}>Cancel</Button>
<Button variant="outline" onclick={() => (deleteOpen = false)}>{$_("common.cancel")}</Button>
<Button variant="destructive" disabled={deleting} onclick={handleDelete}>
{deleting ? "Deleting" : "Delete"}
{deleting ? $_("admin.common.deleting") : $_("common.delete")}
</Button>
</Dialog.Footer>
</Dialog.Content>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { toast } from "svelte-sonner";
import { _ } from "svelte-i18n";
import { updateVideo, setVideoModels, uploadFile } from "$lib/services";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
@@ -37,9 +38,9 @@
try {
const res = await uploadFile(fd);
imageId = res.id;
toast.success("Cover image uploaded");
toast.success($_("admin.video_form.cover_uploaded"));
} catch {
toast.error("Image upload failed");
toast.error($_("admin.common.image_upload_failed"));
}
}
@@ -51,9 +52,9 @@
try {
const res = await uploadFile(fd);
movieId = res.id;
toast.success("Video uploaded");
toast.success($_("admin.video_form.video_uploaded"));
} catch {
toast.error("Video upload failed");
toast.error($_("admin.video_form.video_upload_failed"));
}
}
@@ -73,10 +74,10 @@
uploadDate: uploadDate || undefined,
});
await setVideoModels(data.video.id, selectedModelIds);
toast.success("Video updated");
toast.success($_("admin.video_form.update_success"));
goto("/admin/videos");
} catch (e: any) {
toast.error(e?.message ?? "Failed to update video");
toast.error(e?.message ?? $_("admin.video_form.update_error"));
} finally {
saving = false;
}
@@ -86,30 +87,30 @@
<div class="p-3 sm:p-6 max-w-2xl">
<div class="flex items-center gap-4 mb-6">
<Button variant="ghost" href="/admin/videos" size="sm">
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>Back
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
</Button>
<h1 class="text-2xl font-bold">Edit video</h1>
<h1 class="text-2xl font-bold">{$_("admin.video_form.edit_title")}</h1>
</div>
<div class="space-y-5">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="title">Title *</Label>
<Input id="title" bind:value={title} placeholder="Video title" />
<Label for="title">{$_("admin.common.title_field")}</Label>
<Input id="title" bind:value={title} placeholder={$_("admin.video_form.title_placeholder")} />
</div>
<div class="space-y-1.5">
<Label for="slug">Slug *</Label>
<Input id="slug" bind:value={slug} placeholder="video-slug" />
<Label for="slug">{$_("admin.common.slug_field")}</Label>
<Input id="slug" bind:value={slug} placeholder={$_("admin.video_form.slug_placeholder")} />
</div>
</div>
<div class="space-y-1.5">
<Label for="description">Description</Label>
<Textarea id="description" bind:value={description} rows={3} />
<Label for="description">{$_("admin.video_form.description")}</Label>
<Textarea id="description" bind:value={description} placeholder={$_("admin.video_form.description_placeholder")} rows={3} />
</div>
<div class="space-y-1.5">
<Label>Cover image</Label>
<Label>{$_("admin.common.cover_image")}</Label>
{#if imageId}
<img
src={getAssetUrl(imageId, "thumbnail")}
@@ -121,43 +122,43 @@
</div>
<div class="space-y-1.5">
<Label>Video file</Label>
<Label>{$_("admin.video_form.video_file")}</Label>
{#if movieId}
<p class="text-xs text-muted-foreground mb-1">Current file: {movieId}</p>
<p class="text-xs text-muted-foreground mb-1">{$_("admin.video_form.current_file", { values: { id: movieId } })}</p>
{/if}
<FileDropZone accept="video/*" maxFileSize={2000 * MEGABYTE} onUpload={handleVideoUpload} />
</div>
<div class="space-y-1.5">
<Label>Tags</Label>
<Label>{$_("admin.common.tags")}</Label>
<TagsInput bind:value={tags} />
</div>
<div class="space-y-1.5">
<Label for="uploadDate">Publish date</Label>
<Label for="uploadDate">{$_("admin.common.publish_date")}</Label>
<Input id="uploadDate" type="datetime-local" bind:value={uploadDate} />
</div>
<div class="flex gap-6">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={premium} class="rounded" />
<span class="text-sm">Premium</span>
<span class="text-sm">{$_("admin.common.premium")}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={featured} class="rounded" />
<span class="text-sm">Featured</span>
<span class="text-sm">{$_("admin.common.featured")}</span>
</label>
</div>
{#if data.models.length > 0}
<div class="space-y-1.5">
<Label>Models</Label>
<Label>{$_("admin.video_form.models")}</Label>
<Select type="multiple" bind:value={selectedModelIds}>
<SelectTrigger class="w-full">
{#if selectedModelIds.length}
{selectedModelIds.length} model{selectedModelIds.length > 1 ? "s" : ""} selected
{$_("admin.video_form.models_selected", { values: { count: selectedModelIds.length } })}
{:else}
<span class="text-muted-foreground">No models</span>
<span class="text-muted-foreground">{$_("admin.video_form.no_models")}</span>
{/if}
</SelectTrigger>
<SelectContent>
@@ -176,9 +177,9 @@
<div class="flex gap-3 pt-2">
<Button onclick={handleSubmit} disabled={saving}>
{saving ? "Saving" : "Save changes"}
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
</Button>
<Button variant="outline" href="/admin/videos">Cancel</Button>
<Button variant="outline" href="/admin/videos">{$_("common.cancel")}</Button>
</div>
</div>
</div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { toast } from "svelte-sonner";
import { _ } from "svelte-i18n";
import { createVideo, setVideoModels, uploadFile } from "$lib/services";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
@@ -38,9 +39,9 @@
try {
const res = await uploadFile(fd);
imageId = res.id;
toast.success("Cover image uploaded");
toast.success($_("admin.video_form.cover_uploaded"));
} catch {
toast.error("Image upload failed");
toast.error($_("admin.common.image_upload_failed"));
}
}
@@ -52,9 +53,9 @@
try {
const res = await uploadFile(fd);
movieId = res.id;
toast.success("Video uploaded");
toast.success($_("admin.video_form.video_uploaded"));
} catch {
toast.error("Video upload failed");
toast.error($_("admin.video_form.video_upload_failed"));
}
}
@@ -66,7 +67,7 @@
async function handleSubmit() {
if (!title || !slug) {
toast.error("Title and slug are required");
toast.error($_("admin.common.title_slug_required"));
return;
}
saving = true;
@@ -85,10 +86,10 @@
if (selectedModelIds.length > 0) {
await setVideoModels(video.id, selectedModelIds);
}
toast.success("Video created");
toast.success($_("admin.video_form.create_success"));
goto("/admin/videos");
} catch (e: any) {
toast.error(e?.message ?? "Failed to create video");
toast.error(e?.message ?? $_("admin.video_form.create_error"));
} finally {
saving = false;
}
@@ -98,70 +99,70 @@
<div class="p-3 sm:p-6 max-w-2xl">
<div class="flex items-center gap-4 mb-6">
<Button variant="ghost" href="/admin/videos" size="sm">
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>Back
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
</Button>
<h1 class="text-2xl font-bold">New video</h1>
<h1 class="text-2xl font-bold">{$_("admin.video_form.new_title")}</h1>
</div>
<div class="space-y-5">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="title">Title *</Label>
<Label for="title">{$_("admin.common.title_field")}</Label>
<Input
id="title"
bind:value={title}
oninput={() => {
if (!slug) slug = generateSlug(title);
}}
placeholder="Video title"
placeholder={$_("admin.video_form.title_placeholder")}
/>
</div>
<div class="space-y-1.5">
<Label for="slug">Slug *</Label>
<Input id="slug" bind:value={slug} placeholder="video-slug" />
<Label for="slug">{$_("admin.common.slug_field")}</Label>
<Input id="slug" bind:value={slug} placeholder={$_("admin.video_form.slug_placeholder")} />
</div>
</div>
<div class="space-y-1.5">
<Label for="description">Description</Label>
<Label for="description">{$_("admin.video_form.description")}</Label>
<Textarea
id="description"
bind:value={description}
placeholder="Optional description"
placeholder={$_("admin.video_form.description_placeholder")}
rows={3}
/>
</div>
<div class="space-y-1.5">
<Label>Cover image</Label>
<Label>{$_("admin.common.cover_image")}</Label>
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
{#if imageId}<p class="text-xs text-green-600 mt-1">Image uploaded ✓</p>{/if}
{#if imageId}<p class="text-xs text-green-600 mt-1">{$_("admin.common.image_uploaded")}</p>{/if}
</div>
<div class="space-y-1.5">
<Label>Video file</Label>
<Label>{$_("admin.video_form.video_file")}</Label>
<FileDropZone accept="video/*" maxFileSize={2000 * MEGABYTE} onUpload={handleVideoUpload} />
{#if movieId}<p class="text-xs text-green-600 mt-1">Video uploaded ✓</p>{/if}
{#if movieId}<p class="text-xs text-green-600 mt-1">{$_("admin.video_form.video_uploaded")}</p>{/if}
</div>
<div class="space-y-1.5">
<Label>Tags</Label>
<Label>{$_("admin.common.tags")}</Label>
<TagsInput bind:value={tags} />
</div>
<div class="space-y-1.5">
<Label for="uploadDate">Publish date</Label>
<Label for="uploadDate">{$_("admin.common.publish_date")}</Label>
<Input id="uploadDate" type="datetime-local" bind:value={uploadDate} />
</div>
<div class="flex gap-6">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={premium} class="rounded" />
<span class="text-sm">Premium</span>
<span class="text-sm">{$_("admin.common.premium")}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={featured} class="rounded" />
<span class="text-sm">Featured</span>
<span class="text-sm">{$_("admin.common.featured")}</span>
</label>
</div>
@@ -188,9 +189,9 @@
<div class="flex gap-3 pt-2">
<Button onclick={handleSubmit} disabled={saving}>
{saving ? "Creating" : "Create video"}
{saving ? $_("admin.common.creating") : $_("admin.video_form.create")}
</Button>
<Button variant="outline" href="/admin/videos">Cancel</Button>
<Button variant="outline" href="/admin/videos">{$_("common.cancel")}</Button>
</div>
</div>
</div>