diff --git a/apps/backoffice/app/books/[id]/page.tsx b/apps/backoffice/app/books/[id]/page.tsx index f913d65..65b0822 100644 --- a/apps/backoffice/app/books/[id]/page.tsx +++ b/apps/backoffice/app/books/[id]/page.tsx @@ -4,16 +4,17 @@ import { ConvertButton } from "../../components/ConvertButton"; import { MarkBookReadButton } from "../../components/MarkBookReadButton"; import { EditBookForm } from "../../components/EditBookForm"; import { SafeHtml } from "../../components/SafeHtml"; +import { getServerTranslations } from "../../../lib/i18n/server"; import Image from "next/image"; import Link from "next/link"; import { notFound } from "next/navigation"; export const dynamic = "force-dynamic"; -const readingStatusConfig: Record = { - unread: { label: "Non lu", className: "bg-muted/60 text-muted-foreground border border-border" }, - reading: { label: "En cours", className: "bg-amber-500/15 text-amber-600 dark:text-amber-400 border border-amber-500/30" }, - read: { label: "Lu", className: "bg-green-500/15 text-green-600 dark:text-green-400 border border-green-500/30" }, +const readingStatusClassNames: Record = { + unread: "bg-muted/60 text-muted-foreground border border-border", + reading: "bg-amber-500/15 text-amber-600 dark:text-amber-400 border border-amber-500/30", + read: "bg-green-500/15 text-green-600 dark:text-green-400 border border-green-500/30", }; async function fetchBook(bookId: string): Promise { @@ -39,6 +40,8 @@ export default async function BookDetailPage({ notFound(); } + const { t, locale } = await getServerTranslations(); + const library = libraries.find(l => l.id === book.library_id); const formatBadge = (book.format ?? book.kind).toUpperCase(); const formatColor = @@ -46,14 +49,15 @@ export default async function BookDetailPage({ formatBadge === "CBR" ? "bg-warning/10 text-warning border-warning/30" : formatBadge === "PDF" ? "bg-destructive/10 text-destructive border-destructive/30" : "bg-muted/50 text-muted-foreground border-border"; - const { label: statusLabel, className: statusClassName } = readingStatusConfig[book.reading_status]; + const statusLabel = t(`status.${book.reading_status}` as "status.unread" | "status.reading" | "status.read"); + const statusClassName = readingStatusClassNames[book.reading_status]; return (
{/* Breadcrumb */}
- Bibliothèques + {t("bookDetail.libraries")} / {library && ( @@ -88,7 +92,7 @@ export default async function BookDetailPage({
{`Couverture {book.reading_last_read_at && ( - {new Date(book.reading_last_read_at).toLocaleDateString()} + {new Date(book.reading_last_read_at).toLocaleDateString(locale)} )} @@ -148,7 +152,7 @@ export default async function BookDetailPage({ {book.page_count && ( - {book.page_count} pages + {book.page_count} {t("dashboard.pages").toLowerCase()} )} {book.language && ( @@ -181,24 +185,24 @@ export default async function BookDetailPage({ - Informations techniques + {t("bookDetail.technicalInfo")}
{book.file_path && (
- Fichier + {t("bookDetail.file")} {book.file_path}
)} {book.file_format && (
- Format fichier + {t("bookDetail.fileFormat")} {book.file_format.toUpperCase()}
)} {book.file_parse_status && (
- Parsing + {t("bookDetail.parsing")} {book.updated_at && (
- Mis à jour - {new Date(book.updated_at).toLocaleString()} + {t("bookDetail.updatedAt")} + {new Date(book.updated_at).toLocaleString(locale)}
)}
diff --git a/apps/backoffice/app/books/page.tsx b/apps/backoffice/app/books/page.tsx index 989c572..ab0945a 100644 --- a/apps/backoffice/app/books/page.tsx +++ b/apps/backoffice/app/books/page.tsx @@ -4,6 +4,7 @@ import { LiveSearchForm } from "../components/LiveSearchForm"; import { Card, CardContent, OffsetPagination } from "../components/ui"; import Link from "next/link"; import Image from "next/image"; +import { getServerTranslations } from "../../lib/i18n/server"; export const dynamic = "force-dynamic"; @@ -12,6 +13,7 @@ export default async function BooksPage({ }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { + const { t } = await getServerTranslations(); const searchParamsAwaited = await searchParams; const libraryId = typeof searchParamsAwaited.library === "string" ? searchParamsAwaited.library : undefined; const searchQuery = typeof searchParamsAwaited.q === "string" ? searchParamsAwaited.q : ""; @@ -78,20 +80,20 @@ export default async function BooksPage({ const totalPages = Math.ceil(total / limit); const libraryOptions = [ - { value: "", label: "Toutes les bibliothèques" }, + { value: "", label: t("books.allLibraries") }, ...libraries.map((lib) => ({ value: lib.id, label: lib.name })), ]; const statusOptions = [ - { value: "", label: "Tous" }, - { value: "unread", label: "Non lu" }, - { value: "reading", label: "En cours" }, - { value: "read", label: "Lu" }, + { value: "", label: t("common.all") }, + { value: "unread", label: t("status.unread") }, + { value: "reading", label: t("status.reading") }, + { value: "read", label: t("status.read") }, ]; const sortOptions = [ - { value: "", label: "Titre" }, - { value: "latest", label: "Ajout récent" }, + { value: "", label: t("books.sortTitle") }, + { value: "latest", label: t("books.sortLatest") }, ]; const hasFilters = searchQuery || libraryId || readingStatus || sort; @@ -103,7 +105,7 @@ export default async function BooksPage({ - Livres + {t("books.title")}
@@ -112,10 +114,10 @@ export default async function BooksPage({ @@ -124,18 +126,18 @@ export default async function BooksPage({ {/* Résultats */} {searchQuery && totalHits !== null ? (

- {totalHits} résultat{totalHits !== 1 ? 's' : ''} pour « {searchQuery} » + {t("books.resultCountFor", { count: String(totalHits), plural: totalHits !== 1 ? "s" : "", query: searchQuery })}

) : !searchQuery && (

- {total} livre{total !== 1 ? 's' : ''} + {t("books.resultCount", { count: String(total), plural: total !== 1 ? "s" : "" })}

)} {/* Séries matchantes */} {seriesHits.length > 0 && (
-

Séries

+

{t("books.seriesHeading")}

{seriesHits.map((s) => ( {`Couverture

- {s.name === "unclassified" ? "Non classé" : s.name} + {s.name === "unclassified" ? t("books.unclassified") : s.name}

- {s.book_count} livre{s.book_count !== 1 ? 's' : ''} + {t("books.bookCount", { count: String(s.book_count), plural: s.book_count !== 1 ? "s" : "" })}

@@ -171,7 +173,7 @@ export default async function BooksPage({ {/* Grille de livres */} {displayBooks.length > 0 ? ( <> - {searchQuery &&

Livres

} + {searchQuery &&

{t("books.title")}

} {!searchQuery && ( @@ -184,7 +186,7 @@ export default async function BooksPage({ )} ) : ( - + )} ); diff --git a/apps/backoffice/app/components/BookCard.tsx b/apps/backoffice/app/components/BookCard.tsx index b486b8a..57c9693 100644 --- a/apps/backoffice/app/components/BookCard.tsx +++ b/apps/backoffice/app/components/BookCard.tsx @@ -4,11 +4,12 @@ import { useState } from "react"; import Image from "next/image"; import Link from "next/link"; import { BookDto, ReadingStatus } from "../../lib/api"; +import { useTranslation } from "../../lib/i18n/context"; -const readingStatusOverlay: Record = { +const readingStatusOverlayClasses: Record = { unread: null, - reading: { label: "En cours", className: "bg-amber-500/90 text-white" }, - read: { label: "Lu", className: "bg-green-600/90 text-white" }, + reading: "bg-amber-500/90 text-white", + read: "bg-green-600/90 text-white", }; interface BookCardProps { @@ -57,9 +58,15 @@ function BookImage({ src, alt }: { src: string; alt: string }) { } export function BookCard({ book, readingStatus }: BookCardProps) { + const { t } = useTranslation(); const coverUrl = book.coverUrl || `/api/books/${book.id}/thumbnail`; const status = readingStatus ?? book.reading_status; - const overlay = status ? readingStatusOverlay[status] : null; + const overlayClass = status ? readingStatusOverlayClasses[status] : null; + const statusLabels: Record = { + unread: t("status.unread"), + reading: t("status.reading"), + read: t("status.read"), + }; const isRead = status === "read"; @@ -71,11 +78,11 @@ export function BookCard({ book, readingStatus }: BookCardProps) {
- {overlay && ( - - {overlay.label} + {overlayClass && status && ( + + {statusLabels[status]} )}
diff --git a/apps/backoffice/app/components/BookPreview.tsx b/apps/backoffice/app/components/BookPreview.tsx index d9c87da..ce37f8f 100644 --- a/apps/backoffice/app/components/BookPreview.tsx +++ b/apps/backoffice/app/components/BookPreview.tsx @@ -2,10 +2,12 @@ import { useState } from "react"; import Image from "next/image"; +import { useTranslation } from "../../lib/i18n/context"; const PAGE_SIZE = 5; export function BookPreview({ bookId, pageCount }: { bookId: string; pageCount: number }) { + const { t } = useTranslation(); const [offset, setOffset] = useState(0); const pages = Array.from({ length: PAGE_SIZE }, (_, i) => offset + i + 1).filter( @@ -16,9 +18,9 @@ export function BookPreview({ bookId, pageCount }: { bookId: string; pageCount:

- Aperçu + {t("bookPreview.preview")} - pages {offset + 1}–{Math.min(offset + PAGE_SIZE, pageCount)} / {pageCount} + {t("bookPreview.pages", { start: offset + 1, end: Math.min(offset + PAGE_SIZE, pageCount), total: pageCount })}

@@ -27,14 +29,14 @@ export function BookPreview({ bookId, pageCount }: { bookId: string; pageCount: disabled={offset === 0} className="px-3 py-1.5 text-sm rounded-lg border border-border bg-muted/50 text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors" > - ← Préc. + {t("bookPreview.prev")}
diff --git a/apps/backoffice/app/components/ConvertButton.tsx b/apps/backoffice/app/components/ConvertButton.tsx index f7c8cf7..1130274 100644 --- a/apps/backoffice/app/components/ConvertButton.tsx +++ b/apps/backoffice/app/components/ConvertButton.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import Link from "next/link"; import { Button } from "./ui"; +import { useTranslation } from "../../lib/i18n/context"; interface ConvertButtonProps { bookId: string; @@ -15,6 +16,7 @@ type ConvertState = | { type: "error"; message: string }; export function ConvertButton({ bookId }: ConvertButtonProps) { + const { t } = useTranslation(); const [state, setState] = useState({ type: "idle" }); const handleConvert = async () => { @@ -23,22 +25,22 @@ export function ConvertButton({ bookId }: ConvertButtonProps) { const res = await fetch(`/api/books/${bookId}/convert`, { method: "POST" }); if (!res.ok) { const body = await res.json().catch(() => ({ error: res.statusText })); - setState({ type: "error", message: body.error || "Échec de la conversion" }); + setState({ type: "error", message: body.error || t("convert.failed") }); return; } const job = await res.json(); setState({ type: "success", jobId: job.id }); } catch (err) { - setState({ type: "error", message: err instanceof Error ? err.message : "Erreur inconnue" }); + setState({ type: "error", message: err instanceof Error ? err.message : t("convert.unknownError") }); } }; if (state.type === "success") { return (
- Conversion lancée. + {t("convert.started")} - Voir la tâche → + {t("convert.viewJob")}
); @@ -52,7 +54,7 @@ export function ConvertButton({ bookId }: ConvertButtonProps) { className="text-xs text-muted-foreground hover:underline text-left" onClick={() => setState({ type: "idle" })} > - Fermer + {t("common.close")}
); @@ -65,7 +67,7 @@ export function ConvertButton({ bookId }: ConvertButtonProps) { onClick={handleConvert} disabled={state.type === "loading"} > - {state.type === "loading" ? "Conversion…" : "Convertir en CBZ"} + {state.type === "loading" ? t("convert.converting") : t("convert.convertToCbz")} ); } diff --git a/apps/backoffice/app/components/EditBookForm.tsx b/apps/backoffice/app/components/EditBookForm.tsx index 96e8154..f65ac38 100644 --- a/apps/backoffice/app/components/EditBookForm.tsx +++ b/apps/backoffice/app/components/EditBookForm.tsx @@ -5,6 +5,7 @@ import { createPortal } from "react-dom"; import { useRouter } from "next/navigation"; import { BookDto } from "@/lib/api"; import { FormField, FormLabel, FormInput } from "./ui/Form"; +import { useTranslation } from "../../lib/i18n/context"; function LockButton({ locked, @@ -15,6 +16,7 @@ function LockButton({ onToggle: () => void; disabled?: boolean; }) { + const { t } = useTranslation(); return ( @@ -227,7 +230,7 @@ export function EditBookForm({ book }: EditBookFormProps) { onChange={(e) => setAuthorInput(e.target.value)} onKeyDown={handleAuthorKeyDown} disabled={isPending} - placeholder="Ajouter un auteur (Entrée pour valider)" + placeholder={t("editBook.addAuthor")} className="flex h-10 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" />