feat(i18n): components pack translated

This commit is contained in:
Julien Froidefond
2025-03-02 06:31:41 +01:00
parent d94232e531
commit 2289753b84
8 changed files with 190 additions and 42 deletions

View File

@@ -13,6 +13,7 @@ import {
Globe, Globe,
} from "lucide-react"; } from "lucide-react";
import { CacheType } from "@/lib/services/base-api.service"; import { CacheType } from "@/lib/services/base-api.service";
import { useTranslation } from "react-i18next";
interface RequestTiming { interface RequestTiming {
url: string; url: string;
@@ -49,6 +50,7 @@ export function DebugInfo() {
const [isMinimized, setIsMinimized] = useState(false); const [isMinimized, setIsMinimized] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const pathname = usePathname(); const pathname = usePathname();
const { t } = useTranslation();
const fetchLogs = async () => { const fetchLogs = async () => {
try { 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 justify-between mb-4 sticky top-0 bg-zinc-900 pb-2">
<div className="flex items-center gap-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 && ( {!isMinimized && (
<span className="text-xs text-zinc-400"> <span className="text-xs text-zinc-400">
{sortedLogs.length} entrée{sortedLogs.length > 1 ? "s" : ""} {sortedLogs.length} {t("debug.entries", { count: sortedLogs.length })}
</span> </span>
)} )}
</div> </div>
@@ -111,7 +113,7 @@ export function DebugInfo() {
<button <button
onClick={fetchLogs} onClick={fetchLogs}
className="hover:bg-zinc-700 rounded-full p-1.5" className="hover:bg-zinc-700 rounded-full p-1.5"
aria-label="Rafraîchir les logs" aria-label={t("debug.actions.refresh")}
disabled={isRefreshing} disabled={isRefreshing}
> >
<RefreshCw className={`h-5 w-5 ${isRefreshing ? "animate-spin" : ""}`} /> <RefreshCw className={`h-5 w-5 ${isRefreshing ? "animate-spin" : ""}`} />
@@ -119,14 +121,14 @@ export function DebugInfo() {
<button <button
onClick={() => setIsMinimized(!isMinimized)} onClick={() => setIsMinimized(!isMinimized)}
className="hover:bg-zinc-700 rounded-full p-1.5" 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" />} {isMinimized ? <Maximize2 className="h-5 w-5" /> : <Minimize2 className="h-5 w-5" />}
</button> </button>
<button <button
onClick={clearLogs} onClick={clearLogs}
className="hover:bg-zinc-700 rounded-full p-1.5" 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" /> <X className="h-5 w-5" />
</button> </button>
@@ -136,32 +138,40 @@ export function DebugInfo() {
{!isMinimized && ( {!isMinimized && (
<div className="space-y-3"> <div className="space-y-3">
{sortedLogs.length === 0 ? ( {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) => ( sortedLogs.map((log, index) => (
<div key={index} className="text-sm space-y-1.5 bg-zinc-800 p-2 rounded"> <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 justify-between items-center">
<div className="flex items-center gap-2 min-w-0 flex-1"> <div className="flex items-center gap-2 min-w-0 flex-1">
{log.fromCache && ( {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" /> <Database className="h-4 w-4" />
</div> </div>
)} )}
{log.mongoAccess && ( {log.mongoAccess && (
<div <div
title={`MongoDB: ${log.mongoAccess.operation}`} title={t("debug.tooltips.mongodb", {
operation: log.mongoAccess.operation,
})}
className="flex-shrink-0" className="flex-shrink-0"
> >
<CircleDot className="h-4 w-4 text-blue-400" /> <CircleDot className="h-4 w-4 text-blue-400" />
</div> </div>
)} )}
{log.pageRender && ( {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" /> <Layout className="h-4 w-4 text-purple-400" />
</div> </div>
)} )}
{!log.fromCache && !log.mongoAccess && !log.pageRender && ( {!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" /> <Globe className="h-4 w-4 text-rose-400" />
</div> </div>
)} )}
@@ -170,7 +180,10 @@ export function DebugInfo() {
</span> </span>
</div> </div>
<div className="flex items-center gap-3 flex-shrink-0"> <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" /> <Clock className="h-3 w-3" />
<span>{formatTime(log.timestamp)}</span> <span>{formatTime(log.timestamp)}</span>
</div> </div>
@@ -188,7 +201,7 @@ export function DebugInfo() {
{formatDuration(log.duration)}ms {formatDuration(log.duration)}ms
</span> </span>
{log.mongoAccess && ( {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 +{formatDuration(log.mongoAccess.duration)}ms
</span> </span>
)} )}

View File

@@ -1,6 +1,7 @@
import { Menu, Moon, Sun } from "lucide-react"; import { Menu, Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import LanguageSelector from "@/components/LanguageSelector"; import LanguageSelector from "@/components/LanguageSelector";
import { useTranslation } from "react-i18next";
interface HeaderProps { interface HeaderProps {
onToggleSidebar: () => void; onToggleSidebar: () => void;
@@ -8,6 +9,7 @@ interface HeaderProps {
export function Header({ onToggleSidebar }: HeaderProps) { export function Header({ onToggleSidebar }: HeaderProps) {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const { t } = useTranslation();
const toggleTheme = () => { const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark"); setTheme(theme === "dark" ? "light" : "dark");
@@ -19,7 +21,7 @@ export function Header({ onToggleSidebar }: HeaderProps) {
<button <button
onClick={onToggleSidebar} onClick={onToggleSidebar}
className="mr-2 px-2 py-1.5 hover:bg-accent hover:text-accent-foreground rounded-md" 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" id="sidebar-toggle"
> >
<div className="flex items-center justify-center w-5 h-5"> <div className="flex items-center justify-center w-5 h-5">
@@ -39,13 +41,13 @@ export function Header({ onToggleSidebar }: HeaderProps) {
<button <button
onClick={toggleTheme} onClick={toggleTheme}
className="px-2 py-1.5 hover:bg-accent hover:text-accent-foreground rounded-md" 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"> <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" /> <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" /> <Moon className="absolute inset-0 h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</div> </div>
<span className="sr-only">Toggle theme</span> <span className="sr-only">{t("header.toggleTheme")}</span>
</button> </button>
</nav> </nav>
</div> </div>

View File

@@ -5,6 +5,7 @@ import { RefreshCw } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
interface RefreshButtonProps { interface RefreshButtonProps {
libraryId: string; libraryId: string;
@@ -14,6 +15,7 @@ interface RefreshButtonProps {
export function RefreshButton({ libraryId, refreshLibrary }: RefreshButtonProps) { export function RefreshButton({ libraryId, refreshLibrary }: RefreshButtonProps) {
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const { toast } = useToast(); const { toast } = useToast();
const { t } = useTranslation();
const handleRefresh = async () => { const handleRefresh = async () => {
setIsRefreshing(true); setIsRefreshing(true);
@@ -22,8 +24,8 @@ export function RefreshButton({ libraryId, refreshLibrary }: RefreshButtonProps)
if (result.success) { if (result.success) {
toast({ toast({
title: "Bibliothèque rafraîchie", title: t("library.refresh.success.title"),
description: "La bibliothèque a été rafraîchie avec succès", description: t("library.refresh.success.description"),
}); });
} else { } else {
throw new Error(result.error); throw new Error(result.error);
@@ -31,8 +33,9 @@ export function RefreshButton({ libraryId, refreshLibrary }: RefreshButtonProps)
} catch (error) { } catch (error) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Erreur", title: t("library.refresh.error.title"),
description: error instanceof Error ? error.message : "Une erreur est survenue", description:
error instanceof Error ? error.message : t("library.refresh.error.description"),
}); });
} finally { } finally {
setIsRefreshing(false); setIsRefreshing(false);
@@ -46,7 +49,7 @@ export function RefreshButton({ libraryId, refreshLibrary }: RefreshButtonProps)
onClick={handleRefresh} onClick={handleRefresh}
disabled={isRefreshing} disabled={isRefreshing}
className="ml-2" 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")} /> <RefreshCw className={cn("h-4 w-4", isRefreshing && "animate-spin")} />
</Button> </Button>

View File

@@ -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", "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" showControls ? "opacity-100" : "opacity-0 pointer-events-none"
)} )}
aria-label="Fermer" aria-label={t("reader.controls.close")}
> >
<X className="h-6 w-6" /> <X className="h-6 w-6" />
</button> </button>
@@ -136,7 +136,7 @@ export const ControlButtons = ({
direction === "rtl" ? "right-4" : "left-4", direction === "rtl" ? "right-4" : "left-4",
showControls ? "opacity-100" : "opacity-0 pointer-events-none" 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" /> <ChevronLeft className="h-8 w-8" />
</button> </button>
@@ -154,7 +154,7 @@ export const ControlButtons = ({
direction === "rtl" ? "left-4" : "right-4", direction === "rtl" ? "left-4" : "right-4",
showControls ? "opacity-100" : "opacity-0 pointer-events-none" showControls ? "opacity-100" : "opacity-0 pointer-events-none"
)} )}
aria-label="Page suivante" aria-label={t("reader.controls.nextPage")}
> >
<ChevronRight className="h-8 w-8" /> <ChevronRight className="h-8 w-8" />
</button> </button>

View File

@@ -5,6 +5,7 @@ import { Button } from "./button";
import { useToast } from "./use-toast"; import { useToast } from "./use-toast";
import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service"; import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
interface MarkAsReadButtonProps { interface MarkAsReadButtonProps {
bookId: string; bookId: string;
@@ -23,6 +24,7 @@ export function MarkAsReadButton({
}: MarkAsReadButtonProps) { }: MarkAsReadButtonProps) {
const { toast } = useToast(); const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const handleMarkAsRead = async (e: React.MouseEvent) => { const handleMarkAsRead = async (e: React.MouseEvent) => {
e.stopPropagation(); // Empêcher la propagation au parent e.stopPropagation(); // Empêcher la propagation au parent
@@ -38,19 +40,19 @@ export function MarkAsReadButton({
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Erreur lors de la mise à jour"); throw new Error(t("books.actions.markAsRead.error.update"));
} }
toast({ toast({
title: "Succès", title: t("books.actions.markAsRead.success.title"),
description: "Le tome a été marqué comme lu", description: t("books.actions.markAsRead.success.description"),
}); });
onSuccess?.(); onSuccess?.();
} catch (error) { } catch (error) {
console.error("Erreur lors de la mise à jour du progresseur de lecture:", error); console.error("Erreur lors de la mise à jour du progresseur de lecture:", error);
toast({ toast({
title: "Erreur", title: t("books.actions.markAsRead.error.title"),
description: "Impossible de marquer le tome comme lu", description: t("books.actions.markAsRead.error.description"),
variant: "destructive", variant: "destructive",
}); });
} finally { } finally {
@@ -65,7 +67,7 @@ export function MarkAsReadButton({
onClick={handleMarkAsRead} onClick={handleMarkAsRead}
className={`h-8 w-8 p-0 rounded-br-lg rounded-tl-lg ${className}`} className={`h-8 w-8 p-0 rounded-br-lg rounded-tl-lg ${className}`}
disabled={isRead || isLoading} 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" />} {isLoading ? <Loader2 className="h-5 w-5 animate-spin" /> : <BookCheck className="h-5 w-5" />}
</Button> </Button>

View File

@@ -5,6 +5,7 @@ import { Button } from "./button";
import { useToast } from "./use-toast"; import { useToast } from "./use-toast";
import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service"; import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
interface MarkAsUnreadButtonProps { interface MarkAsUnreadButtonProps {
bookId: string; bookId: string;
@@ -15,6 +16,7 @@ interface MarkAsUnreadButtonProps {
export function MarkAsUnreadButton({ bookId, onSuccess, className }: MarkAsUnreadButtonProps) { export function MarkAsUnreadButton({ bookId, onSuccess, className }: MarkAsUnreadButtonProps) {
const { toast } = useToast(); const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const handleMarkAsUnread = async (e: React.MouseEvent) => { const handleMarkAsUnread = async (e: React.MouseEvent) => {
e.stopPropagation(); // Empêcher la propagation au parent e.stopPropagation(); // Empêcher la propagation au parent
@@ -26,19 +28,19 @@ export function MarkAsUnreadButton({ bookId, onSuccess, className }: MarkAsUnrea
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Erreur lors de la mise à jour"); throw new Error(t("books.actions.markAsUnread.error.update"));
} }
toast({ toast({
title: "Succès", title: t("books.actions.markAsUnread.success.title"),
description: "Le tome a été marqué comme non lu", description: t("books.actions.markAsUnread.success.description"),
}); });
onSuccess?.(); onSuccess?.();
} catch (error) { } catch (error) {
console.error("Erreur lors de la mise à jour du progresseur de lecture:", error); console.error("Erreur lors de la mise à jour du progresseur de lecture:", error);
toast({ toast({
title: "Erreur", title: t("books.actions.markAsUnread.error.title"),
description: "Impossible de marquer le tome comme non lu", description: t("books.actions.markAsUnread.error.description"),
variant: "destructive", variant: "destructive",
}); });
} finally { } finally {
@@ -53,7 +55,7 @@ export function MarkAsUnreadButton({ bookId, onSuccess, className }: MarkAsUnrea
onClick={handleMarkAsUnread} onClick={handleMarkAsUnread}
className={`h-8 w-8 p-0 rounded-br-lg rounded-tl-lg ${className}`} className={`h-8 w-8 p-0 rounded-br-lg rounded-tl-lg ${className}`}
disabled={isLoading} 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" />} {isLoading ? <Loader2 className="h-5 w-5 animate-spin" /> : <BookX className="h-5 w-5" />}
</Button> </Button>

View File

@@ -146,7 +146,18 @@
"available": "Available", "available": "Available",
"unavailable": "Unavailable" "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": { "series": {
"empty": "No series available", "empty": "No series available",
@@ -180,7 +191,9 @@
"favorite": { "favorite": {
"add": "Added to favorites", "add": "Added to favorites",
"remove": "Removed from favorites" "remove": "Removed from favorites"
} },
"toggleSidebar": "Toggle sidebar",
"toggleTheme": "Toggle theme"
} }
}, },
"books": { "books": {
@@ -200,7 +213,33 @@
"showAll": "Show all", "showAll": "Show all",
"unread": "Unread" "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": { "downloads": {
"page": { "page": {
@@ -278,7 +317,30 @@
"fullscreen": { "fullscreen": {
"enter": "Enter fullscreen", "enter": "Enter fullscreen",
"exit": "Exit 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"
} }
} }
} }

View File

@@ -146,7 +146,18 @@
"available": "Disponible", "available": "Disponible",
"unavailable": "Non 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": { "series": {
"empty": "Aucune série disponible", "empty": "Aucune série disponible",
@@ -200,7 +211,33 @@
"showAll": "Afficher tout", "showAll": "Afficher tout",
"unread": "À lire" "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": { "downloads": {
"page": { "page": {
@@ -278,7 +315,34 @@
"fullscreen": { "fullscreen": {
"enter": "Plein écran", "enter": "Plein écran",
"exit": "Quitter le 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"
} }
} }
} }