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

@@ -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>