Compare commits

...

5 Commits

Author SHA1 Message Date
Admin
bfd0ad8fb7 fix: chapter content vanishes — replace stale untrack snapshots with $derived
Some checks failed
Release / Test backend (push) Successful in 44s
Release / Check ui (push) Successful in 46s
Release / Docker / caddy (push) Successful in 48s
Release / Docker / backend (push) Successful in 2m53s
Release / Docker / runner (push) Successful in 2m58s
Release / Docker / ui (push) Successful in 4m58s
Release / Gitea Release (push) Has been cancelled
html and fetchingContent were captured with untrack() at mount time.
When SvelteKit re-ran the page load (triggered by the layout's settings PUT),
data.html updated but html stayed stale. The {#key} block in the layout then
destroyed and recreated the component, and on remount data.html was momentarily
empty so html became '' and the live-scrape fallback ran unnecessarily.

Fix:
- html is now $derived(scrapedHtml || data.html || '') — always tracks load
- scrapedHtml is a separate $state only set by the live-scrape fallback
- fetchingContent starts false; the fallback sets it true only when actually fetching
- translationStatus/translatingLang: dropped untrack() so they also react to re-runs
- Removed unused untrack import
2026-04-04 20:26:43 +05:00
Admin
4b7fcf432b fix: pass POLAR_API_TOKEN and POLAR_WEBHOOK_SECRET to ui container
All checks were successful
Release / Test backend (push) Successful in 39s
Release / Check ui (push) Successful in 47s
Release / Docker / caddy (push) Successful in 45s
Release / Docker / backend (push) Successful in 2m35s
Release / Docker / runner (push) Successful in 2m36s
Release / Docker / ui (push) Successful in 2m25s
Release / Gitea Release (push) Successful in 43s
Both vars were present in Doppler but never injected into the ui service
environment, causing all checkout requests to fail with 500 and webhooks
to be silently rejected.
2026-04-04 20:18:19 +05:00
Admin
c4a0256f6e feat: /subscribe pricing page + Pro nav link + fix checkout token scope
All checks were successful
Release / Test backend (push) Successful in 40s
Release / Check ui (push) Successful in 46s
Release / Docker / caddy (push) Successful in 42s
Release / Docker / backend (push) Successful in 2m35s
Release / Docker / runner (push) Successful in 2m38s
Release / Docker / ui (push) Successful in 2m12s
Release / Gitea Release (push) Successful in 41s
- Add /subscribe route with hero, benefits list, and pricing cards
  (annual featured with Save 33% badge, monthly secondary)
- Add 'Pro' link in nav for non-Pro users
- Add 'See plans' link in profile subscription section
- i18n keys across en/fr/id/pt/ru for all subscribe strings

Note: checkout still requires POLAR_API_TOKEN with checkouts:write scope.
Regenerate the token at polar.sh to fix the 502 error on subscribe buttons.
2026-04-04 20:12:24 +05:00
Admin
18f490f790 feat: admin controls on book detail page (cover/desc/chapter-names/audio TTS)
All checks were successful
Release / Test backend (push) Successful in 48s
Release / Check ui (push) Successful in 44s
Release / Docker / caddy (push) Successful in 44s
Release / Docker / backend (push) Successful in 3m22s
Release / Docker / runner (push) Successful in 2m51s
Release / Docker / ui (push) Successful in 3m17s
Release / Gitea Release (push) Successful in 1m12s
Add 5 admin sections to /books/[slug] for admins:
- Book cover generation (CF AI image-gen with preview + save)
- Chapter cover generation (chapter number input + preview)
- Description regeneration (preview + apply/discard)
- Chapter names generation (preview table + apply/discard)
- Audio TTS bulk enqueue (voice selector, chapter range, cancel)

Also adds /api/admin/audio/bulk and /api/admin/audio/cancel-bulk proxy routes,
and all i18n keys across en/fr/id/pt/ru.
2026-04-04 19:57:16 +05:00
Admin
6456e8cf5d perf: skip ffmpeg transcode for PocketTTS streaming — use WAV directly
All checks were successful
Release / Test backend (push) Successful in 42s
Release / Check ui (push) Successful in 46s
Release / Docker / caddy (push) Successful in 41s
Release / Docker / backend (push) Successful in 2m46s
Release / Docker / runner (push) Successful in 3m12s
Release / Docker / ui (push) Successful in 2m19s
Release / Gitea Release (push) Successful in 36s
PocketTTS emits 16-bit PCM WAV (16 kHz mono). WAV is natively supported
on all browsers including iOS/macOS Safari, so the ffmpeg MP3 transcode
is unnecessary for the streaming path.

Using format=wav for PocketTTS voices eliminates the ffmpeg subprocess
startup delay (~200–400 ms) and a pipeline stage, giving lower latency
to first audio frame. Kokoro and CF AI continue using MP3 (they output
MP3 natively or via the OpenAI-compatible endpoint).

The runner (MinIO storage) is unaffected — it still stores MP3 for
space efficiency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 19:55:05 +05:00
55 changed files with 2768 additions and 59 deletions

View File

@@ -310,6 +310,9 @@ services:
GOOGLE_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET}"
GITHUB_CLIENT_ID: "${GITHUB_CLIENT_ID}"
GITHUB_CLIENT_SECRET: "${GITHUB_CLIENT_SECRET}"
# Polar (subscriptions)
POLAR_API_TOKEN: "${POLAR_API_TOKEN}"
POLAR_WEBHOOK_SECRET: "${POLAR_WEBHOOK_SECRET}"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
interval: 15s

View File

@@ -302,6 +302,24 @@
"book_detail_range_queuing": "Queuing\u2026",
"book_detail_scrape_range": "Scrape range",
"book_detail_admin": "Admin",
"book_detail_admin_book_cover": "Book Cover",
"book_detail_admin_chapter_cover": "Chapter Cover",
"book_detail_admin_chapter_n": "Chapter #",
"book_detail_admin_description": "Description",
"book_detail_admin_chapter_names": "Chapter Names",
"book_detail_admin_audio_tts": "Audio TTS",
"book_detail_admin_voice": "Voice",
"book_detail_admin_generate": "Generate",
"book_detail_admin_save_cover": "Save Cover",
"book_detail_admin_saving": "Saving…",
"book_detail_admin_saved": "Saved",
"book_detail_admin_apply": "Apply",
"book_detail_admin_applying": "Applying…",
"book_detail_admin_applied": "Applied",
"book_detail_admin_discard": "Discard",
"book_detail_admin_enqueue_audio": "Enqueue Audio",
"book_detail_admin_cancel_audio": "Cancel",
"book_detail_admin_enqueued": "Enqueued {enqueued}, skipped {skipped}",
"book_detail_scraping_progress": "Fetching the first 20 chapters. This page will refresh automatically.",
"book_detail_scraping_home": "\u2190 Home",
"book_detail_rescrape_book": "Rescrape book",
@@ -348,6 +366,26 @@
"profile_upgrade_monthly": "Monthly \u2014 $6 / mo",
"profile_upgrade_annual": "Annual \u2014 $48 / yr",
"profile_free_limits": "Free plan: 3 audio chapters per day, English reading only.",
"subscribe_page_title": "Go Pro \u2014 libnovel",
"subscribe_heading": "Read more. Listen more.",
"subscribe_subheading": "Upgrade to Pro and unlock the full libnovel experience.",
"subscribe_monthly_label": "Monthly",
"subscribe_monthly_price": "$6",
"subscribe_monthly_period": "per month",
"subscribe_annual_label": "Annual",
"subscribe_annual_price": "$48",
"subscribe_annual_period": "per year",
"subscribe_annual_save": "Save 33%",
"subscribe_cta_monthly": "Start monthly plan",
"subscribe_cta_annual": "Start annual plan",
"subscribe_already_pro": "You already have a Pro subscription.",
"subscribe_manage": "Manage subscription",
"subscribe_benefit_audio": "Unlimited audio chapters per day",
"subscribe_benefit_voices": "Voice selection across all TTS engines",
"subscribe_benefit_translation": "Read in French, Indonesian, Portuguese, and Russian",
"subscribe_benefit_downloads": "Download chapters for offline listening",
"subscribe_login_prompt": "Sign in to subscribe",
"subscribe_login_cta": "Sign in",
"user_currently_reading": "Currently Reading",
"user_library_count": "Library ({n})",

View File

@@ -302,6 +302,24 @@
"book_detail_range_queuing": "En file d'attente…",
"book_detail_scrape_range": "Plage d'extraction",
"book_detail_admin": "Admin",
"book_detail_admin_book_cover": "Couverture du livre",
"book_detail_admin_chapter_cover": "Couverture du chapitre",
"book_detail_admin_chapter_n": "Chapitre n°",
"book_detail_admin_description": "Description",
"book_detail_admin_chapter_names": "Noms des chapitres",
"book_detail_admin_audio_tts": "Audio TTS",
"book_detail_admin_voice": "Voix",
"book_detail_admin_generate": "Générer",
"book_detail_admin_save_cover": "Enregistrer la couverture",
"book_detail_admin_saving": "Enregistrement…",
"book_detail_admin_saved": "Enregistré",
"book_detail_admin_apply": "Appliquer",
"book_detail_admin_applying": "Application…",
"book_detail_admin_applied": "Appliqué",
"book_detail_admin_discard": "Ignorer",
"book_detail_admin_enqueue_audio": "Mettre en file audio",
"book_detail_admin_cancel_audio": "Annuler",
"book_detail_admin_enqueued": "{enqueued} en file, {skipped} ignorés",
"book_detail_scraping_progress": "Récupération des 20 premiers chapitres. Cette page sera actualisée automatiquement.",
"book_detail_scraping_home": "← Accueil",
"book_detail_rescrape_book": "Réextraire le livre",
@@ -348,6 +366,26 @@
"profile_upgrade_monthly": "Mensuel — 6 $ / mois",
"profile_upgrade_annual": "Annuel — 48 $ / an",
"profile_free_limits": "Plan gratuit : 3 chapitres audio par jour, lecture en anglais uniquement.",
"subscribe_page_title": "Passer Pro \u2014 libnovel",
"subscribe_heading": "Lisez plus. Écoutez plus.",
"subscribe_subheading": "Passez Pro et débloquez l'expérience libnovel complète.",
"subscribe_monthly_label": "Mensuel",
"subscribe_monthly_price": "6 $",
"subscribe_monthly_period": "par mois",
"subscribe_annual_label": "Annuel",
"subscribe_annual_price": "48 $",
"subscribe_annual_period": "par an",
"subscribe_annual_save": "Économisez 33 %",
"subscribe_cta_monthly": "Commencer le plan mensuel",
"subscribe_cta_annual": "Commencer le plan annuel",
"subscribe_already_pro": "Vous avez déjà un abonnement Pro.",
"subscribe_manage": "Gérer l'abonnement",
"subscribe_benefit_audio": "Chapitres audio illimités par jour",
"subscribe_benefit_voices": "Sélection de voix pour tous les moteurs TTS",
"subscribe_benefit_translation": "Lire en français, indonésien, portugais et russe",
"subscribe_benefit_downloads": "Télécharger des chapitres pour une écoute hors ligne",
"subscribe_login_prompt": "Connectez-vous pour vous abonner",
"subscribe_login_cta": "Se connecter",
"user_currently_reading": "En cours de lecture",
"user_library_count": "Bibliothèque ({n})",

View File

@@ -302,6 +302,24 @@
"book_detail_range_queuing": "Mengantri…",
"book_detail_scrape_range": "Rentang scrape",
"book_detail_admin": "Admin",
"book_detail_admin_book_cover": "Sampul Buku",
"book_detail_admin_chapter_cover": "Sampul Bab",
"book_detail_admin_chapter_n": "Bab #",
"book_detail_admin_description": "Deskripsi",
"book_detail_admin_chapter_names": "Nama Bab",
"book_detail_admin_audio_tts": "Audio TTS",
"book_detail_admin_voice": "Suara",
"book_detail_admin_generate": "Buat",
"book_detail_admin_save_cover": "Simpan Sampul",
"book_detail_admin_saving": "Menyimpan…",
"book_detail_admin_saved": "Tersimpan",
"book_detail_admin_apply": "Terapkan",
"book_detail_admin_applying": "Menerapkan…",
"book_detail_admin_applied": "Diterapkan",
"book_detail_admin_discard": "Buang",
"book_detail_admin_enqueue_audio": "Antre Audio",
"book_detail_admin_cancel_audio": "Batal",
"book_detail_admin_enqueued": "Diantre {enqueued}, dilewati {skipped}",
"book_detail_scraping_progress": "Mengambil 20 bab pertama. Halaman ini akan dimuat ulang otomatis.",
"book_detail_scraping_home": "← Beranda",
"book_detail_rescrape_book": "Scrape ulang buku",
@@ -348,6 +366,26 @@
"profile_upgrade_monthly": "Bulanan — $6 / bln",
"profile_upgrade_annual": "Tahunan — $48 / thn",
"profile_free_limits": "Paket gratis: 3 bab audio per hari, hanya bahasa Inggris.",
"subscribe_page_title": "Jadi Pro \u2014 libnovel",
"subscribe_heading": "Baca lebih. Dengarkan lebih.",
"subscribe_subheading": "Tingkatkan ke Pro dan buka pengalaman libnovel sepenuhnya.",
"subscribe_monthly_label": "Bulanan",
"subscribe_monthly_price": "$6",
"subscribe_monthly_period": "per bulan",
"subscribe_annual_label": "Tahunan",
"subscribe_annual_price": "$48",
"subscribe_annual_period": "per tahun",
"subscribe_annual_save": "Hemat 33%",
"subscribe_cta_monthly": "Mulai paket bulanan",
"subscribe_cta_annual": "Mulai paket tahunan",
"subscribe_already_pro": "Anda sudah berlangganan Pro.",
"subscribe_manage": "Kelola langganan",
"subscribe_benefit_audio": "Bab audio tak terbatas per hari",
"subscribe_benefit_voices": "Pilihan suara untuk semua mesin TTS",
"subscribe_benefit_translation": "Baca dalam bahasa Prancis, Indonesia, Portugis, dan Rusia",
"subscribe_benefit_downloads": "Unduh bab untuk didengarkan secara offline",
"subscribe_login_prompt": "Masuk untuk berlangganan",
"subscribe_login_cta": "Masuk",
"user_currently_reading": "Sedang Dibaca",
"user_library_count": "Perpustakaan ({n})",

View File

@@ -302,6 +302,24 @@
"book_detail_range_queuing": "Na fila…",
"book_detail_scrape_range": "Intervalo de extração",
"book_detail_admin": "Admin",
"book_detail_admin_book_cover": "Capa do Livro",
"book_detail_admin_chapter_cover": "Capa do Capítulo",
"book_detail_admin_chapter_n": "Capítulo nº",
"book_detail_admin_description": "Descrição",
"book_detail_admin_chapter_names": "Nomes dos Capítulos",
"book_detail_admin_audio_tts": "Áudio TTS",
"book_detail_admin_voice": "Voz",
"book_detail_admin_generate": "Gerar",
"book_detail_admin_save_cover": "Salvar Capa",
"book_detail_admin_saving": "Salvando…",
"book_detail_admin_saved": "Salvo",
"book_detail_admin_apply": "Aplicar",
"book_detail_admin_applying": "Aplicando…",
"book_detail_admin_applied": "Aplicado",
"book_detail_admin_discard": "Descartar",
"book_detail_admin_enqueue_audio": "Enfileirar Áudio",
"book_detail_admin_cancel_audio": "Cancelar",
"book_detail_admin_enqueued": "{enqueued} enfileirados, {skipped} ignorados",
"book_detail_scraping_progress": "Buscando os primeiros 20 capítulos. Esta página será atualizada automaticamente.",
"book_detail_scraping_home": "← Início",
"book_detail_rescrape_book": "Reextrair livro",
@@ -348,6 +366,26 @@
"profile_upgrade_monthly": "Mensal — $6 / mês",
"profile_upgrade_annual": "Anual — $48 / ano",
"profile_free_limits": "Plano gratuito: 3 capítulos de áudio por dia, somente inglês.",
"subscribe_page_title": "Seja Pro \u2014 libnovel",
"subscribe_heading": "Leia mais. Ouça mais.",
"subscribe_subheading": "Torne-se Pro e desbloqueie a experiência completa do libnovel.",
"subscribe_monthly_label": "Mensal",
"subscribe_monthly_price": "$6",
"subscribe_monthly_period": "por mês",
"subscribe_annual_label": "Anual",
"subscribe_annual_price": "$48",
"subscribe_annual_period": "por ano",
"subscribe_annual_save": "Economize 33%",
"subscribe_cta_monthly": "Começar plano mensal",
"subscribe_cta_annual": "Começar plano anual",
"subscribe_already_pro": "Você já tem uma assinatura Pro.",
"subscribe_manage": "Gerenciar assinatura",
"subscribe_benefit_audio": "Capítulos de áudio ilimitados por dia",
"subscribe_benefit_voices": "Seleção de voz para todos os mecanismos TTS",
"subscribe_benefit_translation": "Leia em francês, indonésio, português e russo",
"subscribe_benefit_downloads": "Baixe capítulos para ouvir offline",
"subscribe_login_prompt": "Entre para assinar",
"subscribe_login_cta": "Entrar",
"user_currently_reading": "Lendo Agora",
"user_library_count": "Biblioteca ({n})",

View File

@@ -302,6 +302,24 @@
"book_detail_range_queuing": "В очереди…",
"book_detail_scrape_range": "Диапазон глав",
"book_detail_admin": "Администрирование",
"book_detail_admin_book_cover": "Обложка книги",
"book_detail_admin_chapter_cover": "Обложка главы",
"book_detail_admin_chapter_n": "Глава №",
"book_detail_admin_description": "Описание",
"book_detail_admin_chapter_names": "Названия глав",
"book_detail_admin_audio_tts": "Аудио TTS",
"book_detail_admin_voice": "Голос",
"book_detail_admin_generate": "Сгенерировать",
"book_detail_admin_save_cover": "Сохранить обложку",
"book_detail_admin_saving": "Сохранение…",
"book_detail_admin_saved": "Сохранено",
"book_detail_admin_apply": "Применить",
"book_detail_admin_applying": "Применение…",
"book_detail_admin_applied": "Применено",
"book_detail_admin_discard": "Отменить",
"book_detail_admin_enqueue_audio": "Поставить в очередь",
"book_detail_admin_cancel_audio": "Отмена",
"book_detail_admin_enqueued": "В очереди {enqueued}, пропущено {skipped}",
"book_detail_scraping_progress": "Загружаются первые 20 глав. Страница обновится автоматически.",
"book_detail_scraping_home": "← На главную",
"book_detail_rescrape_book": "Перепарсить книгу",
@@ -348,6 +366,26 @@
"profile_upgrade_monthly": "Ежемесячно — $6 / мес",
"profile_upgrade_annual": "Ежегодно — $48 / год",
"profile_free_limits": "Бесплатный план: 3 аудиоглавы в день, только английский.",
"subscribe_page_title": "Перейти на Pro \u2014 libnovel",
"subscribe_heading": "Читайте больше. Слушайте больше.",
"subscribe_subheading": "Перейдите на Pro и откройте полный опыт libnovel.",
"subscribe_monthly_label": "Ежемесячно",
"subscribe_monthly_price": "$6",
"subscribe_monthly_period": "в месяц",
"subscribe_annual_label": "Ежегодно",
"subscribe_annual_price": "$48",
"subscribe_annual_period": "в год",
"subscribe_annual_save": "Сэкономьте 33%",
"subscribe_cta_monthly": "Начать месячный план",
"subscribe_cta_annual": "Начать годовой план",
"subscribe_already_pro": "У вас уже есть подписка Pro.",
"subscribe_manage": "Управление подпиской",
"subscribe_benefit_audio": "Неограниченные аудиоглавы в день",
"subscribe_benefit_voices": "Выбор голоса для всех TTS-движков",
"subscribe_benefit_translation": "Читайте на французском, индонезийском, португальском и русском",
"subscribe_benefit_downloads": "Скачивайте главы для прослушивания офлайн",
"subscribe_login_prompt": "Войдите, чтобы оформить подписку",
"subscribe_login_cta": "Войти",
"user_currently_reading": "Сейчас читает",
"user_library_count": "Библиотека ({n})",

View File

@@ -574,7 +574,12 @@
// generation runs and MinIO is populated concurrently.
// Skip when enqueued=true to avoid double-generation with the async runner.
if (!voice.startsWith('cfai:') && !presignResult.enqueued) {
const qs = new URLSearchParams({ voice, format: 'mp3' });
// PocketTTS outputs raw WAV — skip the ffmpeg transcode entirely.
// WAV (PCM) is natively supported on all platforms including iOS Safari.
// Kokoro and CF AI output MP3 natively, so keep mp3 for those.
const isPocketTTS = voices.some((v) => v.id === voice && v.engine === 'pocket-tts');
const format = isPocketTTS ? 'wav' : 'mp3';
const qs = new URLSearchParams({ voice, format });
const streamUrl = `/api/audio-stream/${slug}/${chapter}?${qs}`;
// HEAD probe: check paywall without triggering generation.
const headRes = await fetch(streamUrl, { method: 'HEAD' }).catch(() => null);

View File

@@ -1,2 +1,4 @@
/* eslint-disable */
export * from './messages/_index.js'
export * from './messages/_index.js'
// enabling auto-import by exposing all messages as m
export * as m from './messages/_index.js'

View File

@@ -279,6 +279,24 @@ export * from './book_detail_to_chapter.js'
export * from './book_detail_range_queuing.js'
export * from './book_detail_scrape_range.js'
export * from './book_detail_admin.js'
export * from './book_detail_admin_book_cover.js'
export * from './book_detail_admin_chapter_cover.js'
export * from './book_detail_admin_chapter_n.js'
export * from './book_detail_admin_description.js'
export * from './book_detail_admin_chapter_names.js'
export * from './book_detail_admin_audio_tts.js'
export * from './book_detail_admin_voice.js'
export * from './book_detail_admin_generate.js'
export * from './book_detail_admin_save_cover.js'
export * from './book_detail_admin_saving.js'
export * from './book_detail_admin_saved.js'
export * from './book_detail_admin_apply.js'
export * from './book_detail_admin_applying.js'
export * from './book_detail_admin_applied.js'
export * from './book_detail_admin_discard.js'
export * from './book_detail_admin_enqueue_audio.js'
export * from './book_detail_admin_cancel_audio.js'
export * from './book_detail_admin_enqueued.js'
export * from './book_detail_scraping_progress.js'
export * from './book_detail_scraping_home.js'
export * from './book_detail_rescrape_book.js'
@@ -321,6 +339,26 @@ export * from './profile_upgrade_desc.js'
export * from './profile_upgrade_monthly.js'
export * from './profile_upgrade_annual.js'
export * from './profile_free_limits.js'
export * from './subscribe_page_title.js'
export * from './subscribe_heading.js'
export * from './subscribe_subheading.js'
export * from './subscribe_monthly_label.js'
export * from './subscribe_monthly_price.js'
export * from './subscribe_monthly_period.js'
export * from './subscribe_annual_label.js'
export * from './subscribe_annual_price.js'
export * from './subscribe_annual_period.js'
export * from './subscribe_annual_save.js'
export * from './subscribe_cta_monthly.js'
export * from './subscribe_cta_annual.js'
export * from './subscribe_already_pro.js'
export * from './subscribe_manage.js'
export * from './subscribe_benefit_audio.js'
export * from './subscribe_benefit_voices.js'
export * from './subscribe_benefit_translation.js'
export * from './subscribe_benefit_downloads.js'
export * from './subscribe_login_prompt.js'
export * from './subscribe_login_cta.js'
export * from './user_currently_reading.js'
export * from './user_library_count.js'
export * from './user_joined.js'

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_AppliedInputs */
const en_book_detail_admin_applied = /** @type {(inputs: Book_Detail_Admin_AppliedInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Applied`)
};
const ru_book_detail_admin_applied = /** @type {(inputs: Book_Detail_Admin_AppliedInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Применено`)
};
const id_book_detail_admin_applied = /** @type {(inputs: Book_Detail_Admin_AppliedInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Diterapkan`)
};
const pt_book_detail_admin_applied = /** @type {(inputs: Book_Detail_Admin_AppliedInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Aplicado`)
};
const fr_book_detail_admin_applied = /** @type {(inputs: Book_Detail_Admin_AppliedInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Appliqué`)
};
/**
* | output |
* | --- |
* | "Applied" |
*
* @param {Book_Detail_Admin_AppliedInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_applied = /** @type {((inputs?: Book_Detail_Admin_AppliedInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_AppliedInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_applied(inputs)
if (locale === "ru") return ru_book_detail_admin_applied(inputs)
if (locale === "id") return id_book_detail_admin_applied(inputs)
if (locale === "pt") return pt_book_detail_admin_applied(inputs)
return fr_book_detail_admin_applied(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_ApplyInputs */
const en_book_detail_admin_apply = /** @type {(inputs: Book_Detail_Admin_ApplyInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Apply`)
};
const ru_book_detail_admin_apply = /** @type {(inputs: Book_Detail_Admin_ApplyInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Применить`)
};
const id_book_detail_admin_apply = /** @type {(inputs: Book_Detail_Admin_ApplyInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Terapkan`)
};
const pt_book_detail_admin_apply = /** @type {(inputs: Book_Detail_Admin_ApplyInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Aplicar`)
};
const fr_book_detail_admin_apply = /** @type {(inputs: Book_Detail_Admin_ApplyInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Appliquer`)
};
/**
* | output |
* | --- |
* | "Apply" |
*
* @param {Book_Detail_Admin_ApplyInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_apply = /** @type {((inputs?: Book_Detail_Admin_ApplyInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_ApplyInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_apply(inputs)
if (locale === "ru") return ru_book_detail_admin_apply(inputs)
if (locale === "id") return id_book_detail_admin_apply(inputs)
if (locale === "pt") return pt_book_detail_admin_apply(inputs)
return fr_book_detail_admin_apply(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_ApplyingInputs */
const en_book_detail_admin_applying = /** @type {(inputs: Book_Detail_Admin_ApplyingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Applying…`)
};
const ru_book_detail_admin_applying = /** @type {(inputs: Book_Detail_Admin_ApplyingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Применение…`)
};
const id_book_detail_admin_applying = /** @type {(inputs: Book_Detail_Admin_ApplyingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Menerapkan…`)
};
const pt_book_detail_admin_applying = /** @type {(inputs: Book_Detail_Admin_ApplyingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Aplicando…`)
};
const fr_book_detail_admin_applying = /** @type {(inputs: Book_Detail_Admin_ApplyingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Application…`)
};
/**
* | output |
* | --- |
* | "Applying…" |
*
* @param {Book_Detail_Admin_ApplyingInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_applying = /** @type {((inputs?: Book_Detail_Admin_ApplyingInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_ApplyingInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_applying(inputs)
if (locale === "ru") return ru_book_detail_admin_applying(inputs)
if (locale === "id") return id_book_detail_admin_applying(inputs)
if (locale === "pt") return pt_book_detail_admin_applying(inputs)
return fr_book_detail_admin_applying(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_Audio_TtsInputs */
const en_book_detail_admin_audio_tts = /** @type {(inputs: Book_Detail_Admin_Audio_TtsInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Audio TTS`)
};
const ru_book_detail_admin_audio_tts = /** @type {(inputs: Book_Detail_Admin_Audio_TtsInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Аудио TTS`)
};
const id_book_detail_admin_audio_tts = /** @type {(inputs: Book_Detail_Admin_Audio_TtsInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Audio TTS`)
};
const pt_book_detail_admin_audio_tts = /** @type {(inputs: Book_Detail_Admin_Audio_TtsInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Áudio TTS`)
};
const fr_book_detail_admin_audio_tts = /** @type {(inputs: Book_Detail_Admin_Audio_TtsInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Audio TTS`)
};
/**
* | output |
* | --- |
* | "Audio TTS" |
*
* @param {Book_Detail_Admin_Audio_TtsInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_audio_tts = /** @type {((inputs?: Book_Detail_Admin_Audio_TtsInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_Audio_TtsInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_audio_tts(inputs)
if (locale === "ru") return ru_book_detail_admin_audio_tts(inputs)
if (locale === "id") return id_book_detail_admin_audio_tts(inputs)
if (locale === "pt") return pt_book_detail_admin_audio_tts(inputs)
return fr_book_detail_admin_audio_tts(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_Book_CoverInputs */
const en_book_detail_admin_book_cover = /** @type {(inputs: Book_Detail_Admin_Book_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Book Cover`)
};
const ru_book_detail_admin_book_cover = /** @type {(inputs: Book_Detail_Admin_Book_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Обложка книги`)
};
const id_book_detail_admin_book_cover = /** @type {(inputs: Book_Detail_Admin_Book_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Sampul Buku`)
};
const pt_book_detail_admin_book_cover = /** @type {(inputs: Book_Detail_Admin_Book_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Capa do Livro`)
};
const fr_book_detail_admin_book_cover = /** @type {(inputs: Book_Detail_Admin_Book_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Couverture du livre`)
};
/**
* | output |
* | --- |
* | "Book Cover" |
*
* @param {Book_Detail_Admin_Book_CoverInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_book_cover = /** @type {((inputs?: Book_Detail_Admin_Book_CoverInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_Book_CoverInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_book_cover(inputs)
if (locale === "ru") return ru_book_detail_admin_book_cover(inputs)
if (locale === "id") return id_book_detail_admin_book_cover(inputs)
if (locale === "pt") return pt_book_detail_admin_book_cover(inputs)
return fr_book_detail_admin_book_cover(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_Cancel_AudioInputs */
const en_book_detail_admin_cancel_audio = /** @type {(inputs: Book_Detail_Admin_Cancel_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Cancel`)
};
const ru_book_detail_admin_cancel_audio = /** @type {(inputs: Book_Detail_Admin_Cancel_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Отмена`)
};
const id_book_detail_admin_cancel_audio = /** @type {(inputs: Book_Detail_Admin_Cancel_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Batal`)
};
const pt_book_detail_admin_cancel_audio = /** @type {(inputs: Book_Detail_Admin_Cancel_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Cancelar`)
};
const fr_book_detail_admin_cancel_audio = /** @type {(inputs: Book_Detail_Admin_Cancel_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Annuler`)
};
/**
* | output |
* | --- |
* | "Cancel" |
*
* @param {Book_Detail_Admin_Cancel_AudioInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_cancel_audio = /** @type {((inputs?: Book_Detail_Admin_Cancel_AudioInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_Cancel_AudioInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_cancel_audio(inputs)
if (locale === "ru") return ru_book_detail_admin_cancel_audio(inputs)
if (locale === "id") return id_book_detail_admin_cancel_audio(inputs)
if (locale === "pt") return pt_book_detail_admin_cancel_audio(inputs)
return fr_book_detail_admin_cancel_audio(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_Chapter_CoverInputs */
const en_book_detail_admin_chapter_cover = /** @type {(inputs: Book_Detail_Admin_Chapter_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Chapter Cover`)
};
const ru_book_detail_admin_chapter_cover = /** @type {(inputs: Book_Detail_Admin_Chapter_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Обложка главы`)
};
const id_book_detail_admin_chapter_cover = /** @type {(inputs: Book_Detail_Admin_Chapter_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Sampul Bab`)
};
const pt_book_detail_admin_chapter_cover = /** @type {(inputs: Book_Detail_Admin_Chapter_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Capa do Capítulo`)
};
const fr_book_detail_admin_chapter_cover = /** @type {(inputs: Book_Detail_Admin_Chapter_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Couverture du chapitre`)
};
/**
* | output |
* | --- |
* | "Chapter Cover" |
*
* @param {Book_Detail_Admin_Chapter_CoverInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_chapter_cover = /** @type {((inputs?: Book_Detail_Admin_Chapter_CoverInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_Chapter_CoverInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_chapter_cover(inputs)
if (locale === "ru") return ru_book_detail_admin_chapter_cover(inputs)
if (locale === "id") return id_book_detail_admin_chapter_cover(inputs)
if (locale === "pt") return pt_book_detail_admin_chapter_cover(inputs)
return fr_book_detail_admin_chapter_cover(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_Chapter_NInputs */
const en_book_detail_admin_chapter_n = /** @type {(inputs: Book_Detail_Admin_Chapter_NInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Chapter #`)
};
const ru_book_detail_admin_chapter_n = /** @type {(inputs: Book_Detail_Admin_Chapter_NInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Глава №`)
};
const id_book_detail_admin_chapter_n = /** @type {(inputs: Book_Detail_Admin_Chapter_NInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Bab #`)
};
const pt_book_detail_admin_chapter_n = /** @type {(inputs: Book_Detail_Admin_Chapter_NInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Capítulo nº`)
};
const fr_book_detail_admin_chapter_n = /** @type {(inputs: Book_Detail_Admin_Chapter_NInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Chapitre n°`)
};
/**
* | output |
* | --- |
* | "Chapter #" |
*
* @param {Book_Detail_Admin_Chapter_NInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_chapter_n = /** @type {((inputs?: Book_Detail_Admin_Chapter_NInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_Chapter_NInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_chapter_n(inputs)
if (locale === "ru") return ru_book_detail_admin_chapter_n(inputs)
if (locale === "id") return id_book_detail_admin_chapter_n(inputs)
if (locale === "pt") return pt_book_detail_admin_chapter_n(inputs)
return fr_book_detail_admin_chapter_n(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_Chapter_NamesInputs */
const en_book_detail_admin_chapter_names = /** @type {(inputs: Book_Detail_Admin_Chapter_NamesInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Chapter Names`)
};
const ru_book_detail_admin_chapter_names = /** @type {(inputs: Book_Detail_Admin_Chapter_NamesInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Названия глав`)
};
const id_book_detail_admin_chapter_names = /** @type {(inputs: Book_Detail_Admin_Chapter_NamesInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Nama Bab`)
};
const pt_book_detail_admin_chapter_names = /** @type {(inputs: Book_Detail_Admin_Chapter_NamesInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Nomes dos Capítulos`)
};
const fr_book_detail_admin_chapter_names = /** @type {(inputs: Book_Detail_Admin_Chapter_NamesInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Noms des chapitres`)
};
/**
* | output |
* | --- |
* | "Chapter Names" |
*
* @param {Book_Detail_Admin_Chapter_NamesInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_chapter_names = /** @type {((inputs?: Book_Detail_Admin_Chapter_NamesInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_Chapter_NamesInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_chapter_names(inputs)
if (locale === "ru") return ru_book_detail_admin_chapter_names(inputs)
if (locale === "id") return id_book_detail_admin_chapter_names(inputs)
if (locale === "pt") return pt_book_detail_admin_chapter_names(inputs)
return fr_book_detail_admin_chapter_names(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_DescriptionInputs */
const en_book_detail_admin_description = /** @type {(inputs: Book_Detail_Admin_DescriptionInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Description`)
};
const ru_book_detail_admin_description = /** @type {(inputs: Book_Detail_Admin_DescriptionInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Описание`)
};
const id_book_detail_admin_description = /** @type {(inputs: Book_Detail_Admin_DescriptionInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Deskripsi`)
};
const pt_book_detail_admin_description = /** @type {(inputs: Book_Detail_Admin_DescriptionInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Descrição`)
};
const fr_book_detail_admin_description = /** @type {(inputs: Book_Detail_Admin_DescriptionInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Description`)
};
/**
* | output |
* | --- |
* | "Description" |
*
* @param {Book_Detail_Admin_DescriptionInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_description = /** @type {((inputs?: Book_Detail_Admin_DescriptionInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_DescriptionInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_description(inputs)
if (locale === "ru") return ru_book_detail_admin_description(inputs)
if (locale === "id") return id_book_detail_admin_description(inputs)
if (locale === "pt") return pt_book_detail_admin_description(inputs)
return fr_book_detail_admin_description(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_DiscardInputs */
const en_book_detail_admin_discard = /** @type {(inputs: Book_Detail_Admin_DiscardInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Discard`)
};
const ru_book_detail_admin_discard = /** @type {(inputs: Book_Detail_Admin_DiscardInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Отменить`)
};
const id_book_detail_admin_discard = /** @type {(inputs: Book_Detail_Admin_DiscardInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Buang`)
};
const pt_book_detail_admin_discard = /** @type {(inputs: Book_Detail_Admin_DiscardInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Descartar`)
};
const fr_book_detail_admin_discard = /** @type {(inputs: Book_Detail_Admin_DiscardInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Ignorer`)
};
/**
* | output |
* | --- |
* | "Discard" |
*
* @param {Book_Detail_Admin_DiscardInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_discard = /** @type {((inputs?: Book_Detail_Admin_DiscardInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_DiscardInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_discard(inputs)
if (locale === "ru") return ru_book_detail_admin_discard(inputs)
if (locale === "id") return id_book_detail_admin_discard(inputs)
if (locale === "pt") return pt_book_detail_admin_discard(inputs)
return fr_book_detail_admin_discard(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_Enqueue_AudioInputs */
const en_book_detail_admin_enqueue_audio = /** @type {(inputs: Book_Detail_Admin_Enqueue_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Enqueue Audio`)
};
const ru_book_detail_admin_enqueue_audio = /** @type {(inputs: Book_Detail_Admin_Enqueue_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Поставить в очередь`)
};
const id_book_detail_admin_enqueue_audio = /** @type {(inputs: Book_Detail_Admin_Enqueue_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Antre Audio`)
};
const pt_book_detail_admin_enqueue_audio = /** @type {(inputs: Book_Detail_Admin_Enqueue_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Enfileirar Áudio`)
};
const fr_book_detail_admin_enqueue_audio = /** @type {(inputs: Book_Detail_Admin_Enqueue_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Mettre en file audio`)
};
/**
* | output |
* | --- |
* | "Enqueue Audio" |
*
* @param {Book_Detail_Admin_Enqueue_AudioInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_enqueue_audio = /** @type {((inputs?: Book_Detail_Admin_Enqueue_AudioInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_Enqueue_AudioInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_enqueue_audio(inputs)
if (locale === "ru") return ru_book_detail_admin_enqueue_audio(inputs)
if (locale === "id") return id_book_detail_admin_enqueue_audio(inputs)
if (locale === "pt") return pt_book_detail_admin_enqueue_audio(inputs)
return fr_book_detail_admin_enqueue_audio(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{ enqueued: NonNullable<unknown>, skipped: NonNullable<unknown> }} Book_Detail_Admin_EnqueuedInputs */
const en_book_detail_admin_enqueued = /** @type {(inputs: Book_Detail_Admin_EnqueuedInputs) => LocalizedString} */ (i) => {
return /** @type {LocalizedString} */ (`Enqueued ${i?.enqueued}, skipped ${i?.skipped}`)
};
const ru_book_detail_admin_enqueued = /** @type {(inputs: Book_Detail_Admin_EnqueuedInputs) => LocalizedString} */ (i) => {
return /** @type {LocalizedString} */ (`В очереди ${i?.enqueued}, пропущено ${i?.skipped}`)
};
const id_book_detail_admin_enqueued = /** @type {(inputs: Book_Detail_Admin_EnqueuedInputs) => LocalizedString} */ (i) => {
return /** @type {LocalizedString} */ (`Diantre ${i?.enqueued}, dilewati ${i?.skipped}`)
};
const pt_book_detail_admin_enqueued = /** @type {(inputs: Book_Detail_Admin_EnqueuedInputs) => LocalizedString} */ (i) => {
return /** @type {LocalizedString} */ (`${i?.enqueued} enfileirados, ${i?.skipped} ignorados`)
};
const fr_book_detail_admin_enqueued = /** @type {(inputs: Book_Detail_Admin_EnqueuedInputs) => LocalizedString} */ (i) => {
return /** @type {LocalizedString} */ (`${i?.enqueued} en file, ${i?.skipped} ignorés`)
};
/**
* | output |
* | --- |
* | "Enqueued {enqueued}, skipped {skipped}" |
*
* @param {Book_Detail_Admin_EnqueuedInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_enqueued = /** @type {((inputs: Book_Detail_Admin_EnqueuedInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_EnqueuedInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_enqueued(inputs)
if (locale === "ru") return ru_book_detail_admin_enqueued(inputs)
if (locale === "id") return id_book_detail_admin_enqueued(inputs)
if (locale === "pt") return pt_book_detail_admin_enqueued(inputs)
return fr_book_detail_admin_enqueued(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_GenerateInputs */
const en_book_detail_admin_generate = /** @type {(inputs: Book_Detail_Admin_GenerateInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Generate`)
};
const ru_book_detail_admin_generate = /** @type {(inputs: Book_Detail_Admin_GenerateInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Сгенерировать`)
};
const id_book_detail_admin_generate = /** @type {(inputs: Book_Detail_Admin_GenerateInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Buat`)
};
const pt_book_detail_admin_generate = /** @type {(inputs: Book_Detail_Admin_GenerateInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Gerar`)
};
const fr_book_detail_admin_generate = /** @type {(inputs: Book_Detail_Admin_GenerateInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Générer`)
};
/**
* | output |
* | --- |
* | "Generate" |
*
* @param {Book_Detail_Admin_GenerateInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_generate = /** @type {((inputs?: Book_Detail_Admin_GenerateInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_GenerateInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_generate(inputs)
if (locale === "ru") return ru_book_detail_admin_generate(inputs)
if (locale === "id") return id_book_detail_admin_generate(inputs)
if (locale === "pt") return pt_book_detail_admin_generate(inputs)
return fr_book_detail_admin_generate(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_Save_CoverInputs */
const en_book_detail_admin_save_cover = /** @type {(inputs: Book_Detail_Admin_Save_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Save Cover`)
};
const ru_book_detail_admin_save_cover = /** @type {(inputs: Book_Detail_Admin_Save_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Сохранить обложку`)
};
const id_book_detail_admin_save_cover = /** @type {(inputs: Book_Detail_Admin_Save_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Simpan Sampul`)
};
const pt_book_detail_admin_save_cover = /** @type {(inputs: Book_Detail_Admin_Save_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Salvar Capa`)
};
const fr_book_detail_admin_save_cover = /** @type {(inputs: Book_Detail_Admin_Save_CoverInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Enregistrer la couverture`)
};
/**
* | output |
* | --- |
* | "Save Cover" |
*
* @param {Book_Detail_Admin_Save_CoverInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_save_cover = /** @type {((inputs?: Book_Detail_Admin_Save_CoverInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_Save_CoverInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_save_cover(inputs)
if (locale === "ru") return ru_book_detail_admin_save_cover(inputs)
if (locale === "id") return id_book_detail_admin_save_cover(inputs)
if (locale === "pt") return pt_book_detail_admin_save_cover(inputs)
return fr_book_detail_admin_save_cover(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_SavedInputs */
const en_book_detail_admin_saved = /** @type {(inputs: Book_Detail_Admin_SavedInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Saved`)
};
const ru_book_detail_admin_saved = /** @type {(inputs: Book_Detail_Admin_SavedInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Сохранено`)
};
const id_book_detail_admin_saved = /** @type {(inputs: Book_Detail_Admin_SavedInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Tersimpan`)
};
const pt_book_detail_admin_saved = /** @type {(inputs: Book_Detail_Admin_SavedInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Salvo`)
};
const fr_book_detail_admin_saved = /** @type {(inputs: Book_Detail_Admin_SavedInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Enregistré`)
};
/**
* | output |
* | --- |
* | "Saved" |
*
* @param {Book_Detail_Admin_SavedInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_saved = /** @type {((inputs?: Book_Detail_Admin_SavedInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_SavedInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_saved(inputs)
if (locale === "ru") return ru_book_detail_admin_saved(inputs)
if (locale === "id") return id_book_detail_admin_saved(inputs)
if (locale === "pt") return pt_book_detail_admin_saved(inputs)
return fr_book_detail_admin_saved(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_SavingInputs */
const en_book_detail_admin_saving = /** @type {(inputs: Book_Detail_Admin_SavingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Saving…`)
};
const ru_book_detail_admin_saving = /** @type {(inputs: Book_Detail_Admin_SavingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Сохранение…`)
};
const id_book_detail_admin_saving = /** @type {(inputs: Book_Detail_Admin_SavingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Menyimpan…`)
};
const pt_book_detail_admin_saving = /** @type {(inputs: Book_Detail_Admin_SavingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Salvando…`)
};
const fr_book_detail_admin_saving = /** @type {(inputs: Book_Detail_Admin_SavingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Enregistrement…`)
};
/**
* | output |
* | --- |
* | "Saving…" |
*
* @param {Book_Detail_Admin_SavingInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_saving = /** @type {((inputs?: Book_Detail_Admin_SavingInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_SavingInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_saving(inputs)
if (locale === "ru") return ru_book_detail_admin_saving(inputs)
if (locale === "id") return id_book_detail_admin_saving(inputs)
if (locale === "pt") return pt_book_detail_admin_saving(inputs)
return fr_book_detail_admin_saving(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Book_Detail_Admin_VoiceInputs */
const en_book_detail_admin_voice = /** @type {(inputs: Book_Detail_Admin_VoiceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Voice`)
};
const ru_book_detail_admin_voice = /** @type {(inputs: Book_Detail_Admin_VoiceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Голос`)
};
const id_book_detail_admin_voice = /** @type {(inputs: Book_Detail_Admin_VoiceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Suara`)
};
const pt_book_detail_admin_voice = /** @type {(inputs: Book_Detail_Admin_VoiceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Voz`)
};
const fr_book_detail_admin_voice = /** @type {(inputs: Book_Detail_Admin_VoiceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Voix`)
};
/**
* | output |
* | --- |
* | "Voice" |
*
* @param {Book_Detail_Admin_VoiceInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const book_detail_admin_voice = /** @type {((inputs?: Book_Detail_Admin_VoiceInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Book_Detail_Admin_VoiceInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_book_detail_admin_voice(inputs)
if (locale === "ru") return ru_book_detail_admin_voice(inputs)
if (locale === "id") return id_book_detail_admin_voice(inputs)
if (locale === "pt") return pt_book_detail_admin_voice(inputs)
return fr_book_detail_admin_voice(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Already_ProInputs */
const en_subscribe_already_pro = /** @type {(inputs: Subscribe_Already_ProInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`You already have a Pro subscription.`)
};
const ru_subscribe_already_pro = /** @type {(inputs: Subscribe_Already_ProInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`У вас уже есть подписка Pro.`)
};
const id_subscribe_already_pro = /** @type {(inputs: Subscribe_Already_ProInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Anda sudah berlangganan Pro.`)
};
const pt_subscribe_already_pro = /** @type {(inputs: Subscribe_Already_ProInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Você já tem uma assinatura Pro.`)
};
const fr_subscribe_already_pro = /** @type {(inputs: Subscribe_Already_ProInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Vous avez déjà un abonnement Pro.`)
};
/**
* | output |
* | --- |
* | "You already have a Pro subscription." |
*
* @param {Subscribe_Already_ProInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_already_pro = /** @type {((inputs?: Subscribe_Already_ProInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Already_ProInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_already_pro(inputs)
if (locale === "ru") return ru_subscribe_already_pro(inputs)
if (locale === "id") return id_subscribe_already_pro(inputs)
if (locale === "pt") return pt_subscribe_already_pro(inputs)
return fr_subscribe_already_pro(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Annual_LabelInputs */
const en_subscribe_annual_label = /** @type {(inputs: Subscribe_Annual_LabelInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Annual`)
};
const ru_subscribe_annual_label = /** @type {(inputs: Subscribe_Annual_LabelInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Ежегодно`)
};
const id_subscribe_annual_label = /** @type {(inputs: Subscribe_Annual_LabelInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Tahunan`)
};
const pt_subscribe_annual_label = /** @type {(inputs: Subscribe_Annual_LabelInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Anual`)
};
const fr_subscribe_annual_label = /** @type {(inputs: Subscribe_Annual_LabelInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Annuel`)
};
/**
* | output |
* | --- |
* | "Annual" |
*
* @param {Subscribe_Annual_LabelInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_annual_label = /** @type {((inputs?: Subscribe_Annual_LabelInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Annual_LabelInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_annual_label(inputs)
if (locale === "ru") return ru_subscribe_annual_label(inputs)
if (locale === "id") return id_subscribe_annual_label(inputs)
if (locale === "pt") return pt_subscribe_annual_label(inputs)
return fr_subscribe_annual_label(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Annual_PeriodInputs */
const en_subscribe_annual_period = /** @type {(inputs: Subscribe_Annual_PeriodInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`per year`)
};
const ru_subscribe_annual_period = /** @type {(inputs: Subscribe_Annual_PeriodInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`в год`)
};
const id_subscribe_annual_period = /** @type {(inputs: Subscribe_Annual_PeriodInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`per tahun`)
};
const pt_subscribe_annual_period = /** @type {(inputs: Subscribe_Annual_PeriodInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`por ano`)
};
const fr_subscribe_annual_period = /** @type {(inputs: Subscribe_Annual_PeriodInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`par an`)
};
/**
* | output |
* | --- |
* | "per year" |
*
* @param {Subscribe_Annual_PeriodInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_annual_period = /** @type {((inputs?: Subscribe_Annual_PeriodInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Annual_PeriodInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_annual_period(inputs)
if (locale === "ru") return ru_subscribe_annual_period(inputs)
if (locale === "id") return id_subscribe_annual_period(inputs)
if (locale === "pt") return pt_subscribe_annual_period(inputs)
return fr_subscribe_annual_period(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Annual_PriceInputs */
const en_subscribe_annual_price = /** @type {(inputs: Subscribe_Annual_PriceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`$48`)
};
const ru_subscribe_annual_price = /** @type {(inputs: Subscribe_Annual_PriceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`$48`)
};
const id_subscribe_annual_price = /** @type {(inputs: Subscribe_Annual_PriceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`$48`)
};
const pt_subscribe_annual_price = /** @type {(inputs: Subscribe_Annual_PriceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`$48`)
};
const fr_subscribe_annual_price = /** @type {(inputs: Subscribe_Annual_PriceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`48 $`)
};
/**
* | output |
* | --- |
* | "$48" |
*
* @param {Subscribe_Annual_PriceInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_annual_price = /** @type {((inputs?: Subscribe_Annual_PriceInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Annual_PriceInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_annual_price(inputs)
if (locale === "ru") return ru_subscribe_annual_price(inputs)
if (locale === "id") return id_subscribe_annual_price(inputs)
if (locale === "pt") return pt_subscribe_annual_price(inputs)
return fr_subscribe_annual_price(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Annual_SaveInputs */
const en_subscribe_annual_save = /** @type {(inputs: Subscribe_Annual_SaveInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Save 33%`)
};
const ru_subscribe_annual_save = /** @type {(inputs: Subscribe_Annual_SaveInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Сэкономьте 33%`)
};
const id_subscribe_annual_save = /** @type {(inputs: Subscribe_Annual_SaveInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Hemat 33%`)
};
const pt_subscribe_annual_save = /** @type {(inputs: Subscribe_Annual_SaveInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Economize 33%`)
};
const fr_subscribe_annual_save = /** @type {(inputs: Subscribe_Annual_SaveInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Économisez 33 %`)
};
/**
* | output |
* | --- |
* | "Save 33%" |
*
* @param {Subscribe_Annual_SaveInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_annual_save = /** @type {((inputs?: Subscribe_Annual_SaveInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Annual_SaveInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_annual_save(inputs)
if (locale === "ru") return ru_subscribe_annual_save(inputs)
if (locale === "id") return id_subscribe_annual_save(inputs)
if (locale === "pt") return pt_subscribe_annual_save(inputs)
return fr_subscribe_annual_save(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Benefit_AudioInputs */
const en_subscribe_benefit_audio = /** @type {(inputs: Subscribe_Benefit_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Unlimited audio chapters per day`)
};
const ru_subscribe_benefit_audio = /** @type {(inputs: Subscribe_Benefit_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Неограниченные аудиоглавы в день`)
};
const id_subscribe_benefit_audio = /** @type {(inputs: Subscribe_Benefit_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Bab audio tak terbatas per hari`)
};
const pt_subscribe_benefit_audio = /** @type {(inputs: Subscribe_Benefit_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Capítulos de áudio ilimitados por dia`)
};
const fr_subscribe_benefit_audio = /** @type {(inputs: Subscribe_Benefit_AudioInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Chapitres audio illimités par jour`)
};
/**
* | output |
* | --- |
* | "Unlimited audio chapters per day" |
*
* @param {Subscribe_Benefit_AudioInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_benefit_audio = /** @type {((inputs?: Subscribe_Benefit_AudioInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Benefit_AudioInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_benefit_audio(inputs)
if (locale === "ru") return ru_subscribe_benefit_audio(inputs)
if (locale === "id") return id_subscribe_benefit_audio(inputs)
if (locale === "pt") return pt_subscribe_benefit_audio(inputs)
return fr_subscribe_benefit_audio(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Benefit_DownloadsInputs */
const en_subscribe_benefit_downloads = /** @type {(inputs: Subscribe_Benefit_DownloadsInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Download chapters for offline listening`)
};
const ru_subscribe_benefit_downloads = /** @type {(inputs: Subscribe_Benefit_DownloadsInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Скачивайте главы для прослушивания офлайн`)
};
const id_subscribe_benefit_downloads = /** @type {(inputs: Subscribe_Benefit_DownloadsInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Unduh bab untuk didengarkan secara offline`)
};
const pt_subscribe_benefit_downloads = /** @type {(inputs: Subscribe_Benefit_DownloadsInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Baixe capítulos para ouvir offline`)
};
const fr_subscribe_benefit_downloads = /** @type {(inputs: Subscribe_Benefit_DownloadsInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Télécharger des chapitres pour une écoute hors ligne`)
};
/**
* | output |
* | --- |
* | "Download chapters for offline listening" |
*
* @param {Subscribe_Benefit_DownloadsInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_benefit_downloads = /** @type {((inputs?: Subscribe_Benefit_DownloadsInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Benefit_DownloadsInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_benefit_downloads(inputs)
if (locale === "ru") return ru_subscribe_benefit_downloads(inputs)
if (locale === "id") return id_subscribe_benefit_downloads(inputs)
if (locale === "pt") return pt_subscribe_benefit_downloads(inputs)
return fr_subscribe_benefit_downloads(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Benefit_TranslationInputs */
const en_subscribe_benefit_translation = /** @type {(inputs: Subscribe_Benefit_TranslationInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Read in French, Indonesian, Portuguese, and Russian`)
};
const ru_subscribe_benefit_translation = /** @type {(inputs: Subscribe_Benefit_TranslationInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Читайте на французском, индонезийском, португальском и русском`)
};
const id_subscribe_benefit_translation = /** @type {(inputs: Subscribe_Benefit_TranslationInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Baca dalam bahasa Prancis, Indonesia, Portugis, dan Rusia`)
};
const pt_subscribe_benefit_translation = /** @type {(inputs: Subscribe_Benefit_TranslationInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Leia em francês, indonésio, português e russo`)
};
const fr_subscribe_benefit_translation = /** @type {(inputs: Subscribe_Benefit_TranslationInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Lire en français, indonésien, portugais et russe`)
};
/**
* | output |
* | --- |
* | "Read in French, Indonesian, Portuguese, and Russian" |
*
* @param {Subscribe_Benefit_TranslationInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_benefit_translation = /** @type {((inputs?: Subscribe_Benefit_TranslationInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Benefit_TranslationInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_benefit_translation(inputs)
if (locale === "ru") return ru_subscribe_benefit_translation(inputs)
if (locale === "id") return id_subscribe_benefit_translation(inputs)
if (locale === "pt") return pt_subscribe_benefit_translation(inputs)
return fr_subscribe_benefit_translation(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Benefit_VoicesInputs */
const en_subscribe_benefit_voices = /** @type {(inputs: Subscribe_Benefit_VoicesInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Voice selection across all TTS engines`)
};
const ru_subscribe_benefit_voices = /** @type {(inputs: Subscribe_Benefit_VoicesInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Выбор голоса для всех TTS-движков`)
};
const id_subscribe_benefit_voices = /** @type {(inputs: Subscribe_Benefit_VoicesInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Pilihan suara untuk semua mesin TTS`)
};
const pt_subscribe_benefit_voices = /** @type {(inputs: Subscribe_Benefit_VoicesInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Seleção de voz para todos os mecanismos TTS`)
};
const fr_subscribe_benefit_voices = /** @type {(inputs: Subscribe_Benefit_VoicesInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Sélection de voix pour tous les moteurs TTS`)
};
/**
* | output |
* | --- |
* | "Voice selection across all TTS engines" |
*
* @param {Subscribe_Benefit_VoicesInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_benefit_voices = /** @type {((inputs?: Subscribe_Benefit_VoicesInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Benefit_VoicesInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_benefit_voices(inputs)
if (locale === "ru") return ru_subscribe_benefit_voices(inputs)
if (locale === "id") return id_subscribe_benefit_voices(inputs)
if (locale === "pt") return pt_subscribe_benefit_voices(inputs)
return fr_subscribe_benefit_voices(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Cta_AnnualInputs */
const en_subscribe_cta_annual = /** @type {(inputs: Subscribe_Cta_AnnualInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Start annual plan`)
};
const ru_subscribe_cta_annual = /** @type {(inputs: Subscribe_Cta_AnnualInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Начать годовой план`)
};
const id_subscribe_cta_annual = /** @type {(inputs: Subscribe_Cta_AnnualInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Mulai paket tahunan`)
};
const pt_subscribe_cta_annual = /** @type {(inputs: Subscribe_Cta_AnnualInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Começar plano anual`)
};
const fr_subscribe_cta_annual = /** @type {(inputs: Subscribe_Cta_AnnualInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Commencer le plan annuel`)
};
/**
* | output |
* | --- |
* | "Start annual plan" |
*
* @param {Subscribe_Cta_AnnualInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_cta_annual = /** @type {((inputs?: Subscribe_Cta_AnnualInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Cta_AnnualInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_cta_annual(inputs)
if (locale === "ru") return ru_subscribe_cta_annual(inputs)
if (locale === "id") return id_subscribe_cta_annual(inputs)
if (locale === "pt") return pt_subscribe_cta_annual(inputs)
return fr_subscribe_cta_annual(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Cta_MonthlyInputs */
const en_subscribe_cta_monthly = /** @type {(inputs: Subscribe_Cta_MonthlyInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Start monthly plan`)
};
const ru_subscribe_cta_monthly = /** @type {(inputs: Subscribe_Cta_MonthlyInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Начать месячный план`)
};
const id_subscribe_cta_monthly = /** @type {(inputs: Subscribe_Cta_MonthlyInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Mulai paket bulanan`)
};
const pt_subscribe_cta_monthly = /** @type {(inputs: Subscribe_Cta_MonthlyInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Começar plano mensal`)
};
const fr_subscribe_cta_monthly = /** @type {(inputs: Subscribe_Cta_MonthlyInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Commencer le plan mensuel`)
};
/**
* | output |
* | --- |
* | "Start monthly plan" |
*
* @param {Subscribe_Cta_MonthlyInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_cta_monthly = /** @type {((inputs?: Subscribe_Cta_MonthlyInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Cta_MonthlyInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_cta_monthly(inputs)
if (locale === "ru") return ru_subscribe_cta_monthly(inputs)
if (locale === "id") return id_subscribe_cta_monthly(inputs)
if (locale === "pt") return pt_subscribe_cta_monthly(inputs)
return fr_subscribe_cta_monthly(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_HeadingInputs */
const en_subscribe_heading = /** @type {(inputs: Subscribe_HeadingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Read more. Listen more.`)
};
const ru_subscribe_heading = /** @type {(inputs: Subscribe_HeadingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Читайте больше. Слушайте больше.`)
};
const id_subscribe_heading = /** @type {(inputs: Subscribe_HeadingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Baca lebih. Dengarkan lebih.`)
};
const pt_subscribe_heading = /** @type {(inputs: Subscribe_HeadingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Leia mais. Ouça mais.`)
};
const fr_subscribe_heading = /** @type {(inputs: Subscribe_HeadingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Lisez plus. Écoutez plus.`)
};
/**
* | output |
* | --- |
* | "Read more. Listen more." |
*
* @param {Subscribe_HeadingInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_heading = /** @type {((inputs?: Subscribe_HeadingInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_HeadingInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_heading(inputs)
if (locale === "ru") return ru_subscribe_heading(inputs)
if (locale === "id") return id_subscribe_heading(inputs)
if (locale === "pt") return pt_subscribe_heading(inputs)
return fr_subscribe_heading(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Login_CtaInputs */
const en_subscribe_login_cta = /** @type {(inputs: Subscribe_Login_CtaInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Sign in`)
};
const ru_subscribe_login_cta = /** @type {(inputs: Subscribe_Login_CtaInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Войти`)
};
const id_subscribe_login_cta = /** @type {(inputs: Subscribe_Login_CtaInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Masuk`)
};
const pt_subscribe_login_cta = /** @type {(inputs: Subscribe_Login_CtaInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Entrar`)
};
const fr_subscribe_login_cta = /** @type {(inputs: Subscribe_Login_CtaInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Se connecter`)
};
/**
* | output |
* | --- |
* | "Sign in" |
*
* @param {Subscribe_Login_CtaInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_login_cta = /** @type {((inputs?: Subscribe_Login_CtaInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Login_CtaInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_login_cta(inputs)
if (locale === "ru") return ru_subscribe_login_cta(inputs)
if (locale === "id") return id_subscribe_login_cta(inputs)
if (locale === "pt") return pt_subscribe_login_cta(inputs)
return fr_subscribe_login_cta(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Login_PromptInputs */
const en_subscribe_login_prompt = /** @type {(inputs: Subscribe_Login_PromptInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Sign in to subscribe`)
};
const ru_subscribe_login_prompt = /** @type {(inputs: Subscribe_Login_PromptInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Войдите, чтобы оформить подписку`)
};
const id_subscribe_login_prompt = /** @type {(inputs: Subscribe_Login_PromptInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Masuk untuk berlangganan`)
};
const pt_subscribe_login_prompt = /** @type {(inputs: Subscribe_Login_PromptInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Entre para assinar`)
};
const fr_subscribe_login_prompt = /** @type {(inputs: Subscribe_Login_PromptInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Connectez-vous pour vous abonner`)
};
/**
* | output |
* | --- |
* | "Sign in to subscribe" |
*
* @param {Subscribe_Login_PromptInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_login_prompt = /** @type {((inputs?: Subscribe_Login_PromptInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Login_PromptInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_login_prompt(inputs)
if (locale === "ru") return ru_subscribe_login_prompt(inputs)
if (locale === "id") return id_subscribe_login_prompt(inputs)
if (locale === "pt") return pt_subscribe_login_prompt(inputs)
return fr_subscribe_login_prompt(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_ManageInputs */
const en_subscribe_manage = /** @type {(inputs: Subscribe_ManageInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Manage subscription`)
};
const ru_subscribe_manage = /** @type {(inputs: Subscribe_ManageInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Управление подпиской`)
};
const id_subscribe_manage = /** @type {(inputs: Subscribe_ManageInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Kelola langganan`)
};
const pt_subscribe_manage = /** @type {(inputs: Subscribe_ManageInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Gerenciar assinatura`)
};
const fr_subscribe_manage = /** @type {(inputs: Subscribe_ManageInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Gérer l'abonnement`)
};
/**
* | output |
* | --- |
* | "Manage subscription" |
*
* @param {Subscribe_ManageInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_manage = /** @type {((inputs?: Subscribe_ManageInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_ManageInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_manage(inputs)
if (locale === "ru") return ru_subscribe_manage(inputs)
if (locale === "id") return id_subscribe_manage(inputs)
if (locale === "pt") return pt_subscribe_manage(inputs)
return fr_subscribe_manage(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Monthly_LabelInputs */
const en_subscribe_monthly_label = /** @type {(inputs: Subscribe_Monthly_LabelInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Monthly`)
};
const ru_subscribe_monthly_label = /** @type {(inputs: Subscribe_Monthly_LabelInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Ежемесячно`)
};
const id_subscribe_monthly_label = /** @type {(inputs: Subscribe_Monthly_LabelInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Bulanan`)
};
const pt_subscribe_monthly_label = /** @type {(inputs: Subscribe_Monthly_LabelInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Mensal`)
};
const fr_subscribe_monthly_label = /** @type {(inputs: Subscribe_Monthly_LabelInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Mensuel`)
};
/**
* | output |
* | --- |
* | "Monthly" |
*
* @param {Subscribe_Monthly_LabelInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_monthly_label = /** @type {((inputs?: Subscribe_Monthly_LabelInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Monthly_LabelInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_monthly_label(inputs)
if (locale === "ru") return ru_subscribe_monthly_label(inputs)
if (locale === "id") return id_subscribe_monthly_label(inputs)
if (locale === "pt") return pt_subscribe_monthly_label(inputs)
return fr_subscribe_monthly_label(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Monthly_PeriodInputs */
const en_subscribe_monthly_period = /** @type {(inputs: Subscribe_Monthly_PeriodInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`per month`)
};
const ru_subscribe_monthly_period = /** @type {(inputs: Subscribe_Monthly_PeriodInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`в месяц`)
};
const id_subscribe_monthly_period = /** @type {(inputs: Subscribe_Monthly_PeriodInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`per bulan`)
};
const pt_subscribe_monthly_period = /** @type {(inputs: Subscribe_Monthly_PeriodInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`por mês`)
};
const fr_subscribe_monthly_period = /** @type {(inputs: Subscribe_Monthly_PeriodInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`par mois`)
};
/**
* | output |
* | --- |
* | "per month" |
*
* @param {Subscribe_Monthly_PeriodInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_monthly_period = /** @type {((inputs?: Subscribe_Monthly_PeriodInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Monthly_PeriodInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_monthly_period(inputs)
if (locale === "ru") return ru_subscribe_monthly_period(inputs)
if (locale === "id") return id_subscribe_monthly_period(inputs)
if (locale === "pt") return pt_subscribe_monthly_period(inputs)
return fr_subscribe_monthly_period(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Monthly_PriceInputs */
const en_subscribe_monthly_price = /** @type {(inputs: Subscribe_Monthly_PriceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`$6`)
};
const ru_subscribe_monthly_price = /** @type {(inputs: Subscribe_Monthly_PriceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`$6`)
};
const id_subscribe_monthly_price = /** @type {(inputs: Subscribe_Monthly_PriceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`$6`)
};
const pt_subscribe_monthly_price = /** @type {(inputs: Subscribe_Monthly_PriceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`$6`)
};
const fr_subscribe_monthly_price = /** @type {(inputs: Subscribe_Monthly_PriceInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`6 $`)
};
/**
* | output |
* | --- |
* | "$6" |
*
* @param {Subscribe_Monthly_PriceInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_monthly_price = /** @type {((inputs?: Subscribe_Monthly_PriceInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Monthly_PriceInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_monthly_price(inputs)
if (locale === "ru") return ru_subscribe_monthly_price(inputs)
if (locale === "id") return id_subscribe_monthly_price(inputs)
if (locale === "pt") return pt_subscribe_monthly_price(inputs)
return fr_subscribe_monthly_price(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_Page_TitleInputs */
const en_subscribe_page_title = /** @type {(inputs: Subscribe_Page_TitleInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Go Pro — libnovel`)
};
const ru_subscribe_page_title = /** @type {(inputs: Subscribe_Page_TitleInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Перейти на Pro — libnovel`)
};
const id_subscribe_page_title = /** @type {(inputs: Subscribe_Page_TitleInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Jadi Pro — libnovel`)
};
const pt_subscribe_page_title = /** @type {(inputs: Subscribe_Page_TitleInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Seja Pro — libnovel`)
};
const fr_subscribe_page_title = /** @type {(inputs: Subscribe_Page_TitleInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Passer Pro — libnovel`)
};
/**
* | output |
* | --- |
* | "Go Pro — libnovel" |
*
* @param {Subscribe_Page_TitleInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_page_title = /** @type {((inputs?: Subscribe_Page_TitleInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_Page_TitleInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_page_title(inputs)
if (locale === "ru") return ru_subscribe_page_title(inputs)
if (locale === "id") return id_subscribe_page_title(inputs)
if (locale === "pt") return pt_subscribe_page_title(inputs)
return fr_subscribe_page_title(inputs)
});

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Subscribe_SubheadingInputs */
const en_subscribe_subheading = /** @type {(inputs: Subscribe_SubheadingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Upgrade to Pro and unlock the full libnovel experience.`)
};
const ru_subscribe_subheading = /** @type {(inputs: Subscribe_SubheadingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Перейдите на Pro и откройте полный опыт libnovel.`)
};
const id_subscribe_subheading = /** @type {(inputs: Subscribe_SubheadingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Tingkatkan ke Pro dan buka pengalaman libnovel sepenuhnya.`)
};
const pt_subscribe_subheading = /** @type {(inputs: Subscribe_SubheadingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Torne-se Pro e desbloqueie a experiência completa do libnovel.`)
};
const fr_subscribe_subheading = /** @type {(inputs: Subscribe_SubheadingInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Passez Pro et débloquez l'expérience libnovel complète.`)
};
/**
* | output |
* | --- |
* | "Upgrade to Pro and unlock the full libnovel experience." |
*
* @param {Subscribe_SubheadingInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const subscribe_subheading = /** @type {((inputs?: Subscribe_SubheadingInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Subscribe_SubheadingInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_subscribe_subheading(inputs)
if (locale === "ru") return ru_subscribe_subheading(inputs)
if (locale === "id") return id_subscribe_subheading(inputs)
if (locale === "pt") return pt_subscribe_subheading(inputs)
return fr_subscribe_subheading(inputs)
});

View File

@@ -407,6 +407,15 @@
>
{m.nav_catalogue()}
</a>
{#if !data.isPro}
<a
href="/subscribe"
class="hidden sm:inline-flex items-center gap-1 text-sm font-semibold transition-colors {page.url.pathname.startsWith('/subscribe') ? 'text-(--color-brand)' : 'text-(--color-brand) opacity-70 hover:opacity-100'}"
>
<svg class="w-3.5 h-3.5 shrink-0" fill="currentColor" viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm4.24 16L12 15.45 7.77 18l1.12-4.81-3.73-3.23 4.92-.42L12 5l1.92 4.53 4.92.42-3.73 3.23L16.23 18z"/></svg>
Pro
</a>
{/if}
<div class="ml-auto flex items-center gap-2">
<!-- Theme dropdown (desktop) -->
<div class="hidden sm:block relative">

View File

@@ -0,0 +1,34 @@
/**
* POST /api/admin/audio/bulk
*
* Admin-only proxy to the Go backend's audio bulk-enqueue endpoint.
* Body: { slug, voice?, from, to, skip_existing?, force? }
* Response 202: { enqueued, skipped, task_ids }
*/
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { log } from '$lib/server/logger';
import { backendFetch } from '$lib/server/scraper';
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user || locals.user.role !== 'admin') {
throw error(403, 'Forbidden');
}
const body = await request.text();
let res: Response;
try {
res = await backendFetch('/api/admin/audio/bulk', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body
});
} catch (e) {
log.error('admin/audio/bulk', 'backend proxy error', { err: String(e) });
throw error(502, 'Could not reach backend');
}
const data = await res.json().catch(() => ({}));
return json(data, { status: res.status });
};

View File

@@ -0,0 +1,34 @@
/**
* POST /api/admin/audio/cancel-bulk
*
* Admin-only proxy to cancel all pending/running audio tasks for a slug.
* Body: { slug }
* Response 200: { cancelled }
*/
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { log } from '$lib/server/logger';
import { backendFetch } from '$lib/server/scraper';
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user || locals.user.role !== 'admin') {
throw error(403, 'Forbidden');
}
const body = await request.text();
let res: Response;
try {
res = await backendFetch('/api/admin/audio/cancel-bulk', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body
});
} catch (e) {
log.error('admin/audio/cancel-bulk', 'backend proxy error', { err: String(e) });
throw error(502, 'Could not reach backend');
}
const data = await res.json().catch(() => ({}));
return json(data, { status: res.status });
};

View File

@@ -133,6 +133,301 @@
// ── Admin panel expand/collapse ───────────────────────────────────────────
let adminOpen = $state(false);
// ── Admin: book cover generation ──────────────────────────────────────────
let coverGenerating = $state(false);
let coverPreview = $state<string | null>(null);
let coverSaving = $state(false);
let coverResult = $state<'saved' | 'error' | ''>('');
async function generateCover() {
const slug = data.book?.slug;
if (coverGenerating || !slug) return;
coverGenerating = true;
coverPreview = null;
coverResult = '';
try {
const res = await fetch('/api/admin/image-gen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug, type: 'cover', prompt: data.book?.title ?? slug })
});
if (res.ok) {
const d = await res.json();
coverPreview = d.image_b64 ? `data:${d.content_type ?? 'image/png'};base64,${d.image_b64}` : null;
} else {
coverResult = 'error';
}
} catch {
coverResult = 'error';
} finally {
coverGenerating = false;
}
}
async function saveCover() {
const slug = data.book?.slug;
if (coverSaving || !coverPreview || !slug) return;
coverSaving = true;
coverResult = '';
try {
const b64 = coverPreview.replace(/^data:[^;]+;base64,/, '');
const res = await fetch('/api/admin/image-gen/save-cover', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug, image_b64: b64 })
});
if (res.ok) {
coverResult = 'saved';
coverPreview = null;
await invalidateAll();
} else {
coverResult = 'error';
}
} catch {
coverResult = 'error';
} finally {
coverSaving = false;
}
}
// ── Admin: chapter cover generation ───────────────────────────────────────
let chapterCoverN = $state('1');
let chapterCoverGenerating = $state(false);
let chapterCoverPreview = $state<string | null>(null);
let chapterCoverResult = $state<'error' | ''>('');
async function generateChapterCover() {
const slug = data.book?.slug;
if (chapterCoverGenerating || !slug) return;
const n = parseInt(chapterCoverN, 10);
if (!n || n < 1) return;
chapterCoverGenerating = true;
chapterCoverPreview = null;
chapterCoverResult = '';
try {
const res = await fetch('/api/admin/image-gen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug, type: 'chapter', chapter: n, prompt: data.book?.title ?? slug })
});
if (res.ok) {
const d = await res.json();
chapterCoverPreview = d.image_b64 ? `data:${d.content_type ?? 'image/png'};base64,${d.image_b64}` : null;
} else {
chapterCoverResult = 'error';
}
} catch {
chapterCoverResult = 'error';
} finally {
chapterCoverGenerating = false;
}
}
// ── Admin: description generation ─────────────────────────────────────────
let descGenerating = $state(false);
let descPreview = $state('');
let descApplying = $state(false);
let descResult = $state<'applied' | 'error' | ''>('');
async function generateDesc() {
const slug = data.book?.slug;
if (descGenerating || !slug) return;
descGenerating = true;
descPreview = '';
descResult = '';
try {
const res = await fetch('/api/admin/text-gen/description', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug })
});
if (res.ok) {
const d = await res.json();
descPreview = d.new_description ?? '';
} else {
descResult = 'error';
}
} catch {
descResult = 'error';
} finally {
descGenerating = false;
}
}
async function applyDesc() {
const slug = data.book?.slug;
if (descApplying || !descPreview || !slug) return;
descApplying = true;
descResult = '';
try {
const res = await fetch('/api/admin/text-gen/description/apply', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug, description: descPreview })
});
if (res.ok) {
descResult = 'applied';
descPreview = '';
await invalidateAll();
} else {
descResult = 'error';
}
} catch {
descResult = 'error';
} finally {
descApplying = false;
}
}
// ── Admin: chapter names generation ───────────────────────────────────────
let chapNamesGenerating = $state(false);
let chapNamesPreview = $state<{ number: number; old_title: string; new_title: string }[]>([]);
let chapNamesApplying = $state(false);
let chapNamesResult = $state<'applied' | 'error' | ''>('');
async function generateChapNames() {
const slug = data.book?.slug;
if (chapNamesGenerating || !slug) return;
chapNamesGenerating = true;
chapNamesPreview = [];
chapNamesResult = '';
try {
const res = await fetch('/api/admin/text-gen/chapter-names', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug, pattern: 'Chapter {n}: {scene}' })
});
if (res.ok) {
const d = await res.json();
chapNamesPreview = d.chapters ?? [];
} else {
chapNamesResult = 'error';
}
} catch {
chapNamesResult = 'error';
} finally {
chapNamesGenerating = false;
}
}
async function applyChapNames() {
const slug = data.book?.slug;
if (chapNamesApplying || chapNamesPreview.length === 0 || !slug) return;
chapNamesApplying = true;
chapNamesResult = '';
try {
const chapters = chapNamesPreview.map((c) => ({ number: c.number, title: c.new_title }));
const res = await fetch('/api/admin/text-gen/chapter-names/apply', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug, chapters })
});
if (res.ok) {
chapNamesResult = 'applied';
chapNamesPreview = [];
await invalidateAll();
} else {
chapNamesResult = 'error';
}
} catch {
chapNamesResult = 'error';
} finally {
chapNamesApplying = false;
}
}
// ── Admin: audio TTS bulk enqueue ─────────────────────────────────────────
interface Voice { id: string; engine: string; lang: string; gender: string }
let audioVoices = $state<Voice[]>([]);
let audioVoicesLoaded = $state(false);
let audioVoice = $state('af_bella');
let audioFrom = $state('1');
let audioTo = $state('');
let audioEnqueuing = $state(false);
let audioResult = $state<{ enqueued: number; skipped: number } | null>(null);
let audioError = $state('');
// Load voices lazily when admin panel opens
$effect(() => {
if (!adminOpen || audioVoicesLoaded) return;
fetch('/api/voices')
.then((r) => r.json())
.then((d: { voices: Voice[] }) => {
audioVoices = d.voices ?? [];
audioVoicesLoaded = true;
})
.catch(() => { audioVoicesLoaded = true; });
});
function voiceLabel(v: Voice): string {
if (v.engine === 'cfai') {
const speaker = v.id.startsWith('cfai:') ? v.id.slice(5) : v.id;
return speaker.replace(/\b\w/g, (c) => c.toUpperCase()) + ' (CF AI)';
}
if (v.engine === 'pocket-tts') {
return v.id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + ' (Pocket)';
}
// Kokoro
const langMap: Record<string, string> = {
af: 'US', am: 'US', bf: 'UK', bm: 'UK',
ef: 'ES', em: 'ES', ff: 'FR', hf: 'IN', hm: 'IN',
'if': 'IT', im: 'IT', jf: 'JP', jm: 'JP', pf: 'PT', pm: 'PT', zf: 'ZH', zm: 'ZH',
};
const prefix = v.id.slice(0, 2);
const name = v.id.slice(3).replace(/^v0/, '').replace(/^([a-z])/, (c) => c.toUpperCase());
const lang = langMap[prefix] ?? prefix.toUpperCase();
return `${name} (${lang})`;
}
async function enqueueAudio() {
const slug = data.book?.slug;
if (audioEnqueuing || !slug) return;
const from = parseInt(audioFrom, 10);
const to = audioTo ? parseInt(audioTo, 10) : (data.book?.total_chapters ?? 1);
if (!from || from < 1) return;
audioEnqueuing = true;
audioResult = null;
audioError = '';
try {
const res = await fetch('/api/admin/audio/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug, voice: audioVoice, from, to })
});
if (res.ok || res.status === 202) {
const d = await res.json();
audioResult = { enqueued: d.enqueued ?? 0, skipped: d.skipped ?? 0 };
} else {
const d = await res.json().catch(() => ({}));
audioError = d.error ?? 'Failed to enqueue';
}
} catch {
audioError = 'Network error';
} finally {
audioEnqueuing = false;
}
}
async function cancelAudio() {
const slug = data.book?.slug;
if (!slug) return;
audioResult = null;
audioError = '';
try {
const res = await fetch('/api/admin/audio/cancel-bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug })
});
if (res.ok) {
const d = await res.json();
audioError = `Cancelled ${d.cancelled ?? 0} task(s).`;
}
} catch {
audioError = 'Cancel failed';
}
}
// ── "More like this" ─────────────────────────────────────────────────────
interface SimilarBook { slug: string; title: string; cover: string | null; author: string | null }
let similarBooks = $state<SimilarBook[]>([]);
@@ -520,78 +815,336 @@
</svg>
</button>
{#if adminOpen}
<div class="px-4 py-3 border-t border-(--color-border) flex flex-col gap-4">
<!-- Rescrape -->
{#if adminOpen}
<div class="px-4 py-3 border-t border-(--color-border) flex flex-col gap-5">
<!-- Rescrape -->
<div class="flex items-center gap-3 flex-wrap">
<button
onclick={rescrape}
disabled={scraping}
class="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors
{scraping ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-3)'}"
>
{#if scraping}
<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
{m.book_detail_rescraping()}
{:else}
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
{m.book_detail_rescrape_book()}
{/if}
</button>
{#if scrapeResult}
<span class="text-xs {scrapeResult === 'queued' ? 'text-green-400' : scrapeResult === 'busy' ? 'text-(--color-brand)' : 'text-(--color-danger)'}">
{scrapeResult === 'queued' ? m.catalogue_scrape_queued_badge() + '.' : scrapeResult === 'busy' ? m.catalogue_scrape_busy_badge() + '.' : m.common_error() + '.'}
</span>
{/if}
</div>
<!-- Range scrape -->
<div class="flex flex-wrap items-end gap-3">
<div class="flex flex-col gap-1">
<label for="range-from" class="text-xs text-(--color-muted)">{m.book_detail_from_chapter()}</label>
<input
id="range-from"
type="number"
min="1"
bind:value={rangeFrom}
placeholder="1"
class="w-24 px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
/>
</div>
<div class="flex flex-col gap-1">
<label for="range-to" class="text-xs text-(--color-muted)">{m.book_detail_to_chapter()}</label>
<input
id="range-to"
type="number"
min="1"
bind:value={rangeTo}
placeholder="end"
class="w-24 px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
/>
</div>
<button
onclick={scrapeRange}
disabled={rangeScraping || !rangeFrom}
class="px-3 py-1.5 rounded text-xs font-medium transition-colors
{rangeScraping || !rangeFrom
? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed'
: 'bg-(--color-brand)/20 text-(--color-brand-dim) hover:bg-(--color-brand)/40 border border-(--color-brand)/30'}"
>
{rangeScraping ? m.book_detail_range_queuing() : m.book_detail_scrape_range()}
</button>
{#if rangeResult}
<span class="text-xs {rangeResult === 'queued' ? 'text-green-400' : rangeResult === 'busy' ? 'text-(--color-brand)' : 'text-(--color-danger)'}">
{rangeResult === 'queued' ? m.catalogue_scrape_queued_badge() + '.' : rangeResult === 'busy' ? m.catalogue_scrape_busy_badge() + '.' : m.common_error() + '.'}
</span>
{/if}
</div>
<hr class="border-(--color-border)" />
<!-- Book cover generation -->
<div class="flex flex-col gap-2">
<p class="text-xs font-medium text-(--color-muted) uppercase tracking-wide">{m.book_detail_admin_book_cover()}</p>
<div class="flex items-center gap-3 flex-wrap">
<button
onclick={rescrape}
disabled={scraping}
onclick={generateCover}
disabled={coverGenerating}
class="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors
{scraping ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-3)'}"
{coverGenerating ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-2)'}"
>
{#if scraping}
<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
{m.book_detail_rescraping()}
{#if coverGenerating}
<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
{:else}
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
{m.book_detail_rescrape_book()}
<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="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{/if}
{m.book_detail_admin_generate()}
</button>
{#if scrapeResult}
<span class="text-xs {scrapeResult === 'queued' ? 'text-green-400' : scrapeResult === 'busy' ? 'text-(--color-brand)' : 'text-(--color-danger)'}">
{scrapeResult === 'queued' ? m.catalogue_scrape_queued_badge() + '.' : scrapeResult === 'busy' ? m.catalogue_scrape_busy_badge() + '.' : m.common_error() + '.'}
</span>
{#if coverResult === 'error'}
<span class="text-xs text-(--color-danger)">{m.common_error()}</span>
{:else if coverResult === 'saved'}
<span class="text-xs text-green-400">{m.book_detail_admin_saved()}</span>
{/if}
</div>
<!-- Range scrape -->
<div class="flex flex-wrap items-end gap-3">
<div class="flex flex-col gap-1">
<label for="range-from" class="text-xs text-(--color-muted)">{m.book_detail_from_chapter()}</label>
<input
id="range-from"
type="number"
min="1"
bind:value={rangeFrom}
placeholder="1"
class="w-24 px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
/>
{#if coverPreview}
<div class="flex items-start gap-3 mt-1">
<img src={coverPreview} alt="Cover preview" class="w-24 rounded border border-(--color-border)" />
<div class="flex flex-col gap-2 pt-1">
<button
onclick={saveCover}
disabled={coverSaving}
class="px-3 py-1.5 rounded text-xs font-medium transition-colors
{coverSaving ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-green-600/20 text-green-400 hover:bg-green-600/30 border border-green-600/30'}"
>
{coverSaving ? m.book_detail_admin_saving() : m.book_detail_admin_save_cover()}
</button>
<button onclick={() => (coverPreview = null)} class="text-xs text-(--color-muted) hover:text-(--color-text) transition-colors">{m.book_detail_admin_discard()}</button>
</div>
</div>
{/if}
</div>
<!-- Chapter cover generation -->
<div class="flex flex-col gap-2">
<p class="text-xs font-medium text-(--color-muted) uppercase tracking-wide">{m.book_detail_admin_chapter_cover()}</p>
<div class="flex items-end gap-3 flex-wrap">
<div class="flex flex-col gap-1">
<label for="range-to" class="text-xs text-(--color-muted)">{m.book_detail_to_chapter()}</label>
<label for="ch-cover-n" class="text-xs text-(--color-muted)">{m.book_detail_admin_chapter_n()}</label>
<input
id="range-to"
id="ch-cover-n"
type="number"
min="1"
bind:value={rangeTo}
placeholder="end"
class="w-24 px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
bind:value={chapterCoverN}
placeholder="1"
class="w-20 px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
/>
</div>
<button
onclick={scrapeRange}
disabled={rangeScraping || !rangeFrom}
class="px-3 py-1.5 rounded text-xs font-medium transition-colors
{rangeScraping || !rangeFrom
? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed'
: 'bg-(--color-brand)/20 text-(--color-brand-dim) hover:bg-(--color-brand)/40 border border-(--color-brand)/30'}"
onclick={generateChapterCover}
disabled={chapterCoverGenerating || !chapterCoverN}
class="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors
{chapterCoverGenerating || !chapterCoverN ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-2)'}"
>
{rangeScraping ? m.book_detail_range_queuing() : m.book_detail_scrape_range()}
{#if chapterCoverGenerating}
<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
{:else}
<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="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{/if}
{m.book_detail_admin_generate()}
</button>
{#if rangeResult}
<span class="text-xs {rangeResult === 'queued' ? 'text-green-400' : rangeResult === 'busy' ? 'text-(--color-brand)' : 'text-(--color-danger)'}">
{rangeResult === 'queued' ? m.catalogue_scrape_queued_badge() + '.' : rangeResult === 'busy' ? m.catalogue_scrape_busy_badge() + '.' : m.common_error() + '.'}
</span>
{#if chapterCoverResult === 'error'}
<span class="text-xs text-(--color-danger)">{m.common_error()}</span>
{/if}
</div>
{#if chapterCoverPreview}
<div class="flex items-start gap-3 mt-1">
<img src={chapterCoverPreview} alt="Chapter cover preview" class="w-24 rounded border border-(--color-border)" />
<button onclick={() => (chapterCoverPreview = null)} class="text-xs text-(--color-muted) hover:text-(--color-text) transition-colors pt-1">{m.book_detail_admin_discard()}</button>
</div>
{/if}
</div>
<hr class="border-(--color-border)" />
<!-- Description generation -->
<div class="flex flex-col gap-2">
<p class="text-xs font-medium text-(--color-muted) uppercase tracking-wide">{m.book_detail_admin_description()}</p>
<div class="flex items-center gap-3 flex-wrap">
<button
onclick={generateDesc}
disabled={descGenerating}
class="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors
{descGenerating ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-2)'}"
>
{#if descGenerating}
<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
{:else}
<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="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
{/if}
{m.book_detail_admin_generate()}
</button>
{#if descResult === 'error'}
<span class="text-xs text-(--color-danger)">{m.common_error()}</span>
{:else if descResult === 'applied'}
<span class="text-xs text-green-400">{m.book_detail_admin_applied()}</span>
{/if}
</div>
{#if descPreview}
<div class="flex flex-col gap-2">
<textarea
bind:value={descPreview}
rows="5"
class="w-full px-2 py-1.5 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand) resize-y"
></textarea>
<div class="flex items-center gap-2">
<button
onclick={applyDesc}
disabled={descApplying}
class="px-3 py-1.5 rounded text-xs font-medium transition-colors
{descApplying ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-green-600/20 text-green-400 hover:bg-green-600/30 border border-green-600/30'}"
>
{descApplying ? m.book_detail_admin_applying() : m.book_detail_admin_apply()}
</button>
<button onclick={() => (descPreview = '')} class="text-xs text-(--color-muted) hover:text-(--color-text) transition-colors">{m.book_detail_admin_discard()}</button>
</div>
</div>
{/if}
</div>
<!-- Chapter names generation -->
<div class="flex flex-col gap-2">
<p class="text-xs font-medium text-(--color-muted) uppercase tracking-wide">{m.book_detail_admin_chapter_names()}</p>
<div class="flex items-center gap-3 flex-wrap">
<button
onclick={generateChapNames}
disabled={chapNamesGenerating}
class="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors
{chapNamesGenerating ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-2)'}"
>
{#if chapNamesGenerating}
<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
{:else}
<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="M4 6h16M4 10h16M4 14h10"/></svg>
{/if}
{m.book_detail_admin_generate()}
</button>
{#if chapNamesResult === 'error'}
<span class="text-xs text-(--color-danger)">{m.common_error()}</span>
{:else if chapNamesResult === 'applied'}
<span class="text-xs text-green-400">{m.book_detail_admin_applied()} ({chapNamesPreview.length > 0 ? chapNamesPreview.length : ''})</span>
{/if}
</div>
{#if chapNamesPreview.length > 0}
<div class="flex flex-col gap-1.5 max-h-48 overflow-y-auto rounded border border-(--color-border) p-2 bg-(--color-surface-3)">
{#each chapNamesPreview as ch}
<div class="flex gap-2 text-xs">
<span class="text-(--color-muted) flex-shrink-0 w-6 text-right">{ch.number}.</span>
<span class="text-(--color-text) truncate">{ch.new_title}</span>
</div>
{/each}
</div>
<div class="flex items-center gap-2">
<button
onclick={applyChapNames}
disabled={chapNamesApplying}
class="px-3 py-1.5 rounded text-xs font-medium transition-colors
{chapNamesApplying ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-green-600/20 text-green-400 hover:bg-green-600/30 border border-green-600/30'}"
>
{chapNamesApplying ? m.book_detail_admin_applying() : m.book_detail_admin_apply()} ({chapNamesPreview.length})
</button>
<button onclick={() => (chapNamesPreview = [])} class="text-xs text-(--color-muted) hover:text-(--color-text) transition-colors">{m.book_detail_admin_discard()}</button>
</div>
{/if}
</div>
<hr class="border-(--color-border)" />
<!-- Audio TTS bulk enqueue -->
<div class="flex flex-col gap-2">
<p class="text-xs font-medium text-(--color-muted) uppercase tracking-wide">{m.book_detail_admin_audio_tts()}</p>
<div class="flex flex-col gap-3">
<!-- Voice selector -->
<div class="flex flex-col gap-1">
<label for="audio-voice" class="text-xs text-(--color-muted)">{m.book_detail_admin_voice()}</label>
<select
id="audio-voice"
bind:value={audioVoice}
class="px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
>
{#if !audioVoicesLoaded}
<option value="af_bella">af_bella (loading…)</option>
{:else}
{#each audioVoices.filter(v => v.engine === 'kokoro') as v}
<option value={v.id}>{voiceLabel(v)}</option>
{/each}
{#each audioVoices.filter(v => v.engine === 'pocket-tts') as v}
<option value={v.id}>{voiceLabel(v)}</option>
{/each}
{#each audioVoices.filter(v => v.engine === 'cfai') as v}
<option value={v.id}>{voiceLabel(v)}</option>
{/each}
{/if}
</select>
</div>
<!-- Chapter range -->
<div class="flex items-end gap-3 flex-wrap">
<div class="flex flex-col gap-1">
<label for="audio-from" class="text-xs text-(--color-muted)">{m.book_detail_from_chapter()}</label>
<input
id="audio-from"
type="number"
min="1"
bind:value={audioFrom}
placeholder="1"
class="w-20 px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
/>
</div>
<div class="flex flex-col gap-1">
<label for="audio-to" class="text-xs text-(--color-muted)">{m.book_detail_to_chapter()}</label>
<input
id="audio-to"
type="number"
min="1"
bind:value={audioTo}
placeholder="end"
class="w-20 px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
/>
</div>
<button
onclick={enqueueAudio}
disabled={audioEnqueuing || !audioFrom}
class="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors
{audioEnqueuing || !audioFrom ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-(--color-brand)/20 text-(--color-brand-dim) hover:bg-(--color-brand)/40 border border-(--color-brand)/30'}"
>
{#if audioEnqueuing}
<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
{:else}
<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="M15.536 8.464a5 5 0 010 7.072M12 6v6m0 0l-2-2m2 2l2-2M6.343 17.657a8 8 0 010-11.314"/></svg>
{/if}
{m.book_detail_admin_enqueue_audio()}
</button>
<button
onclick={cancelAudio}
class="px-3 py-1.5 rounded text-xs font-medium text-(--color-muted) hover:text-(--color-danger) transition-colors border border-(--color-border)"
>
{m.book_detail_admin_cancel_audio()}
</button>
</div>
{#if audioResult}
<span class="text-xs text-green-400">{m.book_detail_admin_enqueued({ enqueued: audioResult.enqueued, skipped: audioResult.skipped })}</span>
{/if}
{#if audioError}
<span class="text-xs text-(--color-muted)">{audioError}</span>
{/if}
</div>
</div>
{/if}
</div>
{/if}
</div>
{/if}
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount, untrack, getContext } from 'svelte';
import { onMount, getContext } from 'svelte';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { page } from '$app/state';
@@ -11,8 +11,9 @@
let { data }: { data: PageData } = $props();
let html = $state(untrack(() => data.html));
let fetchingContent = $state(untrack(() => !data.isPreview && !data.html));
let scrapedHtml = $state(''); // only set by the live-preview fallback
let html = $derived(scrapedHtml || data.html || '');
let fetchingContent = $state(false);
let fetchError = $state('');
let audioProRequired = $state(false);
@@ -202,8 +203,8 @@
{ code: 'pt', label: 'PT' },
{ code: 'fr', label: 'FR' }
];
let translationStatus = $state(untrack(() => data.translationStatus ?? 'idle'));
let translatingLang = $state(untrack(() => data.lang ?? ''));
let translationStatus = $state(data.translationStatus ?? 'idle');
let translatingLang = $state(data.lang ?? '');
let pollingTimer: ReturnType<typeof setTimeout> | null = null;
function currentLang() {
@@ -293,6 +294,7 @@
// If the normal path returned no content, fall back to live preview scrape
if (!data.isPreview && !data.html) {
fetchingContent = true;
(async () => {
try {
const res = await fetch(
@@ -302,7 +304,7 @@
const d = (await res.json()) as { text?: string };
if (d.text) {
const { marked } = await import('marked');
html = await marked(d.text, { async: true });
scrapedHtml = await marked(d.text, { async: true });
} else {
fetchError = m.reader_audio_error();
}

View File

@@ -375,7 +375,7 @@
</div>
<div class="mt-5 pt-5 border-t border-(--color-border)">
<p class="text-sm font-medium text-(--color-text) mb-1">{m.profile_upgrade_heading()}</p>
<p class="text-sm text-(--color-muted) mb-4">{m.profile_upgrade_desc()}</p>
<p class="text-sm text-(--color-muted) mb-4">{m.profile_upgrade_desc()} <a href="/subscribe" class="text-(--color-brand) hover:underline">See plans →</a></p>
{#if checkoutError}
<p class="text-sm text-(--color-danger) mb-3">{checkoutError}</p>
{/if}

View File

@@ -0,0 +1,5 @@
// Data is inherited from the root layout (user, isPro).
// This file exists only to ensure the route is treated as a page by SvelteKit.
export const load = async () => {
return {};
};

View File

@@ -0,0 +1,162 @@
<script lang="ts">
import type { LayoutData } from '../$types';
import * as m from '$lib/paraglide/messages.js';
// Data flows from root layout (user, isPro)
let { data }: { data: LayoutData } = $props();
let checkoutLoading = $state<'monthly' | 'annual' | null>(null);
let checkoutError = $state('');
async function startCheckout(product: 'monthly' | 'annual') {
checkoutLoading = product;
checkoutError = '';
try {
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product })
});
if (!res.ok) {
const body = await res.json().catch(() => ({})) as { message?: string };
checkoutError = body.message ?? `Checkout failed (${res.status}). Please try again.`;
return;
}
const { url } = await res.json() as { url: string };
window.location.href = url;
} catch {
checkoutError = 'Network error. Please try again.';
} finally {
checkoutLoading = null;
}
}
const benefits = [
{ icon: 'M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3', label: () => m.subscribe_benefit_audio() },
{ icon: 'M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z', label: () => m.subscribe_benefit_voices() },
{ icon: 'M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129', label: () => m.subscribe_benefit_translation() },
{ icon: 'M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4', label: () => m.subscribe_benefit_downloads() },
];
</script>
<svelte:head>
<title>{m.subscribe_page_title()}</title>
</svelte:head>
<div class="min-h-[calc(100vh-3.5rem)] flex flex-col items-center justify-center px-4 py-16">
<!-- Hero -->
<div class="text-center mb-12 max-w-xl">
<div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-(--color-brand)/15 border border-(--color-brand)/30 text-(--color-brand) text-xs font-semibold uppercase tracking-wider mb-5">
<svg class="w-3.5 h-3.5 shrink-0" fill="currentColor" viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm4.24 16L12 15.45 7.77 18l1.12-4.81-3.73-3.23 4.92-.42L12 5l1.92 4.53 4.92.42-3.73 3.23L16.23 18z"/></svg>
Pro
</div>
<h1 class="text-4xl sm:text-5xl font-bold text-(--color-text) tracking-tight mb-4">
{m.subscribe_heading()}
</h1>
<p class="text-lg text-(--color-muted)">
{m.subscribe_subheading()}
</p>
</div>
<!-- Benefits list -->
<ul class="mb-12 space-y-3 w-full max-w-sm">
{#each benefits as b}
<li class="flex items-center gap-3 text-sm text-(--color-text)">
<span class="shrink-0 w-8 h-8 rounded-full bg-(--color-brand)/15 text-(--color-brand) flex items-center justify-center">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d={b.icon}/>
</svg>
</span>
{b.label()}
</li>
{/each}
</ul>
{#if data.isPro}
<!-- Already Pro -->
<div class="w-full max-w-sm bg-(--color-surface-2) border border-(--color-brand)/40 rounded-xl p-6 text-center">
<p class="text-base font-semibold text-(--color-text) mb-1">{m.subscribe_already_pro()}</p>
<a
href="https://polar.sh/libnovel/portal"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 mt-3 text-sm font-medium text-(--color-brand) hover:underline"
>
{m.subscribe_manage()}
<svg class="w-3.5 h-3.5" 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"/></svg>
</a>
</div>
{:else if !data.user}
<!-- Not logged in -->
<div class="w-full max-w-sm bg-(--color-surface-2) border border-(--color-border) rounded-xl p-6 text-center">
<p class="text-sm text-(--color-muted) mb-4">{m.subscribe_login_prompt()}</p>
<a
href="/login?next=/subscribe"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-(--color-brand) text-(--color-surface) font-semibold text-sm hover:bg-(--color-brand-dim) transition-colors"
>
{m.subscribe_login_cta()}
</a>
</div>
{:else}
<!-- Pricing cards -->
{#if checkoutError}
<p class="text-sm text-(--color-danger) mb-4 text-center">{checkoutError}</p>
{/if}
<div class="w-full max-w-sm grid gap-4">
<!-- Annual card (featured) -->
<div class="relative bg-(--color-surface-2) border-2 border-(--color-brand) rounded-xl p-6">
<div class="absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-0.5 bg-(--color-brand) text-(--color-surface) text-xs font-bold rounded-full tracking-wide uppercase">
{m.subscribe_annual_save()}
</div>
<div class="flex items-baseline justify-between mb-5">
<span class="text-base font-semibold text-(--color-text)">{m.subscribe_annual_label()}</span>
<div class="text-right">
<span class="text-3xl font-bold text-(--color-text)">{m.subscribe_annual_price()}</span>
<span class="text-sm text-(--color-muted) ml-1">{m.subscribe_annual_period()}</span>
</div>
</div>
<button
type="button"
onclick={() => startCheckout('annual')}
disabled={checkoutLoading !== null}
class="w-full flex items-center justify-center gap-2 py-2.5 rounded-lg bg-(--color-brand) text-(--color-surface) font-semibold text-sm hover:bg-(--color-brand-dim) transition-colors disabled:opacity-60 disabled:cursor-wait"
>
{#if checkoutLoading === 'annual'}
<svg class="w-4 h-4 shrink-0 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"/></svg>
{/if}
{m.subscribe_cta_annual()}
</button>
</div>
<!-- Monthly card -->
<div class="bg-(--color-surface-2) border border-(--color-border) rounded-xl p-6">
<div class="flex items-baseline justify-between mb-5">
<span class="text-base font-semibold text-(--color-text)">{m.subscribe_monthly_label()}</span>
<div class="text-right">
<span class="text-3xl font-bold text-(--color-text)">{m.subscribe_monthly_price()}</span>
<span class="text-sm text-(--color-muted) ml-1">{m.subscribe_monthly_period()}</span>
</div>
</div>
<button
type="button"
onclick={() => startCheckout('monthly')}
disabled={checkoutLoading !== null}
class="w-full flex items-center justify-center gap-2 py-2.5 rounded-lg border border-(--color-brand) text-(--color-brand) font-semibold text-sm hover:bg-(--color-brand)/10 transition-colors disabled:opacity-60 disabled:cursor-wait"
>
{#if checkoutLoading === 'monthly'}
<svg class="w-4 h-4 shrink-0 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"/></svg>
{/if}
{m.subscribe_cta_monthly()}
</button>
</div>
</div>
{/if}
<p class="mt-8 text-xs text-(--color-muted) text-center max-w-xs">
Payments processed securely by <a href="https://polar.sh" target="_blank" rel="noopener noreferrer" class="underline hover:text-(--color-text)">Polar</a>. Cancel anytime.
</p>
</div>