feat(i18n): add Paraglide i18n with 5 locales (v2.3.22)
Some checks failed
CI / Backend (pull_request) Successful in 41s
CI / UI (pull_request) Failing after 27s
CI / UI (push) Failing after 27s
CI / Backend (push) Successful in 48s
Release / Test backend (push) Successful in 23s
Release / Check ui (push) Failing after 33s
Release / Docker / ui (push) Has been skipped
Release / Docker / caddy (push) Failing after 52s
Release / Docker / backend (push) Failing after 11s
Release / Docker / runner (push) Successful in 1m51s
Release / Gitea Release (push) Has been skipped
Some checks failed
CI / Backend (pull_request) Successful in 41s
CI / UI (pull_request) Failing after 27s
CI / UI (push) Failing after 27s
CI / Backend (push) Successful in 48s
Release / Test backend (push) Successful in 23s
Release / Check ui (push) Failing after 33s
Release / Docker / ui (push) Has been skipped
Release / Docker / caddy (push) Failing after 52s
Release / Docker / backend (push) Failing after 11s
Release / Docker / runner (push) Successful in 1m51s
Release / Gitea Release (push) Has been skipped
- Install @inlang/paraglide-js v2.15.1; configure project.inlang settings - Add en/ru/id/pt-BR/fr message catalogues (~140 keys each) - Wire paraglideVitePlugin in vite.config.ts, reroute hook in hooks.ts, and paraglideHandle middleware in hooks.server.ts - Migrate all routes and shared components to use m.*() message calls - Fix duplicate onMount body in chapters/[n]/+page.svelte - Build passes; svelte-check: 0 errors, 3 pre-existing warnings
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
|
||||
const internalLinks = [
|
||||
{ href: '/admin/scrape', label: 'Scrape' },
|
||||
@@ -27,7 +28,7 @@
|
||||
<aside class="w-48 shrink-0 border-r border-(--color-border) px-3 py-6 flex flex-col gap-6">
|
||||
<!-- Internal pages -->
|
||||
<div>
|
||||
<p class="px-2 mb-2 text-xs font-semibold text-(--color-muted) uppercase tracking-widest">Pages</p>
|
||||
<p class="px-2 mb-2 text-xs font-semibold text-(--color-muted) uppercase tracking-widest">{m.admin_pages_label()}</p>
|
||||
<nav class="flex flex-col gap-0.5">
|
||||
{#each internalLinks as link}
|
||||
<a
|
||||
@@ -45,7 +46,7 @@
|
||||
|
||||
<!-- External tools -->
|
||||
<div>
|
||||
<p class="px-2 mb-2 text-xs font-semibold text-(--color-muted) uppercase tracking-widest">Tools</p>
|
||||
<p class="px-2 mb-2 text-xs font-semibold text-(--color-muted) uppercase tracking-widest">{m.admin_tools_label()}</p>
|
||||
<nav class="flex flex-col gap-0.5">
|
||||
{#each externalLinks as link}
|
||||
<a
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import type { PageData } from './$types';
|
||||
import type { AudioJob, AudioCacheEntry } from '$lib/server/pocketbase';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
@@ -94,13 +95,13 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Audio — libnovel admin</title>
|
||||
<title>{m.admin_audio_page_title()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-(--color-text)">Audio</h1>
|
||||
<h1 class="text-2xl font-bold text-(--color-text)">{m.admin_audio_heading()}</h1>
|
||||
<p class="text-(--color-muted) text-sm mt-1">
|
||||
{stats.total} job{stats.total !== 1 ? 's' : ''} ·
|
||||
<span class="text-green-400">{stats.done} done</span>
|
||||
@@ -142,13 +143,13 @@
|
||||
<input
|
||||
type="search"
|
||||
bind:value={jobsQ}
|
||||
placeholder="Filter by slug, voice or status…"
|
||||
placeholder={m.admin_audio_filter_jobs()}
|
||||
class="w-full max-w-sm bg-(--color-surface-2) border border-(--color-border) rounded-lg px-3 py-2 text-(--color-text) text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-(--color-brand)"
|
||||
/>
|
||||
|
||||
{#if filteredJobs.length === 0}
|
||||
<p class="text-(--color-muted) text-sm py-8 text-center">
|
||||
{jobsQ.trim() ? 'No matching jobs.' : 'No audio jobs yet.'}
|
||||
{jobsQ.trim() ? m.admin_audio_no_matching_jobs() : m.admin_audio_no_jobs()}
|
||||
</p>
|
||||
{:else}
|
||||
<!-- Desktop table -->
|
||||
@@ -218,13 +219,13 @@
|
||||
<input
|
||||
type="search"
|
||||
bind:value={cacheQ}
|
||||
placeholder="Filter by slug, chapter or voice…"
|
||||
placeholder={m.admin_audio_filter_cache()}
|
||||
class="w-full max-w-sm bg-(--color-surface-2) border border-(--color-border) rounded-lg px-3 py-2 text-(--color-text) text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-(--color-brand)"
|
||||
/>
|
||||
|
||||
{#if filteredCache.length === 0}
|
||||
<p class="text-(--color-muted) text-sm py-8 text-center">
|
||||
{cacheQ.trim() ? 'No results.' : 'Audio cache is empty.'}
|
||||
{cacheQ.trim() ? m.admin_audio_no_cache_results() : m.admin_audio_cache_empty()}
|
||||
</p>
|
||||
{:else}
|
||||
<!-- Desktop table -->
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
@@ -11,19 +12,19 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Changelog — libnovel admin</title>
|
||||
<title>{m.admin_changelog_page_title()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="space-y-6 max-w-2xl">
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="text-xl font-semibold text-(--color-text) flex-1">Changelog</h1>
|
||||
<h1 class="text-xl font-semibold text-(--color-text) flex-1">{m.admin_changelog_heading()}</h1>
|
||||
<a
|
||||
href="https://gitea.kalekber.cc/kamil/libnovel/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-xs text-(--color-muted) hover:text-(--color-text) transition-colors flex items-center gap-1"
|
||||
>
|
||||
Gitea releases
|
||||
{m.admin_changelog_gitea()}
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
@@ -32,9 +33,9 @@
|
||||
</div>
|
||||
|
||||
{#if data.error}
|
||||
<p class="text-sm text-(--color-danger)">Could not load releases: {data.error}</p>
|
||||
<p class="text-sm text-(--color-danger)">{m.admin_changelog_load_error({ error: data.error })}</p>
|
||||
{:else if data.releases.length === 0}
|
||||
<p class="text-sm text-(--color-muted) py-8 text-center">No releases found.</p>
|
||||
<p class="text-sm text-(--color-muted) py-8 text-center">{m.admin_changelog_no_releases()}</p>
|
||||
{:else}
|
||||
<div class="space-y-0 divide-y divide-(--color-border) border border-(--color-border) rounded-xl overflow-hidden">
|
||||
{#each data.releases as release}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import type { PageData } from './$types';
|
||||
import type { ScrapingTask } from '$lib/server/pocketbase';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
@@ -228,15 +229,15 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Scrape tasks — libnovel admin</title>
|
||||
<title>{m.admin_scrape_page_title()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<h1 class="text-xl font-semibold text-(--color-text) flex-1">Scrape</h1>
|
||||
<h1 class="text-xl font-semibold text-(--color-text) flex-1">{m.admin_scrape_heading()}</h1>
|
||||
<span class="text-xs {running ? 'text-(--color-brand) animate-pulse' : 'text-green-500'}">
|
||||
● {running ? 'Running' : 'Idle'}
|
||||
● {running ? m.admin_scrape_status_running() : m.admin_scrape_status_idle()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -244,20 +245,20 @@
|
||||
<div class="divide-y divide-(--color-border) border border-(--color-border) rounded-xl overflow-hidden">
|
||||
<!-- Full catalogue -->
|
||||
<div class="flex items-center gap-4 px-4 py-3 bg-(--color-surface)">
|
||||
<span class="text-sm text-(--color-muted) w-36 shrink-0">Full catalogue</span>
|
||||
<span class="text-sm text-(--color-muted) w-36 shrink-0">{m.admin_scrape_full_catalogue()}</span>
|
||||
<button
|
||||
onclick={triggerCatalogueScrape}
|
||||
disabled={running || cataloguing}
|
||||
class="px-3 py-1.5 rounded-md bg-(--color-brand) text-(--color-surface) font-semibold text-xs hover:bg-(--color-brand-dim) transition-colors disabled:opacity-50"
|
||||
>
|
||||
{cataloguing ? 'Queuing…' : running ? 'Running…' : 'Start scrape'}
|
||||
{cataloguing ? m.admin_scrape_queuing() : running ? m.admin_scrape_running() : m.admin_scrape_start()}
|
||||
</button>
|
||||
{#if catalogueError}<span class="text-xs text-(--color-danger)">{catalogueError}</span>{/if}
|
||||
</div>
|
||||
|
||||
<!-- Single book -->
|
||||
<div id="book-form" class="flex items-center gap-3 px-4 py-3 bg-(--color-surface)">
|
||||
<span class="text-sm text-(--color-muted) w-36 shrink-0">Single book</span>
|
||||
<span class="text-sm text-(--color-muted) w-36 shrink-0">{m.admin_scrape_single_book()}</span>
|
||||
<input
|
||||
type="url"
|
||||
bind:value={scrapeUrl}
|
||||
@@ -269,14 +270,14 @@
|
||||
disabled={!scrapeUrl.trim() || running || scraping}
|
||||
class="shrink-0 px-3 py-1.5 rounded-md bg-(--color-surface-3) text-(--color-text) font-medium text-xs hover:bg-zinc-600 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{scraping ? 'Queuing…' : 'Scrape'}
|
||||
{scraping ? m.admin_scrape_queuing() : m.admin_scrape_submit()}
|
||||
</button>
|
||||
{#if scrapeError}<span class="text-xs text-(--color-danger)">{scrapeError}</span>{/if}
|
||||
</div>
|
||||
|
||||
<!-- Range scrape -->
|
||||
<div id="range-form" class="flex items-center gap-3 px-4 py-3 bg-(--color-surface) flex-wrap">
|
||||
<span class="text-sm text-(--color-muted) w-36 shrink-0">Chapter range</span>
|
||||
<span class="text-sm text-(--color-muted) w-36 shrink-0">{m.admin_scrape_range()}</span>
|
||||
<input
|
||||
type="url"
|
||||
bind:value={rangeUrl}
|
||||
@@ -302,14 +303,14 @@
|
||||
disabled={!rangeUrl.trim() || rangeFrom === null || running || ranging}
|
||||
class="shrink-0 px-3 py-1.5 rounded-md bg-(--color-surface-3) text-(--color-text) font-medium text-xs hover:bg-zinc-600 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{ranging ? 'Queuing…' : 'Go'}
|
||||
{ranging ? m.admin_scrape_queuing() : 'Go'}
|
||||
</button>
|
||||
{#if rangeError}<span class="text-xs text-(--color-danger) w-full pl-40">{rangeError}</span>{/if}
|
||||
</div>
|
||||
|
||||
<!-- Quick genre chips -->
|
||||
<div class="flex items-center gap-3 px-4 py-3 bg-(--color-surface) flex-wrap">
|
||||
<span class="text-sm text-(--color-muted) w-36 shrink-0">Quick genres</span>
|
||||
<span class="text-sm text-(--color-muted) w-36 shrink-0">{m.admin_scrape_quick_genres()}</span>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{#each quickScrapes as qs}
|
||||
<button
|
||||
@@ -334,18 +335,18 @@
|
||||
<!-- Tasks table -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<h2 class="text-sm font-semibold text-(--color-muted) flex-1 uppercase tracking-widest">Task history</h2>
|
||||
<h2 class="text-sm font-semibold text-(--color-muted) flex-1 uppercase tracking-widest">{m.admin_scrape_task_history()}</h2>
|
||||
<input
|
||||
type="search"
|
||||
bind:value={q}
|
||||
placeholder="Filter by kind, status or URL…"
|
||||
placeholder={m.admin_scrape_filter_placeholder()}
|
||||
class="w-full max-w-xs bg-(--color-surface-2) border border-(--color-border) rounded-lg px-3 py-2 text-(--color-text) text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-(--color-brand)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if filtered.length === 0}
|
||||
<p class="text-(--color-muted) text-sm py-8 text-center">
|
||||
{q.trim() ? 'No matching tasks.' : 'No scrape tasks yet.'}
|
||||
{q.trim() ? m.admin_scrape_no_matching() : m.admin_tasks_empty()}
|
||||
</p>
|
||||
{:else}
|
||||
<!-- Desktop table -->
|
||||
@@ -393,7 +394,7 @@
|
||||
disabled={cancellingIds.has(task.id)}
|
||||
class="px-2 py-1 rounded text-xs font-medium bg-(--color-surface-3) text-(--color-text) hover:bg-red-900 hover:text-red-300 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{cancellingIds.has(task.id) ? 'Cancelling…' : 'Cancel'}
|
||||
{cancellingIds.has(task.id) ? 'Cancelling…' : m.admin_scrape_cancel()}
|
||||
</button>
|
||||
{/if}
|
||||
{#if task.kind === 'book_range' && task.status !== 'pending' && task.status !== 'running' && (task.chapters_scraped ?? 0) > 0}
|
||||
@@ -461,7 +462,7 @@
|
||||
disabled={cancellingIds.has(task.id)}
|
||||
class="flex-1 px-3 py-1.5 rounded-lg text-xs font-medium bg-(--color-surface-3) text-(--color-text) hover:bg-red-900 hover:text-red-300 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{cancellingIds.has(task.id) ? 'Cancelling…' : 'Cancel task'}
|
||||
{cancellingIds.has(task.id) ? 'Cancelling…' : m.admin_scrape_cancel()} task
|
||||
</button>
|
||||
{/if}
|
||||
{#if task.kind === 'book_range' && task.status !== 'pending' && task.status !== 'running' && (task.chapters_scraped ?? 0) > 0}
|
||||
|
||||
Reference in New Issue
Block a user