refactor: streamline LibraryPage component by integrating ClientLibraryPage for improved structure and error handling
This commit is contained in:
83
src/app/libraries/[libraryId]/ClientLibraryPage.tsx
Normal file
83
src/app/libraries/[libraryId]/ClientLibraryPage.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { PaginatedSeriesGrid } from "@/components/library/PaginatedSeriesGrid";
|
||||||
|
import { RefreshButton } from "@/components/library/RefreshButton";
|
||||||
|
import { ErrorMessage } from "@/components/ui/ErrorMessage";
|
||||||
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
|
import type { LibraryResponse } from "@/types/library";
|
||||||
|
import type { KomgaSeries, KomgaLibrary } from "@/types/komga";
|
||||||
|
import type { UserPreferences } from "@/types/preferences";
|
||||||
|
|
||||||
|
interface ClientLibraryPageProps {
|
||||||
|
library: KomgaLibrary | null;
|
||||||
|
series: LibraryResponse<KomgaSeries> | null;
|
||||||
|
currentPage: number;
|
||||||
|
libraryId: string;
|
||||||
|
refreshLibrary: (libraryId: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
preferences: UserPreferences;
|
||||||
|
unreadOnly: boolean;
|
||||||
|
errorCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClientLibraryPage({
|
||||||
|
library,
|
||||||
|
series,
|
||||||
|
currentPage,
|
||||||
|
libraryId,
|
||||||
|
refreshLibrary,
|
||||||
|
preferences,
|
||||||
|
unreadOnly,
|
||||||
|
errorCode,
|
||||||
|
}: ClientLibraryPageProps) {
|
||||||
|
const { t } = useTranslate();
|
||||||
|
|
||||||
|
if (errorCode) {
|
||||||
|
return (
|
||||||
|
<div className="container py-8 space-y-8">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-3xl font-bold">
|
||||||
|
{library?.name || t("series.empty")}
|
||||||
|
</h1>
|
||||||
|
<RefreshButton libraryId={libraryId} refreshLibrary={refreshLibrary} />
|
||||||
|
</div>
|
||||||
|
<ErrorMessage errorCode={errorCode} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!library || !series) {
|
||||||
|
return (
|
||||||
|
<div className="container py-8 space-y-8">
|
||||||
|
<ErrorMessage errorCode="SERIES_FETCH_ERROR" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container py-8 space-y-8">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-3xl font-bold">{library.name}</h1>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{series.totalElements > 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("series.display.showing", {
|
||||||
|
start: ((currentPage - 1) * (preferences.displayMode?.itemsPerPage || 20)) + 1,
|
||||||
|
end: Math.min(currentPage * (preferences.displayMode?.itemsPerPage || 20), series.totalElements),
|
||||||
|
total: series.totalElements,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<RefreshButton libraryId={libraryId} refreshLibrary={refreshLibrary} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PaginatedSeriesGrid
|
||||||
|
series={series.content || []}
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={series.totalPages}
|
||||||
|
totalElements={series.totalElements}
|
||||||
|
defaultShowOnlyUnread={preferences.showOnlyUnread}
|
||||||
|
showOnlyUnread={unreadOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import { PaginatedSeriesGrid } from "@/components/library/PaginatedSeriesGrid";
|
|
||||||
import { LibraryService } from "@/lib/services/library.service";
|
import { LibraryService } from "@/lib/services/library.service";
|
||||||
import { PreferencesService } from "@/lib/services/preferences.service";
|
import { PreferencesService } from "@/lib/services/preferences.service";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { RefreshButton } from "@/components/library/RefreshButton";
|
|
||||||
import { withPageTiming } from "@/lib/hoc/withPageTiming";
|
import { withPageTiming } from "@/lib/hoc/withPageTiming";
|
||||||
import { ErrorMessage } from "@/components/ui/ErrorMessage";
|
|
||||||
import type { LibraryResponse } from "@/types/library";
|
import type { LibraryResponse } from "@/types/library";
|
||||||
import type { KomgaSeries, KomgaLibrary } from "@/types/komga";
|
import type { KomgaSeries, KomgaLibrary } from "@/types/komga";
|
||||||
import type { UserPreferences } from "@/types/preferences";
|
import type { UserPreferences } from "@/types/preferences";
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
|
import { ClientLibraryPage } from "./ClientLibraryPage";
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: { libraryId: string };
|
params: { libraryId: string };
|
||||||
@@ -27,8 +25,8 @@ async function refreshLibrary(libraryId: string) {
|
|||||||
revalidatePath(`/libraries/${libraryId}`);
|
revalidatePath(`/libraries/${libraryId}`);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors du rafraîchissement:", error);
|
console.error("Error during refresh:", error);
|
||||||
return { success: false, error: "Erreur lors du rafraîchissement de la bibliothèque" };
|
return { success: false, error: "Error refreshing library" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,44 +73,42 @@ async function LibraryPage({ params, searchParams }: PageProps) {
|
|||||||
await getLibrarySeries(libraryId, currentPage, unreadOnly, search, pageSize);
|
await getLibrarySeries(libraryId, currentPage, unreadOnly, search, pageSize);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container py-8 space-y-8">
|
<ClientLibraryPage
|
||||||
<div className="flex items-center justify-between">
|
library={library}
|
||||||
<h1 className="text-3xl font-bold">{library.name}</h1>
|
series={series}
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{series.totalElements > 0 && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{series.totalElements} série{series.totalElements > 1 ? "s" : ""}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<RefreshButton libraryId={libraryId} refreshLibrary={refreshLibrary} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<PaginatedSeriesGrid
|
|
||||||
series={series.content || []}
|
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={series.totalPages}
|
libraryId={libraryId}
|
||||||
totalElements={series.totalElements}
|
refreshLibrary={refreshLibrary}
|
||||||
defaultShowOnlyUnread={preferences.showOnlyUnread}
|
preferences={preferences}
|
||||||
showOnlyUnread={unreadOnly}
|
unreadOnly={unreadOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
return (
|
return (
|
||||||
<div className="container py-8 space-y-8">
|
<ClientLibraryPage
|
||||||
<div className="flex items-center justify-between">
|
library={null}
|
||||||
<h1 className="text-3xl font-bold">Séries</h1>
|
series={null}
|
||||||
<RefreshButton libraryId={libraryId} refreshLibrary={refreshLibrary} />
|
currentPage={currentPage}
|
||||||
</div>
|
libraryId={libraryId}
|
||||||
<ErrorMessage errorCode={error.code} />
|
refreshLibrary={refreshLibrary}
|
||||||
</div>
|
preferences={preferences}
|
||||||
|
unreadOnly={unreadOnly}
|
||||||
|
errorCode={error.code}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="container py-8 space-y-8">
|
<ClientLibraryPage
|
||||||
<ErrorMessage errorCode={ERROR_CODES.SERIES.FETCH_ERROR} />
|
library={null}
|
||||||
</div>
|
series={null}
|
||||||
|
currentPage={currentPage}
|
||||||
|
libraryId={libraryId}
|
||||||
|
refreshLibrary={refreshLibrary}
|
||||||
|
preferences={preferences}
|
||||||
|
unreadOnly={unreadOnly}
|
||||||
|
errorCode={ERROR_CODES.SERIES.FETCH_ERROR}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import type { KomgaBook, KomgaSeries } from "@/types/komga";
|
import type { KomgaBook, KomgaSeries } from "@/types/komga";
|
||||||
import { BookCover } from "../ui/book-cover";
|
import { BookCover } from "../ui/book-cover";
|
||||||
import { SeriesCover } from "../ui/series-cover";
|
import { SeriesCover } from "../ui/series-cover";
|
||||||
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
|
|
||||||
interface BaseItem {
|
interface BaseItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -43,6 +44,7 @@ export function MediaRow({ title, items, icon }: MediaRowProps) {
|
|||||||
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
||||||
const [showRightArrow, setShowRightArrow] = useState(true);
|
const [showRightArrow, setShowRightArrow] = useState(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { t } = useTranslate();
|
||||||
|
|
||||||
const onItemClick = (item: OptimizedSeries | OptimizedBook) => {
|
const onItemClick = (item: OptimizedSeries | OptimizedBook) => {
|
||||||
const path = "booksCount" in item ? `/series/${item.id}` : `/books/${item.id}`;
|
const path = "booksCount" in item ? `/series/${item.id}` : `/books/${item.id}`;
|
||||||
@@ -78,7 +80,7 @@ export function MediaRow({ title, items, icon }: MediaRowProps) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => scroll("left")}
|
onClick={() => scroll("left")}
|
||||||
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 p-2 rounded-full bg-background/90 shadow-md border transition-opacity"
|
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 p-2 rounded-full bg-background/90 shadow-md border transition-opacity"
|
||||||
aria-label="Défiler vers la gauche"
|
aria-label={t("navigation.scrollLeft")}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-6 w-6" />
|
<ChevronLeft className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
@@ -100,7 +102,7 @@ export function MediaRow({ title, items, icon }: MediaRowProps) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => scroll("right")}
|
onClick={() => scroll("right")}
|
||||||
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 p-2 rounded-full bg-background/90 shadow-md border transition-opacity"
|
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 p-2 rounded-full bg-background/90 shadow-md border transition-opacity"
|
||||||
aria-label="Défiler vers la droite"
|
aria-label={t("navigation.scrollRight")}
|
||||||
>
|
>
|
||||||
<ChevronRight className="h-6 w-6" />
|
<ChevronRight className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
@@ -116,10 +118,14 @@ interface MediaCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MediaCard({ item, onClick }: MediaCardProps) {
|
function MediaCard({ item, onClick }: MediaCardProps) {
|
||||||
|
const { t } = useTranslate();
|
||||||
const isSeries = "booksCount" in item;
|
const isSeries = "booksCount" in item;
|
||||||
const title = isSeries
|
const title = isSeries
|
||||||
? item.metadata.title
|
? item.metadata.title
|
||||||
: item.metadata.title || `Tome ${item.metadata.number}`;
|
: item.metadata.title ||
|
||||||
|
(item.metadata.number
|
||||||
|
? t("navigation.volume", { number: item.metadata.number })
|
||||||
|
: "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -133,7 +139,7 @@ function MediaCard({ item, onClick }: MediaCardProps) {
|
|||||||
<div className="absolute inset-0 bg-black/60 opacity-0 hover:opacity-100 transition-opacity duration-200 flex flex-col justify-end p-3">
|
<div className="absolute inset-0 bg-black/60 opacity-0 hover:opacity-100 transition-opacity duration-200 flex flex-col justify-end p-3">
|
||||||
<h3 className="font-medium text-sm text-white line-clamp-2">{title}</h3>
|
<h3 className="font-medium text-sm text-white line-clamp-2">{title}</h3>
|
||||||
<p className="text-xs text-white/80 mt-1">
|
<p className="text-xs text-white/80 mt-1">
|
||||||
{item.booksCount} tome{item.booksCount > 1 ? "s" : ""}
|
{t("series.books", { count: item.booksCount })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -105,24 +105,9 @@ export function PaginatedSeriesGrid({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate start and end indices for display
|
|
||||||
const startIndex = (currentPage - 1) * itemsPerPage + 1;
|
|
||||||
const endIndex = Math.min(currentPage * itemsPerPage, totalElements);
|
|
||||||
|
|
||||||
const getShowingText = () => {
|
|
||||||
if (!totalElements) return t("series.empty");
|
|
||||||
|
|
||||||
return t("series.display.showing", {
|
|
||||||
start: startIndex,
|
|
||||||
end: endIndex,
|
|
||||||
total: totalElements,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<p className="text-sm text-muted-foreground text-right">{getShowingText()}</p>
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<SearchInput placeholder={t("series.filters.search")} />
|
<SearchInput placeholder={t("series.filters.search")} />
|
||||||
|
|||||||
@@ -85,7 +85,10 @@ export function BookGrid({ books, onBookClick, isCompact = false }: BookGridProp
|
|||||||
<BookCover
|
<BookCover
|
||||||
book={book}
|
book={book}
|
||||||
alt={t("books.coverAlt", {
|
alt={t("books.coverAlt", {
|
||||||
title: book.metadata.title || `Tome ${book.metadata.number}`,
|
title: book.metadata.title ||
|
||||||
|
(book.metadata.number
|
||||||
|
? t("navigation.volume", { number: book.metadata.number })
|
||||||
|
: ""),
|
||||||
})}
|
})}
|
||||||
onSuccess={(book, action) => handleOnSuccess(book, action)}
|
onSuccess={(book, action) => handleOnSuccess(book, action)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -158,7 +158,10 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
|
|||||||
{statusInfo.label}
|
{statusInfo.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-white/80">
|
<span className="text-sm text-white/80">
|
||||||
{t("series.header.books", { count: series.booksCount })}
|
{series.booksCount === 1
|
||||||
|
? t("series.header.books", { count: series.booksCount })
|
||||||
|
: t("series.header.books_plural", { count: series.booksCount })
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const getReadingStatusInfo = (book: KomgaBook, t: (key: string, options?: any) =
|
|||||||
|
|
||||||
export function BookCover({
|
export function BookCover({
|
||||||
book,
|
book,
|
||||||
alt = "Image de couverture",
|
alt,
|
||||||
className,
|
className,
|
||||||
showProgressUi = true,
|
showProgressUi = true,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
@@ -84,7 +84,7 @@ export function BookCover({
|
|||||||
<div className="relative w-full h-full">
|
<div className="relative w-full h-full">
|
||||||
<CoverClient
|
<CoverClient
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
alt={alt}
|
alt={alt || t("books.defaultCoverAlt")}
|
||||||
className={className}
|
className={className}
|
||||||
isCompleted={isCompleted}
|
isCompleted={isCompleted}
|
||||||
/>
|
/>
|
||||||
@@ -121,7 +121,10 @@ export function BookCover({
|
|||||||
{showOverlay && overlayVariant === "default" && (
|
{showOverlay && overlayVariant === "default" && (
|
||||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-4 space-y-2 translate-y-full group-hover:translate-y-0 transition-transform duration-200">
|
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-4 space-y-2 translate-y-full group-hover:translate-y-0 transition-transform duration-200">
|
||||||
<p className="text-sm font-medium text-white text-left line-clamp-2">
|
<p className="text-sm font-medium text-white text-left line-clamp-2">
|
||||||
{book.metadata.title || `Tome ${book.metadata.number}`}
|
{book.metadata.title ||
|
||||||
|
(book.metadata.number
|
||||||
|
? t("navigation.volume", { number: book.metadata.number })
|
||||||
|
: "")}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`px-2 py-0.5 rounded-full text-xs ${statusInfo.className}`}>
|
<span className={`px-2 py-0.5 rounded-full text-xs ${statusInfo.className}`}>
|
||||||
@@ -135,10 +138,16 @@ export function BookCover({
|
|||||||
{showOverlay && overlayVariant === "home" && (
|
{showOverlay && overlayVariant === "home" && (
|
||||||
<div className="absolute inset-0 bg-black/60 opacity-0 hover:opacity-100 transition-opacity duration-200 flex flex-col justify-end p-3">
|
<div className="absolute inset-0 bg-black/60 opacity-0 hover:opacity-100 transition-opacity duration-200 flex flex-col justify-end p-3">
|
||||||
<h3 className="font-medium text-sm text-white line-clamp-2">
|
<h3 className="font-medium text-sm text-white line-clamp-2">
|
||||||
{book.metadata.title || `Tome ${book.metadata.number}`}
|
{book.metadata.title ||
|
||||||
|
(book.metadata.number
|
||||||
|
? t("navigation.volume", { number: book.metadata.number })
|
||||||
|
: "")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-white/80 mt-1">
|
<p className="text-xs text-white/80 mt-1">
|
||||||
{currentPage} / {book.media.pagesCount}
|
{t("books.status.progress", {
|
||||||
|
current: currentPage,
|
||||||
|
total: book.media.pagesCount,
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -201,6 +201,7 @@
|
|||||||
"books": {
|
"books": {
|
||||||
"empty": "No books available",
|
"empty": "No books available",
|
||||||
"coverAlt": "Cover of {{title}}",
|
"coverAlt": "Cover of {{title}}",
|
||||||
|
"defaultCoverAlt": "Cover image",
|
||||||
"status": {
|
"status": {
|
||||||
"unread": "Unread",
|
"unread": "Unread",
|
||||||
"read": "Read",
|
"read": "Read",
|
||||||
@@ -380,6 +381,10 @@
|
|||||||
"enter": "Enter fullscreen",
|
"enter": "Enter fullscreen",
|
||||||
"exit": "Exit fullscreen"
|
"exit": "Exit fullscreen"
|
||||||
},
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"show": "Show thumbnails",
|
||||||
|
"hide": "Hide thumbnails"
|
||||||
|
},
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"previousPage": "Previous page",
|
"previousPage": "Previous page",
|
||||||
"nextPage": "Next page"
|
"nextPage": "Next page"
|
||||||
@@ -388,6 +393,11 @@
|
|||||||
"endOfSeriesMessage": "You have finished all the books in this series!",
|
"endOfSeriesMessage": "You have finished all the books in this series!",
|
||||||
"backToSeries": "Back to series"
|
"backToSeries": "Back to series"
|
||||||
},
|
},
|
||||||
|
"navigation": {
|
||||||
|
"scrollLeft": "Scroll left",
|
||||||
|
"scrollRight": "Scroll right",
|
||||||
|
"volume": "Volume {number}"
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"title": "DEBUG",
|
"title": "DEBUG",
|
||||||
"entries": "entry",
|
"entries": "entry",
|
||||||
|
|||||||
@@ -203,6 +203,7 @@
|
|||||||
"books": {
|
"books": {
|
||||||
"empty": "Aucun tome disponible",
|
"empty": "Aucun tome disponible",
|
||||||
"coverAlt": "Couverture de {{title}}",
|
"coverAlt": "Couverture de {{title}}",
|
||||||
|
"defaultCoverAlt": "Image de couverture",
|
||||||
"status": {
|
"status": {
|
||||||
"unread": "Non lu",
|
"unread": "Non lu",
|
||||||
"read": "Lu",
|
"read": "Lu",
|
||||||
@@ -382,6 +383,10 @@
|
|||||||
"enter": "Plein écran",
|
"enter": "Plein écran",
|
||||||
"exit": "Quitter le plein écran"
|
"exit": "Quitter le plein écran"
|
||||||
},
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"show": "Afficher les vignettes",
|
||||||
|
"hide": "Masquer les vignettes"
|
||||||
|
},
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"previousPage": "Page précédente",
|
"previousPage": "Page précédente",
|
||||||
"nextPage": "Page suivante"
|
"nextPage": "Page suivante"
|
||||||
@@ -394,6 +399,11 @@
|
|||||||
"toggleSidebar": "Afficher/masquer le menu latéral",
|
"toggleSidebar": "Afficher/masquer le menu latéral",
|
||||||
"toggleTheme": "Changer le thème"
|
"toggleTheme": "Changer le thème"
|
||||||
},
|
},
|
||||||
|
"navigation": {
|
||||||
|
"scrollLeft": "Défiler vers la gauche",
|
||||||
|
"scrollRight": "Défiler vers la droite",
|
||||||
|
"volume": "Tome {number}"
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"title": "DEBUG",
|
"title": "DEBUG",
|
||||||
"entries": "entrée",
|
"entries": "entrée",
|
||||||
|
|||||||
Reference in New Issue
Block a user