feat(i18n): components pack translated
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
Globe,
|
||||
} from "lucide-react";
|
||||
import { CacheType } from "@/lib/services/base-api.service";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface RequestTiming {
|
||||
url: string;
|
||||
@@ -49,6 +50,7 @@ export function DebugInfo() {
|
||||
const [isMinimized, setIsMinimized] = useState(false);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
@@ -100,10 +102,10 @@ export function DebugInfo() {
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4 sticky top-0 bg-zinc-900 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="font-bold text-lg">DEBUG</h2>
|
||||
<h2 className="font-bold text-lg">{t("debug.title")}</h2>
|
||||
{!isMinimized && (
|
||||
<span className="text-xs text-zinc-400">
|
||||
{sortedLogs.length} entrée{sortedLogs.length > 1 ? "s" : ""}
|
||||
{sortedLogs.length} {t("debug.entries", { count: sortedLogs.length })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -111,7 +113,7 @@ export function DebugInfo() {
|
||||
<button
|
||||
onClick={fetchLogs}
|
||||
className="hover:bg-zinc-700 rounded-full p-1.5"
|
||||
aria-label="Rafraîchir les logs"
|
||||
aria-label={t("debug.actions.refresh")}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw className={`h-5 w-5 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
@@ -119,14 +121,14 @@ export function DebugInfo() {
|
||||
<button
|
||||
onClick={() => setIsMinimized(!isMinimized)}
|
||||
className="hover:bg-zinc-700 rounded-full p-1.5"
|
||||
aria-label={isMinimized ? "Agrandir" : "Minimiser"}
|
||||
aria-label={t(isMinimized ? "debug.actions.maximize" : "debug.actions.minimize")}
|
||||
>
|
||||
{isMinimized ? <Maximize2 className="h-5 w-5" /> : <Minimize2 className="h-5 w-5" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={clearLogs}
|
||||
className="hover:bg-zinc-700 rounded-full p-1.5"
|
||||
aria-label="Effacer les logs"
|
||||
aria-label={t("debug.actions.clear")}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -136,32 +138,40 @@ export function DebugInfo() {
|
||||
{!isMinimized && (
|
||||
<div className="space-y-3">
|
||||
{sortedLogs.length === 0 ? (
|
||||
<p className="text-sm opacity-75">Aucune requête enregistrée</p>
|
||||
<p className="text-sm opacity-75">{t("debug.noRequests")}</p>
|
||||
) : (
|
||||
sortedLogs.map((log, index) => (
|
||||
<div key={index} className="text-sm space-y-1.5 bg-zinc-800 p-2 rounded">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
{log.fromCache && (
|
||||
<div title={`Cache: ${log.cacheType || "DEFAULT"}`} className="flex-shrink-0">
|
||||
<div
|
||||
title={t("debug.tooltips.cache", { type: log.cacheType || "DEFAULT" })}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<Database className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
{log.mongoAccess && (
|
||||
<div
|
||||
title={`MongoDB: ${log.mongoAccess.operation}`}
|
||||
title={t("debug.tooltips.mongodb", {
|
||||
operation: log.mongoAccess.operation,
|
||||
})}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<CircleDot className="h-4 w-4 text-blue-400" />
|
||||
</div>
|
||||
)}
|
||||
{log.pageRender && (
|
||||
<div title={`Page Render: ${log.pageRender.page}`} className="flex-shrink-0">
|
||||
<div
|
||||
title={t("debug.tooltips.pageRender", { page: log.pageRender.page })}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<Layout className="h-4 w-4 text-purple-400" />
|
||||
</div>
|
||||
)}
|
||||
{!log.fromCache && !log.mongoAccess && !log.pageRender && (
|
||||
<div title="API Call" className="flex-shrink-0">
|
||||
<div title={t("debug.tooltips.apiCall")} className="flex-shrink-0">
|
||||
<Globe className="h-4 w-4 text-rose-400" />
|
||||
</div>
|
||||
)}
|
||||
@@ -170,7 +180,10 @@ export function DebugInfo() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
<div className="flex items-center gap-1 text-zinc-400" title="Heure du log">
|
||||
<div
|
||||
className="flex items-center gap-1 text-zinc-400"
|
||||
title={t("debug.tooltips.logTime")}
|
||||
>
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{formatTime(log.timestamp)}</span>
|
||||
</div>
|
||||
@@ -188,7 +201,7 @@ export function DebugInfo() {
|
||||
{formatDuration(log.duration)}ms
|
||||
</span>
|
||||
{log.mongoAccess && (
|
||||
<span className="text-blue-400" title="Temps d'accès MongoDB">
|
||||
<span className="text-blue-400" title={t("debug.tooltips.mongoAccess")}>
|
||||
+{formatDuration(log.mongoAccess.duration)}ms
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Menu, Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import LanguageSelector from "@/components/LanguageSelector";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface HeaderProps {
|
||||
onToggleSidebar: () => void;
|
||||
@@ -8,6 +9,7 @@ interface HeaderProps {
|
||||
|
||||
export function Header({ onToggleSidebar }: HeaderProps) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
@@ -19,7 +21,7 @@ export function Header({ onToggleSidebar }: HeaderProps) {
|
||||
<button
|
||||
onClick={onToggleSidebar}
|
||||
className="mr-2 px-2 py-1.5 hover:bg-accent hover:text-accent-foreground rounded-md"
|
||||
aria-label="Toggle sidebar"
|
||||
aria-label={t("header.toggleSidebar")}
|
||||
id="sidebar-toggle"
|
||||
>
|
||||
<div className="flex items-center justify-center w-5 h-5">
|
||||
@@ -39,13 +41,13 @@ export function Header({ onToggleSidebar }: HeaderProps) {
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="px-2 py-1.5 hover:bg-accent hover:text-accent-foreground rounded-md"
|
||||
aria-label="Toggle theme"
|
||||
aria-label={t("header.toggleTheme")}
|
||||
>
|
||||
<div className="relative flex items-center w-5 h-5">
|
||||
<Sun className="absolute inset-0 h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute inset-0 h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
</div>
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
<span className="sr-only">{t("header.toggleTheme")}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { RefreshCw } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface RefreshButtonProps {
|
||||
libraryId: string;
|
||||
@@ -14,6 +15,7 @@ interface RefreshButtonProps {
|
||||
export function RefreshButton({ libraryId, refreshLibrary }: RefreshButtonProps) {
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
@@ -22,8 +24,8 @@ export function RefreshButton({ libraryId, refreshLibrary }: RefreshButtonProps)
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: "Bibliothèque rafraîchie",
|
||||
description: "La bibliothèque a été rafraîchie avec succès",
|
||||
title: t("library.refresh.success.title"),
|
||||
description: t("library.refresh.success.description"),
|
||||
});
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
@@ -31,8 +33,9 @@ export function RefreshButton({ libraryId, refreshLibrary }: RefreshButtonProps)
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Erreur",
|
||||
description: error instanceof Error ? error.message : "Une erreur est survenue",
|
||||
title: t("library.refresh.error.title"),
|
||||
description:
|
||||
error instanceof Error ? error.message : t("library.refresh.error.description"),
|
||||
});
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
@@ -46,7 +49,7 @@ export function RefreshButton({ libraryId, refreshLibrary }: RefreshButtonProps)
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
className="ml-2"
|
||||
aria-label="Rafraîchir la bibliothèque"
|
||||
aria-label={t("library.refresh.button")}
|
||||
>
|
||||
<RefreshCw className={cn("h-4 w-4", isRefreshing && "animate-spin")} />
|
||||
</Button>
|
||||
|
||||
@@ -118,7 +118,7 @@ export const ControlButtons = ({
|
||||
"absolute top-4 right-4 p-2 rounded-full bg-background/50 hover:bg-background/80 transition-all duration-300 z-30",
|
||||
showControls ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
)}
|
||||
aria-label="Fermer"
|
||||
aria-label={t("reader.controls.close")}
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
</button>
|
||||
@@ -136,7 +136,7 @@ export const ControlButtons = ({
|
||||
direction === "rtl" ? "right-4" : "left-4",
|
||||
showControls ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
)}
|
||||
aria-label="Page précédente"
|
||||
aria-label={t("reader.controls.previousPage")}
|
||||
>
|
||||
<ChevronLeft className="h-8 w-8" />
|
||||
</button>
|
||||
@@ -154,7 +154,7 @@ export const ControlButtons = ({
|
||||
direction === "rtl" ? "left-4" : "right-4",
|
||||
showControls ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
)}
|
||||
aria-label="Page suivante"
|
||||
aria-label={t("reader.controls.nextPage")}
|
||||
>
|
||||
<ChevronRight className="h-8 w-8" />
|
||||
</button>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "./button";
|
||||
import { useToast } from "./use-toast";
|
||||
import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface MarkAsReadButtonProps {
|
||||
bookId: string;
|
||||
@@ -23,6 +24,7 @@ export function MarkAsReadButton({
|
||||
}: MarkAsReadButtonProps) {
|
||||
const { toast } = useToast();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleMarkAsRead = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation(); // Empêcher la propagation au parent
|
||||
@@ -38,19 +40,19 @@ export function MarkAsReadButton({
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Erreur lors de la mise à jour");
|
||||
throw new Error(t("books.actions.markAsRead.error.update"));
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Le tome a été marqué comme lu",
|
||||
title: t("books.actions.markAsRead.success.title"),
|
||||
description: t("books.actions.markAsRead.success.description"),
|
||||
});
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la mise à jour du progresseur de lecture:", error);
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de marquer le tome comme lu",
|
||||
title: t("books.actions.markAsRead.error.title"),
|
||||
description: t("books.actions.markAsRead.error.description"),
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
@@ -65,7 +67,7 @@ export function MarkAsReadButton({
|
||||
onClick={handleMarkAsRead}
|
||||
className={`h-8 w-8 p-0 rounded-br-lg rounded-tl-lg ${className}`}
|
||||
disabled={isRead || isLoading}
|
||||
aria-label="Marquer comme lu"
|
||||
aria-label={t("books.actions.markAsRead.button")}
|
||||
>
|
||||
{isLoading ? <Loader2 className="h-5 w-5 animate-spin" /> : <BookCheck className="h-5 w-5" />}
|
||||
</Button>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "./button";
|
||||
import { useToast } from "./use-toast";
|
||||
import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface MarkAsUnreadButtonProps {
|
||||
bookId: string;
|
||||
@@ -15,6 +16,7 @@ interface MarkAsUnreadButtonProps {
|
||||
export function MarkAsUnreadButton({ bookId, onSuccess, className }: MarkAsUnreadButtonProps) {
|
||||
const { toast } = useToast();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleMarkAsUnread = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation(); // Empêcher la propagation au parent
|
||||
@@ -26,19 +28,19 @@ export function MarkAsUnreadButton({ bookId, onSuccess, className }: MarkAsUnrea
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Erreur lors de la mise à jour");
|
||||
throw new Error(t("books.actions.markAsUnread.error.update"));
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Le tome a été marqué comme non lu",
|
||||
title: t("books.actions.markAsUnread.success.title"),
|
||||
description: t("books.actions.markAsUnread.success.description"),
|
||||
});
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la mise à jour du progresseur de lecture:", error);
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de marquer le tome comme non lu",
|
||||
title: t("books.actions.markAsUnread.error.title"),
|
||||
description: t("books.actions.markAsUnread.error.description"),
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
@@ -53,7 +55,7 @@ export function MarkAsUnreadButton({ bookId, onSuccess, className }: MarkAsUnrea
|
||||
onClick={handleMarkAsUnread}
|
||||
className={`h-8 w-8 p-0 rounded-br-lg rounded-tl-lg ${className}`}
|
||||
disabled={isLoading}
|
||||
aria-label="Marquer comme non lu"
|
||||
aria-label={t("books.actions.markAsUnread.button")}
|
||||
>
|
||||
{isLoading ? <Loader2 className="h-5 w-5 animate-spin" /> : <BookX className="h-5 w-5" />}
|
||||
</Button>
|
||||
|
||||
@@ -146,7 +146,18 @@
|
||||
"available": "Available",
|
||||
"unavailable": "Unavailable"
|
||||
},
|
||||
"lastUpdated": "Last updated: {date}"
|
||||
"lastUpdated": "Last updated: {date}",
|
||||
"refresh": {
|
||||
"button": "Refresh library",
|
||||
"success": {
|
||||
"title": "Library refreshed",
|
||||
"description": "The library has been refreshed successfully"
|
||||
},
|
||||
"error": {
|
||||
"title": "Error",
|
||||
"description": "An error occurred"
|
||||
}
|
||||
}
|
||||
},
|
||||
"series": {
|
||||
"empty": "No series available",
|
||||
@@ -180,7 +191,9 @@
|
||||
"favorite": {
|
||||
"add": "Added to favorites",
|
||||
"remove": "Removed from favorites"
|
||||
}
|
||||
},
|
||||
"toggleSidebar": "Toggle sidebar",
|
||||
"toggleTheme": "Toggle theme"
|
||||
}
|
||||
},
|
||||
"books": {
|
||||
@@ -200,7 +213,33 @@
|
||||
"showAll": "Show all",
|
||||
"unread": "Unread"
|
||||
},
|
||||
"loading": "Loading..."
|
||||
"loading": "Loading...",
|
||||
"actions": {
|
||||
"markAsRead": {
|
||||
"button": "Mark as read",
|
||||
"success": {
|
||||
"title": "Success",
|
||||
"description": "Book has been marked as read"
|
||||
},
|
||||
"error": {
|
||||
"title": "Error",
|
||||
"description": "Unable to mark book as read",
|
||||
"update": "Error updating status"
|
||||
}
|
||||
},
|
||||
"markAsUnread": {
|
||||
"button": "Mark as unread",
|
||||
"success": {
|
||||
"title": "Success",
|
||||
"description": "Book has been marked as unread"
|
||||
},
|
||||
"error": {
|
||||
"title": "Error",
|
||||
"description": "Unable to mark book as unread",
|
||||
"update": "Error updating status"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloads": {
|
||||
"page": {
|
||||
@@ -278,7 +317,30 @@
|
||||
"fullscreen": {
|
||||
"enter": "Enter fullscreen",
|
||||
"exit": "Exit fullscreen"
|
||||
}
|
||||
},
|
||||
"close": "Close",
|
||||
"previousPage": "Previous page",
|
||||
"nextPage": "Next page"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"title": "DEBUG",
|
||||
"entries": "entry",
|
||||
"entries_plural": "entries",
|
||||
"noRequests": "No recorded requests",
|
||||
"actions": {
|
||||
"refresh": "Refresh logs",
|
||||
"maximize": "Maximize",
|
||||
"minimize": "Minimize",
|
||||
"clear": "Clear logs"
|
||||
},
|
||||
"tooltips": {
|
||||
"cache": "Cache: {{type}}",
|
||||
"mongodb": "MongoDB: {{operation}}",
|
||||
"pageRender": "Page Render: {{page}}",
|
||||
"apiCall": "API Call",
|
||||
"logTime": "Log time",
|
||||
"mongoAccess": "MongoDB access time"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,18 @@
|
||||
"available": "Disponible",
|
||||
"unavailable": "Non disponible"
|
||||
},
|
||||
"lastUpdated": "Dernière mise à jour : {date}"
|
||||
"lastUpdated": "Dernière mise à jour : {date}",
|
||||
"refresh": {
|
||||
"button": "Rafraîchir la bibliothèque",
|
||||
"success": {
|
||||
"title": "Bibliothèque rafraîchie",
|
||||
"description": "La bibliothèque a été rafraîchie avec succès"
|
||||
},
|
||||
"error": {
|
||||
"title": "Erreur",
|
||||
"description": "Une erreur est survenue"
|
||||
}
|
||||
}
|
||||
},
|
||||
"series": {
|
||||
"empty": "Aucune série disponible",
|
||||
@@ -200,7 +211,33 @@
|
||||
"showAll": "Afficher tout",
|
||||
"unread": "À lire"
|
||||
},
|
||||
"loading": "Chargement..."
|
||||
"loading": "Chargement...",
|
||||
"actions": {
|
||||
"markAsRead": {
|
||||
"button": "Marquer comme lu",
|
||||
"success": {
|
||||
"title": "Succès",
|
||||
"description": "Le tome a été marqué comme lu"
|
||||
},
|
||||
"error": {
|
||||
"title": "Erreur",
|
||||
"description": "Impossible de marquer le tome comme lu",
|
||||
"update": "Erreur lors de la mise à jour"
|
||||
}
|
||||
},
|
||||
"markAsUnread": {
|
||||
"button": "Marquer comme non lu",
|
||||
"success": {
|
||||
"title": "Succès",
|
||||
"description": "Le tome a été marqué comme non lu"
|
||||
},
|
||||
"error": {
|
||||
"title": "Erreur",
|
||||
"description": "Impossible de marquer le tome comme non lu",
|
||||
"update": "Erreur lors de la mise à jour"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloads": {
|
||||
"page": {
|
||||
@@ -278,7 +315,34 @@
|
||||
"fullscreen": {
|
||||
"enter": "Plein écran",
|
||||
"exit": "Quitter le plein écran"
|
||||
}
|
||||
},
|
||||
"close": "Fermer",
|
||||
"previousPage": "Page précédente",
|
||||
"nextPage": "Page suivante"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"toggleSidebar": "Afficher/masquer le menu latéral",
|
||||
"toggleTheme": "Changer le thème"
|
||||
},
|
||||
"debug": {
|
||||
"title": "DEBUG",
|
||||
"entries": "entrée",
|
||||
"entries_plural": "entrées",
|
||||
"noRequests": "Aucune requête enregistrée",
|
||||
"actions": {
|
||||
"refresh": "Rafraîchir les logs",
|
||||
"maximize": "Agrandir",
|
||||
"minimize": "Minimiser",
|
||||
"clear": "Effacer les logs"
|
||||
},
|
||||
"tooltips": {
|
||||
"cache": "Cache: {{type}}",
|
||||
"mongodb": "MongoDB: {{operation}}",
|
||||
"pageRender": "Page Render: {{page}}",
|
||||
"apiCall": "API Call",
|
||||
"logTime": "Heure du log",
|
||||
"mongoAccess": "Temps d'accès MongoDB"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user