feat: add i18n support (FR/EN) to backoffice with English as default
Implement full internationalization for the Next.js backoffice: - i18n infrastructure: type-safe dictionaries (fr.ts/en.ts), cookie-based locale detection, React Context for client components, server-side translation helper - Language selector in Settings page (General tab) with cookie + DB persistence - All ~35 pages and components translated via t() / useTranslation() - Default locale set to English, French available via settings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
60
apps/backoffice/lib/i18n/context.tsx
Normal file
60
apps/backoffice/lib/i18n/context.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useCallback, useState, type ReactNode } from "react";
|
||||
import type { Locale } from "./types";
|
||||
import { LOCALE_COOKIE } from "./types";
|
||||
import { getDictionarySync, createTranslateFunction } from "./dictionaries";
|
||||
import type { TranslateFunction } from "./dictionaries";
|
||||
|
||||
interface LocaleContextValue {
|
||||
locale: Locale;
|
||||
t: TranslateFunction;
|
||||
setLocale: (locale: Locale) => void;
|
||||
}
|
||||
|
||||
const LocaleContext = createContext<LocaleContextValue | null>(null);
|
||||
|
||||
interface LocaleProviderProps {
|
||||
initialLocale: Locale;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function LocaleProvider({ initialLocale, children }: LocaleProviderProps) {
|
||||
const [locale] = useState<Locale>(initialLocale);
|
||||
const dict = getDictionarySync(locale);
|
||||
const t = createTranslateFunction(dict);
|
||||
|
||||
const setLocale = useCallback(async (newLocale: Locale) => {
|
||||
// Set cookie
|
||||
document.cookie = `${LOCALE_COOKIE}=${newLocale};path=/;max-age=${365 * 24 * 60 * 60}`;
|
||||
|
||||
// Save to DB
|
||||
try {
|
||||
const apiBase = process.env.NEXT_PUBLIC_API_URL || "http://localhost:7080";
|
||||
await fetch(`${apiBase}/settings`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ language: newLocale }),
|
||||
});
|
||||
} catch {
|
||||
// Best effort — cookie is the primary source for rendering
|
||||
}
|
||||
|
||||
// Reload to apply new locale everywhere (server + client)
|
||||
window.location.reload();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LocaleContext.Provider value={{ locale, t, setLocale }}>
|
||||
{children}
|
||||
</LocaleContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTranslation(): LocaleContextValue {
|
||||
const ctx = useContext(LocaleContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useTranslation must be used within a LocaleProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
35
apps/backoffice/lib/i18n/dictionaries.ts
Normal file
35
apps/backoffice/lib/i18n/dictionaries.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Locale } from "./types";
|
||||
import type { TranslationKey } from "./fr";
|
||||
|
||||
const dictionaries: Record<Locale, () => Promise<Record<TranslationKey, string>>> = {
|
||||
fr: () => import("./fr").then((m) => m.default),
|
||||
en: () => import("./en").then((m) => m.default),
|
||||
};
|
||||
|
||||
export async function getDictionary(locale: Locale): Promise<Record<TranslationKey, string>> {
|
||||
return dictionaries[locale]();
|
||||
}
|
||||
|
||||
// Synchronous versions for client-side use
|
||||
import fr from "./fr";
|
||||
import en from "./en";
|
||||
|
||||
const dictionariesSync: Record<Locale, Record<TranslationKey, string>> = { fr, en };
|
||||
|
||||
export function getDictionarySync(locale: Locale): Record<TranslationKey, string> {
|
||||
return dictionariesSync[locale];
|
||||
}
|
||||
|
||||
export type TranslateFunction = (key: TranslationKey, params?: Record<string, string | number>) => string;
|
||||
|
||||
export function createTranslateFunction(dict: Record<TranslationKey, string>): TranslateFunction {
|
||||
return (key: TranslationKey, params?: Record<string, string | number>): string => {
|
||||
let value: string = dict[key] ?? key;
|
||||
if (params) {
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
value = value.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), String(v));
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
555
apps/backoffice/lib/i18n/en.ts
Normal file
555
apps/backoffice/lib/i18n/en.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
import type { TranslationKey } from "./fr";
|
||||
|
||||
const en: Record<TranslationKey, string> = {
|
||||
// Navigation
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.books": "Books",
|
||||
"nav.series": "Series",
|
||||
"nav.libraries": "Libraries",
|
||||
"nav.jobs": "Jobs",
|
||||
"nav.tokens": "Tokens",
|
||||
"nav.settings": "Settings",
|
||||
"nav.navigation": "Navigation",
|
||||
"nav.closeMenu": "Close menu",
|
||||
"nav.openMenu": "Open menu",
|
||||
|
||||
// Common
|
||||
"common.save": "Save",
|
||||
"common.saving": "Saving...",
|
||||
"common.cancel": "Cancel",
|
||||
"common.close": "Close",
|
||||
"common.delete": "Delete",
|
||||
"common.edit": "Edit",
|
||||
"common.search": "Search",
|
||||
"common.clear": "Clear",
|
||||
"common.view": "View",
|
||||
"common.all": "All",
|
||||
"common.enabled": "Enabled",
|
||||
"common.disabled": "Disabled",
|
||||
"common.browse": "Browse",
|
||||
"common.add": "Add",
|
||||
"common.noData": "No data",
|
||||
"common.loading": "Loading...",
|
||||
"common.error": "Error",
|
||||
"common.networkError": "Network error",
|
||||
"common.show": "Show",
|
||||
"common.perPage": "per page",
|
||||
"common.next": "Next",
|
||||
"common.previous": "Previous",
|
||||
"common.first": "First",
|
||||
"common.previousPage": "Previous page",
|
||||
"common.nextPage": "Next page",
|
||||
"common.backoffice": "backoffice",
|
||||
"common.and": "and",
|
||||
"common.via": "via",
|
||||
|
||||
// Reading status
|
||||
"status.unread": "Unread",
|
||||
"status.reading": "Reading",
|
||||
"status.read": "Read",
|
||||
|
||||
// Series status
|
||||
"seriesStatus.ongoing": "Ongoing",
|
||||
"seriesStatus.ended": "Ended",
|
||||
"seriesStatus.hiatus": "Hiatus",
|
||||
"seriesStatus.cancelled": "Cancelled",
|
||||
"seriesStatus.upcoming": "Upcoming",
|
||||
"seriesStatus.allStatuses": "All statuses",
|
||||
"seriesStatus.notDefined": "Not defined",
|
||||
|
||||
// Dashboard
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.subtitle": "Overview of your comic book collection. Manage your libraries, track your reading progress and explore your books and series.",
|
||||
"dashboard.loadError": "Unable to load statistics. Check that the API is running.",
|
||||
"dashboard.books": "Books",
|
||||
"dashboard.series": "Series",
|
||||
"dashboard.libraries": "Libraries",
|
||||
"dashboard.pages": "Pages",
|
||||
"dashboard.authors": "Authors",
|
||||
"dashboard.totalSize": "Total size",
|
||||
"dashboard.readingStatus": "Reading status",
|
||||
"dashboard.byFormat": "By format",
|
||||
"dashboard.byLibrary": "By library",
|
||||
"dashboard.booksAdded": "Books added (last 12 months)",
|
||||
"dashboard.popularSeries": "Popular series",
|
||||
"dashboard.noSeries": "No series yet",
|
||||
"dashboard.unknown": "Unknown",
|
||||
"dashboard.readCount": "{{read}}/{{total}} read",
|
||||
|
||||
// Books page
|
||||
"books.title": "Books",
|
||||
"books.searchPlaceholder": "Search by title, author, series...",
|
||||
"books.library": "Library",
|
||||
"books.allLibraries": "All libraries",
|
||||
"books.status": "Status",
|
||||
"books.sort": "Sort",
|
||||
"books.sortTitle": "Title",
|
||||
"books.sortLatest": "Latest added",
|
||||
"books.resultCount": "{{count}} result{{plural}}",
|
||||
"books.resultCountFor": "{{count}} result{{plural}} for \"{{query}}\"",
|
||||
"books.bookCount": "{{count}} book{{plural}}",
|
||||
"books.seriesHeading": "Series",
|
||||
"books.unclassified": "Unclassified",
|
||||
"books.noResults": "No books found for \"{{query}}\"",
|
||||
"books.noBooks": "No books available",
|
||||
"books.coverOf": "Cover of {{name}}",
|
||||
|
||||
// Series page
|
||||
"series.title": "Series",
|
||||
"series.searchPlaceholder": "Search by series name...",
|
||||
"series.reading": "Reading",
|
||||
"series.missing": "Missing",
|
||||
"series.missingBooks": "Missing books",
|
||||
"series.matchingQuery": "matching",
|
||||
"series.noResults": "No series found matching your filters",
|
||||
"series.noSeries": "No series available",
|
||||
"series.missingCount": "{{count}} missing",
|
||||
"series.readCount": "{{read}}/{{total}} read",
|
||||
|
||||
// Libraries page
|
||||
"libraries.title": "Libraries",
|
||||
"libraries.addLibrary": "Add a library",
|
||||
"libraries.addLibraryDescription": "Create a new library from an existing folder",
|
||||
"libraries.disabled": "Disabled",
|
||||
"libraries.books": "Books",
|
||||
"libraries.series": "Series",
|
||||
"libraries.auto": "Auto",
|
||||
"libraries.manual": "Manual",
|
||||
"libraries.nextScan": "Next: {{time}}",
|
||||
"libraries.imminent": "Imminent",
|
||||
"libraries.index": "Index",
|
||||
"libraries.fullIndex": "Full",
|
||||
"libraries.batchMetadata": "Batch metadata",
|
||||
"libraries.libraryName": "Library name",
|
||||
"libraries.addButton": "Add library",
|
||||
|
||||
// Library sub-pages
|
||||
"libraryBooks.allBooks": "All books",
|
||||
"libraryBooks.booksOfSeries": "Books from \"{{series}}\"",
|
||||
"libraryBooks.filterLabel": "Books from series \"{{series}}\"",
|
||||
"libraryBooks.viewAll": "View all books",
|
||||
"libraryBooks.noBooks": "No books in this library",
|
||||
"libraryBooks.noBooksInSeries": "No books in series \"{{series}}\"",
|
||||
"librarySeries.noSeries": "No series found in this library",
|
||||
"librarySeries.noBooksInSeries": "No books in this series",
|
||||
|
||||
// Library actions
|
||||
"libraryActions.autoScan": "Auto scan",
|
||||
"libraryActions.fileWatch": "File watch ⚡",
|
||||
"libraryActions.schedule": "📅 Schedule",
|
||||
"libraryActions.provider": "Provider",
|
||||
"libraryActions.fallback": "Fallback",
|
||||
"libraryActions.default": "Default",
|
||||
"libraryActions.none": "None",
|
||||
"libraryActions.saving": "Saving...",
|
||||
|
||||
// Library sub-page header
|
||||
"libraryHeader.libraries": "Libraries",
|
||||
"libraryHeader.bookCount": "{{count}} book{{plural}}",
|
||||
"libraryHeader.enabled": "Enabled",
|
||||
|
||||
// Monitoring
|
||||
"monitoring.auto": "Auto",
|
||||
"monitoring.manual": "Manual",
|
||||
"monitoring.hourly": "Hourly",
|
||||
"monitoring.daily": "Daily",
|
||||
"monitoring.weekly": "Weekly",
|
||||
"monitoring.fileWatch": "Real-time file watching",
|
||||
|
||||
// Jobs page
|
||||
"jobs.title": "Indexing jobs",
|
||||
"jobs.startJob": "Start a job",
|
||||
"jobs.startJobDescription": "Select a library (or all) and choose the action to perform.",
|
||||
"jobs.allLibraries": "All libraries",
|
||||
"jobs.rebuild": "Rebuild",
|
||||
"jobs.fullRebuild": "Full rebuild",
|
||||
"jobs.generateThumbnails": "Generate thumbnails",
|
||||
"jobs.regenerateThumbnails": "Regenerate thumbnails",
|
||||
"jobs.batchMetadata": "Batch metadata",
|
||||
"jobs.referenceTitle": "Job types reference",
|
||||
"jobs.rebuildDescription": "Incremental scan: detects files added, modified, or deleted since the last scan, indexes them, and generates missing thumbnails. Existing unmodified data is preserved. This is the most common and fastest action.",
|
||||
"jobs.fullRebuildDescription": "Deletes all indexed data (books, series, thumbnails) then performs a full scan from scratch. Useful if the database is out of sync or corrupted. Long and destructive operation: reading statuses and manual metadata will be lost.",
|
||||
"jobs.generateThumbnailsDescription": "Generates thumbnails only for books that don't have one yet. Existing thumbnails are not affected. Useful after an import or if some thumbnails are missing.",
|
||||
"jobs.regenerateThumbnailsDescription": "Regenerates all thumbnails from scratch, replacing existing ones. Useful if thumbnail quality or size has changed in the configuration, or if thumbnails are corrupted.",
|
||||
"jobs.batchMetadataDescription": "Automatically searches metadata for each series in the library from the configured provider (with fallback if configured). Only results with a unique 100% confidence match are applied automatically. Already linked series are skipped. A detailed per-series report is available at the end of the job. <strong>Requires a specific library</strong> (does not work on \"All libraries\").",
|
||||
|
||||
// Jobs list
|
||||
"jobsList.id": "ID",
|
||||
"jobsList.library": "Library",
|
||||
"jobsList.type": "Type",
|
||||
"jobsList.status": "Status",
|
||||
"jobsList.files": "Files",
|
||||
"jobsList.thumbnails": "Thumbnails",
|
||||
"jobsList.duration": "Duration",
|
||||
"jobsList.created": "Created",
|
||||
"jobsList.actions": "Actions",
|
||||
|
||||
// Job row
|
||||
"jobRow.showProgress": "Show progress",
|
||||
"jobRow.hideProgress": "Hide progress",
|
||||
"jobRow.scanned": "{{count}} scanned",
|
||||
"jobRow.view": "View",
|
||||
|
||||
// Job progress
|
||||
"jobProgress.loadingProgress": "Loading progress...",
|
||||
"jobProgress.sseError": "Failed to parse SSE data",
|
||||
"jobProgress.connectionLost": "Connection lost",
|
||||
"jobProgress.error": "Error: {{message}}",
|
||||
"jobProgress.done": "Done",
|
||||
"jobProgress.currentFile": "Current: {{file}}",
|
||||
"jobProgress.pages": "pages",
|
||||
"jobProgress.thumbnails": "thumbnails",
|
||||
"jobProgress.filesUnit": "files",
|
||||
"jobProgress.scanned": "Scanned: {{count}}",
|
||||
"jobProgress.indexed": "Indexed: {{count}}",
|
||||
"jobProgress.removed": "Removed: {{count}}",
|
||||
"jobProgress.errors": "Errors: {{count}}",
|
||||
|
||||
// Job detail
|
||||
"jobDetail.backToJobs": "Back to jobs",
|
||||
"jobDetail.title": "Job details",
|
||||
"jobDetail.completedIn": "Completed in {{duration}}",
|
||||
"jobDetail.failedAfter": "after {{duration}}",
|
||||
"jobDetail.jobFailed": "Job failed",
|
||||
"jobDetail.cancelled": "Cancelled",
|
||||
"jobDetail.overview": "Overview",
|
||||
"jobDetail.timeline": "Timeline",
|
||||
"jobDetail.created": "Created",
|
||||
"jobDetail.started": "Started",
|
||||
"jobDetail.pendingStart": "Pending start…",
|
||||
"jobDetail.finished": "Finished",
|
||||
"jobDetail.failed": "Failed",
|
||||
"jobDetail.library": "Library",
|
||||
"jobDetail.book": "Book",
|
||||
"jobDetail.allLibraries": "All libraries",
|
||||
"jobDetail.phase1": "Phase 1 — Discovery",
|
||||
"jobDetail.phase2a": "Phase 2a — Page extraction",
|
||||
"jobDetail.phase2b": "Phase 2b — Thumbnail generation",
|
||||
"jobDetail.metadataSearch": "Metadata search",
|
||||
"jobDetail.metadataSearchDesc": "Searching external providers for each series",
|
||||
"jobDetail.phase1Desc": "Scanning and indexing library files",
|
||||
"jobDetail.phase2aDesc": "Extracting the first page of each archive (page count + raw image)",
|
||||
"jobDetail.phase2bDesc": "Generating thumbnails for scanned books",
|
||||
"jobDetail.inProgress": "in progress",
|
||||
"jobDetail.duration": "Duration: {{duration}}",
|
||||
"jobDetail.currentFile": "Current file",
|
||||
"jobDetail.generated": "Generated",
|
||||
"jobDetail.processed": "Processed",
|
||||
"jobDetail.total": "Total",
|
||||
"jobDetail.remaining": "Remaining",
|
||||
"jobDetail.indexStats": "Index statistics",
|
||||
"jobDetail.scanned": "Scanned",
|
||||
"jobDetail.indexed": "Indexed",
|
||||
"jobDetail.removed": "Removed",
|
||||
"jobDetail.warnings": "Warnings",
|
||||
"jobDetail.errors": "Errors",
|
||||
"jobDetail.thumbnailStats": "Thumbnail statistics",
|
||||
"jobDetail.batchReport": "Batch report",
|
||||
"jobDetail.seriesAnalyzed": "{{count}} series analyzed",
|
||||
"jobDetail.autoMatched": "Auto-matched",
|
||||
"jobDetail.alreadyLinked": "Already linked",
|
||||
"jobDetail.noResults": "No results",
|
||||
"jobDetail.tooManyResults": "Too many results",
|
||||
"jobDetail.lowConfidence": "Low confidence",
|
||||
"jobDetail.resultsBySeries": "Results by series",
|
||||
"jobDetail.seriesProcessed": "{{count}} series processed",
|
||||
"jobDetail.candidates": "candidate{{plural}}",
|
||||
"jobDetail.confidence": "confidence",
|
||||
"jobDetail.match": "Match: {{title}}",
|
||||
"jobDetail.fileErrors": "File errors ({{count}})",
|
||||
"jobDetail.fileErrorsDesc": "Errors encountered while processing files",
|
||||
|
||||
// Job types
|
||||
"jobType.rebuild": "Indexing",
|
||||
"jobType.full_rebuild": "Full indexing",
|
||||
"jobType.thumbnail_rebuild": "Thumbnails",
|
||||
"jobType.thumbnail_regenerate": "Regen. thumbnails",
|
||||
"jobType.cbr_to_cbz": "CBR → CBZ",
|
||||
"jobType.metadata_batch": "Batch metadata",
|
||||
"jobType.rebuildLabel": "Incremental indexing",
|
||||
"jobType.rebuildDesc": "Scans new/modified files, analyzes them, and generates missing thumbnails.",
|
||||
"jobType.full_rebuildLabel": "Full reindexing",
|
||||
"jobType.full_rebuildDesc": "Deletes all existing data then performs a full scan, re-analysis, and thumbnail generation.",
|
||||
"jobType.thumbnail_rebuildLabel": "Thumbnail rebuild",
|
||||
"jobType.thumbnail_rebuildDesc": "Generates thumbnails only for books that don't have one. Existing thumbnails are preserved.",
|
||||
"jobType.thumbnail_regenerateLabel": "Thumbnail regeneration",
|
||||
"jobType.thumbnail_regenerateDesc": "Regenerates all thumbnails from scratch, replacing existing ones.",
|
||||
"jobType.cbr_to_cbzLabel": "CBR → CBZ conversion",
|
||||
"jobType.cbr_to_cbzDesc": "Converts a CBR archive to the open CBZ format.",
|
||||
"jobType.metadata_batchLabel": "Batch metadata",
|
||||
"jobType.metadata_batchDesc": "Searches external metadata providers for all series in the library and automatically applies 100% confidence matches.",
|
||||
|
||||
// Status badges
|
||||
"statusBadge.extracting_pages": "Extracting pages",
|
||||
"statusBadge.generating_thumbnails": "Thumbnails",
|
||||
|
||||
// Jobs indicator
|
||||
"jobsIndicator.viewAll": "View all jobs",
|
||||
"jobsIndicator.activeTasks": "Active jobs",
|
||||
"jobsIndicator.runningAndPending": "{{running}} running, {{pending}} pending",
|
||||
"jobsIndicator.pendingTasks": "{{count}} pending job{{plural}}",
|
||||
"jobsIndicator.overallProgress": "Overall progress",
|
||||
"jobsIndicator.viewAllLink": "View all →",
|
||||
"jobsIndicator.noActiveTasks": "No active jobs",
|
||||
"jobsIndicator.autoRefresh": "Auto-refresh every 2s",
|
||||
"jobsIndicator.taskCount": "{{count}} active job{{plural}}",
|
||||
"jobsIndicator.thumbnails": "Thumbnails",
|
||||
"jobsIndicator.regeneration": "Regeneration",
|
||||
|
||||
// Time
|
||||
"time.justNow": "Just now",
|
||||
"time.minutesAgo": "{{count}}m ago",
|
||||
"time.hoursAgo": "{{count}}h ago",
|
||||
|
||||
// Tokens page
|
||||
"tokens.title": "API Tokens",
|
||||
"tokens.created": "Token created",
|
||||
"tokens.createdDescription": "Copy it now, it won't be shown again",
|
||||
"tokens.createNew": "Create a new token",
|
||||
"tokens.createDescription": "Generate a new API token with the desired scope",
|
||||
"tokens.tokenName": "Token name",
|
||||
"tokens.scopeRead": "Read",
|
||||
"tokens.scopeAdmin": "Admin",
|
||||
"tokens.createButton": "Create token",
|
||||
"tokens.name": "Name",
|
||||
"tokens.scope": "Scope",
|
||||
"tokens.prefix": "Prefix",
|
||||
"tokens.status": "Status",
|
||||
"tokens.actions": "Actions",
|
||||
"tokens.revoked": "Revoked",
|
||||
"tokens.active": "Active",
|
||||
"tokens.revoke": "Revoke",
|
||||
|
||||
// Settings page
|
||||
"settings.title": "Settings",
|
||||
"settings.general": "General",
|
||||
"settings.integrations": "Integrations",
|
||||
"settings.savedSuccess": "Settings saved successfully",
|
||||
"settings.savedError": "Failed to save settings",
|
||||
"settings.saveError": "Error saving settings",
|
||||
"settings.cacheClearError": "Failed to clear cache",
|
||||
|
||||
// Settings - Image Processing
|
||||
"settings.imageProcessing": "Image processing",
|
||||
"settings.imageProcessingDesc": "These settings only apply when a client explicitly requests a format conversion via the API (e.g. <code>?format=webp&width=800</code>). Pages served without parameters are delivered as-is from the archive, without processing.",
|
||||
"settings.defaultFormat": "Default output format",
|
||||
"settings.defaultQuality": "Default quality (1-100)",
|
||||
"settings.defaultFilter": "Default resize filter",
|
||||
"settings.filterLanczos": "Lanczos3 (Best quality)",
|
||||
"settings.filterTriangle": "Triangle (Faster)",
|
||||
"settings.filterNearest": "Nearest (Fastest)",
|
||||
"settings.maxWidth": "Maximum allowed width (px)",
|
||||
|
||||
// Settings - Cache
|
||||
"settings.cache": "Cache",
|
||||
"settings.cacheDesc": "Manage image cache and storage",
|
||||
"settings.cacheSize": "Cache size",
|
||||
"settings.files": "Files",
|
||||
"settings.directory": "Directory",
|
||||
"settings.cacheDirectory": "Cache directory",
|
||||
"settings.maxSizeMb": "Max size (MB)",
|
||||
"settings.clearing": "Clearing...",
|
||||
"settings.clearCache": "Clear cache",
|
||||
|
||||
// Settings - Performance
|
||||
"settings.performanceLimits": "Performance limits",
|
||||
"settings.performanceDesc": "Configure API performance, rate limiting, and thumbnail generation concurrency",
|
||||
"settings.concurrentRenders": "Concurrent renders",
|
||||
"settings.concurrentRendersHelp": "Maximum number of parallel page renders and thumbnail generations",
|
||||
"settings.timeoutSeconds": "Timeout (seconds)",
|
||||
"settings.rateLimit": "Rate limit (req/s)",
|
||||
"settings.limitsNote": "Note: Changes to limits require a server restart to take effect. The \"Concurrent renders\" setting controls both page rendering and thumbnail generation parallelism.",
|
||||
|
||||
// Settings - Thumbnails
|
||||
"settings.thumbnails": "Thumbnails",
|
||||
"settings.thumbnailsDesc": "Configure thumbnail generation during indexing",
|
||||
"settings.enableThumbnails": "Enable thumbnails",
|
||||
"settings.outputFormat": "Output format",
|
||||
"settings.formatOriginal": "Original (No re-encoding)",
|
||||
"settings.formatOriginalDesc": "Resizes to target dimensions, keeps the source format (JPEG→JPEG). Much faster generation.",
|
||||
"settings.formatReencodeDesc": "Resizes and re-encodes to the selected format.",
|
||||
"settings.width": "Width (px)",
|
||||
"settings.height": "Height (px)",
|
||||
"settings.quality": "Quality (1-100)",
|
||||
"settings.thumbnailDirectory": "Thumbnail directory",
|
||||
"settings.totalSize": "Total size",
|
||||
"settings.thumbnailsNote": "Note: Thumbnail settings are used during indexing. Existing thumbnails will not be automatically regenerated. Thumbnail generation concurrency is controlled by the \"Concurrent renders\" setting in Performance limits above.",
|
||||
|
||||
// Settings - Komga
|
||||
"settings.komgaSync": "Komga sync",
|
||||
"settings.komgaDesc": "Import reading status from a Komga server. Books are matched by title (case-insensitive). Credentials are not stored.",
|
||||
"settings.komgaUrl": "Komga URL",
|
||||
"settings.username": "Username",
|
||||
"settings.password": "Password",
|
||||
"settings.syncing": "Syncing...",
|
||||
"settings.syncReadBooks": "Sync read books",
|
||||
"settings.komgaRead": "Read on Komga",
|
||||
"settings.matched": "Matched",
|
||||
"settings.alreadyRead": "Already read",
|
||||
"settings.newlyMarked": "Newly marked",
|
||||
"settings.matchedBooks": "{{count}} matched book{{plural}}",
|
||||
"settings.unmatchedBooks": "{{count}} unmatched book{{plural}}",
|
||||
"settings.syncHistory": "Sync history",
|
||||
"settings.read": "read",
|
||||
"settings.new": "new",
|
||||
"settings.unmatched": "unmatched",
|
||||
|
||||
// Settings - Metadata Providers
|
||||
"settings.metadataProviders": "Metadata providers",
|
||||
"settings.metadataProvidersDesc": "Configure external metadata providers for series/book enrichment. Each library can override the default provider. All providers are available for quick search in the metadata modal.",
|
||||
"settings.defaultProvider": "Default provider",
|
||||
"settings.defaultProviderHelp": "Used by default for metadata search. Libraries can override it individually.",
|
||||
"settings.metadataLanguage": "Metadata language",
|
||||
"settings.metadataLanguageHelp": "Preferred language for search results and descriptions. Fallback: English.",
|
||||
"settings.apiKeys": "API keys",
|
||||
"settings.googleBooksKey": "Google Books API key",
|
||||
"settings.googleBooksPlaceholder": "Optional — for higher rate limits",
|
||||
"settings.googleBooksHelp": "Works without a key but with lower rate limits.",
|
||||
"settings.comicvineKey": "ComicVine API key",
|
||||
"settings.comicvinePlaceholder": "Required to use ComicVine",
|
||||
"settings.comicvineHelp": "Get your key at",
|
||||
"settings.freeProviders": "are free and do not require an API key.",
|
||||
|
||||
// Settings - Language
|
||||
"settings.language": "Language",
|
||||
"settings.languageDesc": "Choose the interface language",
|
||||
|
||||
// Pagination
|
||||
"pagination.show": "Show",
|
||||
"pagination.displaying": "Displaying {{count}} items",
|
||||
"pagination.range": "{{start}}-{{end}} of {{total}}",
|
||||
|
||||
// Book detail
|
||||
"bookDetail.libraries": "Libraries",
|
||||
"bookDetail.coverOf": "Cover of {{title}}",
|
||||
"bookDetail.technicalInfo": "Technical information",
|
||||
"bookDetail.file": "File",
|
||||
"bookDetail.fileFormat": "File format",
|
||||
"bookDetail.parsing": "Parsing",
|
||||
"bookDetail.updatedAt": "Updated",
|
||||
|
||||
// Book preview
|
||||
"bookPreview.preview": "Preview",
|
||||
"bookPreview.pages": "pages {{start}}–{{end}} / {{total}}",
|
||||
"bookPreview.prev": "← Prev",
|
||||
"bookPreview.next": "Next →",
|
||||
|
||||
// Edit book form
|
||||
"editBook.editMetadata": "Edit metadata",
|
||||
"editBook.title": "Title",
|
||||
"editBook.titlePlaceholder": "Book title",
|
||||
"editBook.authors": "Author(s)",
|
||||
"editBook.addAuthor": "Add an author (Enter to confirm)",
|
||||
"editBook.language": "Language",
|
||||
"editBook.languagePlaceholder": "e.g. fr, en, jp",
|
||||
"editBook.series": "Series",
|
||||
"editBook.seriesPlaceholder": "Series name",
|
||||
"editBook.volume": "Volume",
|
||||
"editBook.volumePlaceholder": "Volume number",
|
||||
"editBook.isbn": "ISBN",
|
||||
"editBook.publishDate": "Publish date",
|
||||
"editBook.publishDatePlaceholder": "e.g. 2023-01-15",
|
||||
"editBook.description": "Description",
|
||||
"editBook.descriptionPlaceholder": "Summary / book description",
|
||||
"editBook.lockedField": "Locked field (protected from syncs)",
|
||||
"editBook.clickToLock": "Click to lock this field",
|
||||
"editBook.lockedFieldsNote": "Locked fields will not be overwritten by external metadata syncs.",
|
||||
"editBook.saveError": "Error saving",
|
||||
"editBook.savingLabel": "Saving…",
|
||||
"editBook.saveLabel": "Save",
|
||||
"editBook.removeAuthor": "Remove {{name}}",
|
||||
|
||||
// Edit series form
|
||||
"editSeries.title": "Edit series",
|
||||
"editSeries.name": "Name",
|
||||
"editSeries.namePlaceholder": "Series name",
|
||||
"editSeries.startYear": "Start year",
|
||||
"editSeries.startYearPlaceholder": "e.g. 1990",
|
||||
"editSeries.totalVolumes": "Number of volumes",
|
||||
"editSeries.status": "Status",
|
||||
"editSeries.authors": "Author(s)",
|
||||
"editSeries.applyToBooks": "→ books",
|
||||
"editSeries.applyToBooksTitle": "Apply author and language to all books in the series",
|
||||
"editSeries.bookAuthor": "Author (books)",
|
||||
"editSeries.bookAuthorPlaceholder": "Overwrites the author field of each book",
|
||||
"editSeries.bookLanguage": "Language (books)",
|
||||
"editSeries.publishers": "Publisher(s)",
|
||||
"editSeries.addPublisher": "Add a publisher (Enter to confirm)",
|
||||
"editSeries.descriptionPlaceholder": "Synopsis or series description…",
|
||||
|
||||
// Convert button
|
||||
"convert.convertToCbz": "Convert to CBZ",
|
||||
"convert.converting": "Converting…",
|
||||
"convert.started": "Conversion started.",
|
||||
"convert.viewJob": "View job →",
|
||||
"convert.failed": "Conversion failed",
|
||||
"convert.unknownError": "Unknown error",
|
||||
|
||||
// Mark read buttons
|
||||
"markRead.markUnread": "Mark unread",
|
||||
"markRead.markAllRead": "Mark all read",
|
||||
"markRead.markAsRead": "Mark as read",
|
||||
|
||||
// Metadata search modal
|
||||
"metadata.metadataLink": "Metadata link",
|
||||
"metadata.searchExternal": "Search external metadata",
|
||||
"metadata.provider": "Provider:",
|
||||
"metadata.searching": "Searching \"{{name}}\"...",
|
||||
"metadata.noResults": "No results found.",
|
||||
"metadata.resultCount": "{{count}} result{{plural}} found",
|
||||
"metadata.howToSync": "How would you like to sync?",
|
||||
"metadata.syncSeriesOnly": "Sync series only",
|
||||
"metadata.syncSeriesOnlyDesc": "Update description, authors, publishers, and year",
|
||||
"metadata.syncSeriesAndBooks": "Sync series + books",
|
||||
"metadata.syncSeriesAndBooksDesc": "Also fetch the book list and show missing volumes",
|
||||
"metadata.backToResults": "Back to results",
|
||||
"metadata.syncingMetadata": "Syncing metadata...",
|
||||
"metadata.syncSuccess": "Metadata synced successfully!",
|
||||
"metadata.seriesLabel": "Series",
|
||||
"metadata.booksLabel": "Books",
|
||||
"metadata.booksMatched": "{{matched}} matched",
|
||||
"metadata.booksUnmatched": "{{count}} unmatched",
|
||||
"metadata.external": "External",
|
||||
"metadata.local": "Local",
|
||||
"metadata.missingLabel": "Missing",
|
||||
"metadata.missingBooks": "{{count}} missing book{{plural}}",
|
||||
"metadata.unknown": "Unknown",
|
||||
"metadata.linkedTo": "Linked to",
|
||||
"metadata.viewExternal": "View on external source",
|
||||
"metadata.searchAgain": "Search again",
|
||||
"metadata.unlink": "Unlink",
|
||||
"metadata.searchButton": "Search metadata",
|
||||
"metadata.metadataButton": "Metadata",
|
||||
"metadata.locked": "locked",
|
||||
"metadata.searchFailed": "Search failed",
|
||||
"metadata.linkFailed": "Link creation failed",
|
||||
"metadata.approveFailed": "Approval failed",
|
||||
"metadata.chapters": "chapters",
|
||||
"metadata.volumes": "volumes",
|
||||
"metadata.inProgress": "in progress",
|
||||
"metadata.fallbackUsed": "(fallback)",
|
||||
|
||||
// Field labels
|
||||
"field.description": "Description",
|
||||
"field.authors": "Authors",
|
||||
"field.publishers": "Publishers",
|
||||
"field.start_year": "Year",
|
||||
"field.total_volumes": "Volumes",
|
||||
"field.status": "Status",
|
||||
"field.summary": "Summary",
|
||||
"field.isbn": "ISBN",
|
||||
"field.publish_date": "Publish date",
|
||||
"field.language": "Language",
|
||||
|
||||
// Folder picker/browser
|
||||
"folder.selectFolder": "Select a folder...",
|
||||
"folder.selectFolderTitle": "Select folder",
|
||||
"folder.clickToSelect": "Click a folder to select it",
|
||||
"folder.noFolders": "No folders found",
|
||||
|
||||
// Series filters
|
||||
"seriesFilters.all": "All",
|
||||
"seriesFilters.missingBooks": "Missing books",
|
||||
};
|
||||
|
||||
export default en;
|
||||
554
apps/backoffice/lib/i18n/fr.ts
Normal file
554
apps/backoffice/lib/i18n/fr.ts
Normal file
@@ -0,0 +1,554 @@
|
||||
const fr = {
|
||||
// Navigation
|
||||
"nav.dashboard": "Tableau de bord",
|
||||
"nav.books": "Livres",
|
||||
"nav.series": "Séries",
|
||||
"nav.libraries": "Bibliothèques",
|
||||
"nav.jobs": "Tâches",
|
||||
"nav.tokens": "Jetons",
|
||||
"nav.settings": "Paramètres",
|
||||
"nav.navigation": "Navigation",
|
||||
"nav.closeMenu": "Fermer le menu",
|
||||
"nav.openMenu": "Ouvrir le menu",
|
||||
|
||||
// Common
|
||||
"common.save": "Enregistrer",
|
||||
"common.saving": "Enregistrement...",
|
||||
"common.cancel": "Annuler",
|
||||
"common.close": "Fermer",
|
||||
"common.delete": "Supprimer",
|
||||
"common.edit": "Modifier",
|
||||
"common.search": "Rechercher",
|
||||
"common.clear": "Effacer",
|
||||
"common.view": "Voir",
|
||||
"common.all": "Tous",
|
||||
"common.enabled": "Activé",
|
||||
"common.disabled": "Désactivé",
|
||||
"common.browse": "Parcourir",
|
||||
"common.add": "Ajouter",
|
||||
"common.noData": "Aucune donnée",
|
||||
"common.loading": "Chargement...",
|
||||
"common.error": "Erreur",
|
||||
"common.networkError": "Erreur réseau",
|
||||
"common.show": "Afficher",
|
||||
"common.perPage": "par page",
|
||||
"common.next": "Suivant",
|
||||
"common.previous": "Précédent",
|
||||
"common.first": "Premier",
|
||||
"common.previousPage": "Page précédente",
|
||||
"common.nextPage": "Page suivante",
|
||||
"common.backoffice": "backoffice",
|
||||
"common.and": "et",
|
||||
"common.via": "via",
|
||||
|
||||
// Reading status
|
||||
"status.unread": "Non lu",
|
||||
"status.reading": "En cours",
|
||||
"status.read": "Lu",
|
||||
|
||||
// Series status
|
||||
"seriesStatus.ongoing": "En cours",
|
||||
"seriesStatus.ended": "Terminée",
|
||||
"seriesStatus.hiatus": "Hiatus",
|
||||
"seriesStatus.cancelled": "Annulée",
|
||||
"seriesStatus.upcoming": "À paraître",
|
||||
"seriesStatus.allStatuses": "Tous les statuts",
|
||||
"seriesStatus.notDefined": "Non défini",
|
||||
|
||||
// Dashboard
|
||||
"dashboard.title": "Tableau de bord",
|
||||
"dashboard.subtitle": "Aperçu de votre collection de bandes dessinées. Gérez vos bibliothèques, suivez votre progression de lecture et explorez vos livres et séries.",
|
||||
"dashboard.loadError": "Impossible de charger les statistiques. Vérifiez que l'API est en cours d'exécution.",
|
||||
"dashboard.books": "Livres",
|
||||
"dashboard.series": "Séries",
|
||||
"dashboard.libraries": "Bibliothèques",
|
||||
"dashboard.pages": "Pages",
|
||||
"dashboard.authors": "Auteurs",
|
||||
"dashboard.totalSize": "Taille totale",
|
||||
"dashboard.readingStatus": "Statut de lecture",
|
||||
"dashboard.byFormat": "Par format",
|
||||
"dashboard.byLibrary": "Par bibliothèque",
|
||||
"dashboard.booksAdded": "Livres ajoutés (12 derniers mois)",
|
||||
"dashboard.popularSeries": "Séries populaires",
|
||||
"dashboard.noSeries": "Aucune série pour le moment",
|
||||
"dashboard.unknown": "Inconnu",
|
||||
"dashboard.readCount": "{{read}}/{{total}} lu",
|
||||
|
||||
// Books page
|
||||
"books.title": "Livres",
|
||||
"books.searchPlaceholder": "Rechercher par titre, auteur, série...",
|
||||
"books.library": "Bibliothèque",
|
||||
"books.allLibraries": "Toutes les bibliothèques",
|
||||
"books.status": "Statut",
|
||||
"books.sort": "Tri",
|
||||
"books.sortTitle": "Titre",
|
||||
"books.sortLatest": "Ajout récent",
|
||||
"books.resultCount": "{{count}} résultat{{plural}}",
|
||||
"books.resultCountFor": "{{count}} résultat{{plural}} pour \u00ab {{query}} \u00bb",
|
||||
"books.bookCount": "{{count}} livre{{plural}}",
|
||||
"books.seriesHeading": "Séries",
|
||||
"books.unclassified": "Non classé",
|
||||
"books.noResults": "Aucun livre trouvé pour \"{{query}}\"",
|
||||
"books.noBooks": "Aucun livre disponible",
|
||||
"books.coverOf": "Couverture de {{name}}",
|
||||
|
||||
// Series page
|
||||
"series.title": "Séries",
|
||||
"series.searchPlaceholder": "Rechercher par nom de série...",
|
||||
"series.reading": "Lecture",
|
||||
"series.missing": "Manquant",
|
||||
"series.missingBooks": "Livres manquants",
|
||||
"series.matchingQuery": "correspondant à",
|
||||
"series.noResults": "Aucune série trouvée correspondant à vos filtres",
|
||||
"series.noSeries": "Aucune série disponible",
|
||||
"series.missingCount": "{{count}} manquant{{plural}}",
|
||||
"series.readCount": "{{read}}/{{total}} lu{{plural}}",
|
||||
|
||||
// Libraries page
|
||||
"libraries.title": "Bibliothèques",
|
||||
"libraries.addLibrary": "Ajouter une bibliothèque",
|
||||
"libraries.addLibraryDescription": "Créer une nouvelle bibliothèque à partir d'un dossier existant",
|
||||
"libraries.disabled": "Désactivée",
|
||||
"libraries.books": "Livres",
|
||||
"libraries.series": "Séries",
|
||||
"libraries.auto": "Auto",
|
||||
"libraries.manual": "Manuel",
|
||||
"libraries.nextScan": "Prochain : {{time}}",
|
||||
"libraries.imminent": "Imminent",
|
||||
"libraries.index": "Indexer",
|
||||
"libraries.fullIndex": "Complet",
|
||||
"libraries.batchMetadata": "Métadonnées en lot",
|
||||
"libraries.libraryName": "Nom de la bibliothèque",
|
||||
"libraries.addButton": "Ajouter une bibliothèque",
|
||||
|
||||
// Library sub-pages
|
||||
"libraryBooks.allBooks": "Tous les livres",
|
||||
"libraryBooks.booksOfSeries": "Livres de \"{{series}}\"",
|
||||
"libraryBooks.filterLabel": "Livres de la série \"{{series}}\"",
|
||||
"libraryBooks.viewAll": "Voir tous les livres",
|
||||
"libraryBooks.noBooks": "Aucun livre dans cette bibliothèque",
|
||||
"libraryBooks.noBooksInSeries": "Aucun livre dans la série \"{{series}}\"",
|
||||
"librarySeries.noSeries": "Aucune série trouvée dans cette bibliothèque",
|
||||
"librarySeries.noBooksInSeries": "Aucun livre dans cette série",
|
||||
|
||||
// Library actions
|
||||
"libraryActions.autoScan": "Scan auto",
|
||||
"libraryActions.fileWatch": "Surveillance fichiers ⚡",
|
||||
"libraryActions.schedule": "📅 Planification",
|
||||
"libraryActions.provider": "Fournisseur",
|
||||
"libraryActions.fallback": "Secours",
|
||||
"libraryActions.default": "Par défaut",
|
||||
"libraryActions.none": "Aucun",
|
||||
"libraryActions.saving": "Enregistrement...",
|
||||
|
||||
// Library sub-page header
|
||||
"libraryHeader.libraries": "Bibliothèques",
|
||||
"libraryHeader.bookCount": "{{count}} livre{{plural}}",
|
||||
"libraryHeader.enabled": "Activée",
|
||||
|
||||
// Monitoring
|
||||
"monitoring.auto": "Auto",
|
||||
"monitoring.manual": "Manuel",
|
||||
"monitoring.hourly": "Toutes les heures",
|
||||
"monitoring.daily": "Quotidien",
|
||||
"monitoring.weekly": "Hebdomadaire",
|
||||
"monitoring.fileWatch": "Surveillance des fichiers en temps réel",
|
||||
|
||||
// Jobs page
|
||||
"jobs.title": "Tâches d'indexation",
|
||||
"jobs.startJob": "Lancer une tâche",
|
||||
"jobs.startJobDescription": "Sélectionnez une bibliothèque (ou toutes) et choisissez l'action à effectuer.",
|
||||
"jobs.allLibraries": "Toutes les bibliothèques",
|
||||
"jobs.rebuild": "Reconstruction",
|
||||
"jobs.fullRebuild": "Reconstruction complète",
|
||||
"jobs.generateThumbnails": "Générer les miniatures",
|
||||
"jobs.regenerateThumbnails": "Regénérer les miniatures",
|
||||
"jobs.batchMetadata": "Métadonnées en lot",
|
||||
"jobs.referenceTitle": "Référence des types de tâches",
|
||||
"jobs.rebuildDescription": "Scan incrémental : détecte les fichiers ajoutés, modifiés ou supprimés depuis le dernier scan, les indexe et génère les miniatures manquantes. Les données existantes non modifiées sont conservées. C'est l'action la plus courante et la plus rapide.",
|
||||
"jobs.fullRebuildDescription": "Supprime toutes les données indexées (livres, séries, miniatures) puis effectue un scan complet depuis zéro. Utile si la base de données est désynchronisée ou corrompue. Opération longue et destructive : les statuts de lecture et les métadonnées manuelles seront perdus.",
|
||||
"jobs.generateThumbnailsDescription": "Génère les miniatures uniquement pour les livres qui n'en ont pas encore. Les miniatures existantes ne sont pas touchées. Utile après un import ou si certaines miniatures sont manquantes.",
|
||||
"jobs.regenerateThumbnailsDescription": "Regénère toutes les miniatures depuis zéro, en remplaçant les existantes. Utile si la qualité ou la taille des miniatures a changé dans la configuration, ou si des miniatures sont corrompues.",
|
||||
"jobs.batchMetadataDescription": "Recherche automatiquement les métadonnées de chaque série de la bibliothèque auprès du provider configuré (avec fallback si configuré). Seuls les résultats avec un match unique à 100% de confiance sont appliqués automatiquement. Les séries déjà liées sont ignorées. Un rapport détaillé par série est disponible à la fin du job. <strong>Requiert une bibliothèque spécifique</strong> (ne fonctionne pas sur \u00ab Toutes les bibliothèques \u00bb).",
|
||||
|
||||
// Jobs list
|
||||
"jobsList.id": "ID",
|
||||
"jobsList.library": "Bibliothèque",
|
||||
"jobsList.type": "Type",
|
||||
"jobsList.status": "Statut",
|
||||
"jobsList.files": "Fichiers",
|
||||
"jobsList.thumbnails": "Miniatures",
|
||||
"jobsList.duration": "Durée",
|
||||
"jobsList.created": "Créé",
|
||||
"jobsList.actions": "Actions",
|
||||
|
||||
// Job row
|
||||
"jobRow.showProgress": "Afficher la progression",
|
||||
"jobRow.hideProgress": "Masquer la progression",
|
||||
"jobRow.scanned": "{{count}} analysés",
|
||||
"jobRow.view": "Voir",
|
||||
|
||||
// Job progress
|
||||
"jobProgress.loadingProgress": "Chargement de la progression...",
|
||||
"jobProgress.sseError": "Échec de l'analyse des données SSE",
|
||||
"jobProgress.connectionLost": "Connexion perdue",
|
||||
"jobProgress.error": "Erreur : {{message}}",
|
||||
"jobProgress.done": "Terminé",
|
||||
"jobProgress.currentFile": "En cours : {{file}}",
|
||||
"jobProgress.pages": "pages",
|
||||
"jobProgress.thumbnails": "miniatures",
|
||||
"jobProgress.filesUnit": "fichiers",
|
||||
"jobProgress.scanned": "Analysés : {{count}}",
|
||||
"jobProgress.indexed": "Indexés : {{count}}",
|
||||
"jobProgress.removed": "Supprimés : {{count}}",
|
||||
"jobProgress.errors": "Erreurs : {{count}}",
|
||||
|
||||
// Job detail
|
||||
"jobDetail.backToJobs": "Retour aux tâches",
|
||||
"jobDetail.title": "Détails de la tâche",
|
||||
"jobDetail.completedIn": "Terminé en {{duration}}",
|
||||
"jobDetail.failedAfter": "après {{duration}}",
|
||||
"jobDetail.jobFailed": "Tâche échouée",
|
||||
"jobDetail.cancelled": "Annulé",
|
||||
"jobDetail.overview": "Aperçu",
|
||||
"jobDetail.timeline": "Chronologie",
|
||||
"jobDetail.created": "Créé",
|
||||
"jobDetail.started": "Démarré",
|
||||
"jobDetail.pendingStart": "En attente de démarrage…",
|
||||
"jobDetail.finished": "Terminé",
|
||||
"jobDetail.failed": "Échoué",
|
||||
"jobDetail.library": "Bibliothèque",
|
||||
"jobDetail.book": "Livre",
|
||||
"jobDetail.allLibraries": "Toutes les bibliothèques",
|
||||
"jobDetail.phase1": "Phase 1 — Découverte",
|
||||
"jobDetail.phase2a": "Phase 2a — Extraction des pages",
|
||||
"jobDetail.phase2b": "Phase 2b — Génération des miniatures",
|
||||
"jobDetail.metadataSearch": "Recherche de métadonnées",
|
||||
"jobDetail.metadataSearchDesc": "Recherche auprès des fournisseurs externes pour chaque série",
|
||||
"jobDetail.phase1Desc": "Scan et indexation des fichiers de la bibliothèque",
|
||||
"jobDetail.phase2aDesc": "Extraction de la première page de chaque archive (nombre de pages + image brute)",
|
||||
"jobDetail.phase2bDesc": "Génération des miniatures pour les livres analysés",
|
||||
"jobDetail.inProgress": "en cours",
|
||||
"jobDetail.duration": "Durée : {{duration}}",
|
||||
"jobDetail.currentFile": "Fichier en cours",
|
||||
"jobDetail.generated": "Générés",
|
||||
"jobDetail.processed": "Traités",
|
||||
"jobDetail.total": "Total",
|
||||
"jobDetail.remaining": "Restants",
|
||||
"jobDetail.indexStats": "Statistiques d'indexation",
|
||||
"jobDetail.scanned": "Scannés",
|
||||
"jobDetail.indexed": "Indexés",
|
||||
"jobDetail.removed": "Supprimés",
|
||||
"jobDetail.warnings": "Avertissements",
|
||||
"jobDetail.errors": "Erreurs",
|
||||
"jobDetail.thumbnailStats": "Statistiques des miniatures",
|
||||
"jobDetail.batchReport": "Rapport du lot",
|
||||
"jobDetail.seriesAnalyzed": "{{count}} séries analysées",
|
||||
"jobDetail.autoMatched": "Auto-associé",
|
||||
"jobDetail.alreadyLinked": "Déjà lié",
|
||||
"jobDetail.noResults": "Aucun résultat",
|
||||
"jobDetail.tooManyResults": "Trop de résultats",
|
||||
"jobDetail.lowConfidence": "Confiance faible",
|
||||
"jobDetail.resultsBySeries": "Résultats par série",
|
||||
"jobDetail.seriesProcessed": "{{count}} séries traitées",
|
||||
"jobDetail.candidates": "candidat{{plural}}",
|
||||
"jobDetail.confidence": "confiance",
|
||||
"jobDetail.match": "Correspondance : {{title}}",
|
||||
"jobDetail.fileErrors": "Erreurs de fichiers ({{count}})",
|
||||
"jobDetail.fileErrorsDesc": "Erreurs rencontrées lors du traitement des fichiers",
|
||||
|
||||
// Job types
|
||||
"jobType.rebuild": "Indexation",
|
||||
"jobType.full_rebuild": "Indexation complète",
|
||||
"jobType.thumbnail_rebuild": "Miniatures",
|
||||
"jobType.thumbnail_regenerate": "Régén. miniatures",
|
||||
"jobType.cbr_to_cbz": "CBR → CBZ",
|
||||
"jobType.metadata_batch": "Métadonnées en lot",
|
||||
"jobType.rebuildLabel": "Indexation incrémentale",
|
||||
"jobType.rebuildDesc": "Scanne les fichiers nouveaux/modifiés, les analyse et génère les miniatures manquantes.",
|
||||
"jobType.full_rebuildLabel": "Réindexation complète",
|
||||
"jobType.full_rebuildDesc": "Supprime toutes les données existantes puis effectue un scan complet, une ré-analyse et la génération des miniatures.",
|
||||
"jobType.thumbnail_rebuildLabel": "Reconstruction des miniatures",
|
||||
"jobType.thumbnail_rebuildDesc": "Génère les miniatures uniquement pour les livres qui n'en ont pas. Les miniatures existantes sont conservées.",
|
||||
"jobType.thumbnail_regenerateLabel": "Regénération des miniatures",
|
||||
"jobType.thumbnail_regenerateDesc": "Regénère toutes les miniatures depuis zéro, en remplaçant les existantes.",
|
||||
"jobType.cbr_to_cbzLabel": "Conversion CBR → CBZ",
|
||||
"jobType.cbr_to_cbzDesc": "Convertit une archive CBR au format ouvert CBZ.",
|
||||
"jobType.metadata_batchLabel": "Métadonnées en lot",
|
||||
"jobType.metadata_batchDesc": "Recherche les métadonnées auprès des fournisseurs externes pour toutes les séries de la bibliothèque et applique automatiquement les correspondances à 100% de confiance.",
|
||||
|
||||
// Status badges
|
||||
"statusBadge.extracting_pages": "Extraction des pages",
|
||||
"statusBadge.generating_thumbnails": "Miniatures",
|
||||
|
||||
// Jobs indicator
|
||||
"jobsIndicator.viewAll": "Voir toutes les tâches",
|
||||
"jobsIndicator.activeTasks": "Tâches actives",
|
||||
"jobsIndicator.runningAndPending": "{{running}} en cours, {{pending}} en attente",
|
||||
"jobsIndicator.pendingTasks": "{{count}} tâche{{plural}} en attente",
|
||||
"jobsIndicator.overallProgress": "Progression globale",
|
||||
"jobsIndicator.viewAllLink": "Tout voir →",
|
||||
"jobsIndicator.noActiveTasks": "Aucune tâche active",
|
||||
"jobsIndicator.autoRefresh": "Actualisation automatique toutes les 2s",
|
||||
"jobsIndicator.taskCount": "{{count}} tâche{{plural}} active{{plural}}",
|
||||
"jobsIndicator.thumbnails": "Miniatures",
|
||||
"jobsIndicator.regeneration": "Regénération",
|
||||
|
||||
// Time
|
||||
"time.justNow": "À l'instant",
|
||||
"time.minutesAgo": "il y a {{count}}m",
|
||||
"time.hoursAgo": "il y a {{count}}h",
|
||||
|
||||
// Tokens page
|
||||
"tokens.title": "Jetons API",
|
||||
"tokens.created": "Jeton créé",
|
||||
"tokens.createdDescription": "Copiez-le maintenant, il ne sera plus affiché",
|
||||
"tokens.createNew": "Créer un nouveau jeton",
|
||||
"tokens.createDescription": "Générer un nouveau jeton API avec la portée souhaitée",
|
||||
"tokens.tokenName": "Nom du jeton",
|
||||
"tokens.scopeRead": "Lecture",
|
||||
"tokens.scopeAdmin": "Admin",
|
||||
"tokens.createButton": "Créer le jeton",
|
||||
"tokens.name": "Nom",
|
||||
"tokens.scope": "Portée",
|
||||
"tokens.prefix": "Préfixe",
|
||||
"tokens.status": "Statut",
|
||||
"tokens.actions": "Actions",
|
||||
"tokens.revoked": "Révoqué",
|
||||
"tokens.active": "Actif",
|
||||
"tokens.revoke": "Révoquer",
|
||||
|
||||
// Settings page
|
||||
"settings.title": "Paramètres",
|
||||
"settings.general": "Général",
|
||||
"settings.integrations": "Intégrations",
|
||||
"settings.savedSuccess": "Paramètres enregistrés avec succès",
|
||||
"settings.savedError": "Échec de l'enregistrement des paramètres",
|
||||
"settings.saveError": "Erreur lors de l'enregistrement des paramètres",
|
||||
"settings.cacheClearError": "Échec du vidage du cache",
|
||||
|
||||
// Settings - Image Processing
|
||||
"settings.imageProcessing": "Traitement d'images",
|
||||
"settings.imageProcessingDesc": "Ces paramètres s'appliquent uniquement lorsqu'un client demande explicitement une conversion de format via l'API (ex. <code>?format=webp&width=800</code>). Les pages servies sans paramètres sont livrées telles quelles depuis l'archive, sans traitement.",
|
||||
"settings.defaultFormat": "Format de sortie par défaut",
|
||||
"settings.defaultQuality": "Qualité par défaut (1-100)",
|
||||
"settings.defaultFilter": "Filtre de redimensionnement par défaut",
|
||||
"settings.filterLanczos": "Lanczos3 (Meilleure qualité)",
|
||||
"settings.filterTriangle": "Triangle (Plus rapide)",
|
||||
"settings.filterNearest": "Nearest (Le plus rapide)",
|
||||
"settings.maxWidth": "Largeur maximale autorisée (px)",
|
||||
|
||||
// Settings - Cache
|
||||
"settings.cache": "Cache",
|
||||
"settings.cacheDesc": "Gérer le cache d'images et le stockage",
|
||||
"settings.cacheSize": "Taille du cache",
|
||||
"settings.files": "Fichiers",
|
||||
"settings.directory": "Répertoire",
|
||||
"settings.cacheDirectory": "Répertoire du cache",
|
||||
"settings.maxSizeMb": "Taille max (Mo)",
|
||||
"settings.clearing": "Vidage en cours...",
|
||||
"settings.clearCache": "Vider le cache",
|
||||
|
||||
// Settings - Performance
|
||||
"settings.performanceLimits": "Limites de performance",
|
||||
"settings.performanceDesc": "Configurer les performances de l'API, la limitation de débit et la concurrence de génération des miniatures",
|
||||
"settings.concurrentRenders": "Rendus simultanés",
|
||||
"settings.concurrentRendersHelp": "Nombre maximum de rendus de pages et de générations de miniatures en parallèle",
|
||||
"settings.timeoutSeconds": "Délai d'expiration (secondes)",
|
||||
"settings.rateLimit": "Limite de débit (req/s)",
|
||||
"settings.limitsNote": "Note : Les modifications des limites nécessitent un redémarrage du serveur pour prendre effet. Le paramètre « Rendus simultanés » contrôle à la fois le rendu des pages et le parallélisme de génération des miniatures.",
|
||||
|
||||
// Settings - Thumbnails
|
||||
"settings.thumbnails": "Miniatures",
|
||||
"settings.thumbnailsDesc": "Configurer la génération des miniatures pendant l'indexation",
|
||||
"settings.enableThumbnails": "Activer les miniatures",
|
||||
"settings.outputFormat": "Format de sortie",
|
||||
"settings.formatOriginal": "Original (Sans ré-encodage)",
|
||||
"settings.formatOriginalDesc": "Redimensionne aux dimensions cibles, conserve le format source (JPEG→JPEG). Génération beaucoup plus rapide.",
|
||||
"settings.formatReencodeDesc": "Redimensionne et ré-encode dans le format sélectionné.",
|
||||
"settings.width": "Largeur (px)",
|
||||
"settings.height": "Hauteur (px)",
|
||||
"settings.quality": "Qualité (1-100)",
|
||||
"settings.thumbnailDirectory": "Répertoire des miniatures",
|
||||
"settings.totalSize": "Taille totale",
|
||||
"settings.thumbnailsNote": "Note : Les paramètres des miniatures sont utilisés pendant l'indexation. Les miniatures existantes ne seront pas regénérées automatiquement. La concurrence de génération des miniatures est contrôlée par le paramètre « Rendus simultanés » dans les Limites de performance ci-dessus.",
|
||||
|
||||
// Settings - Komga
|
||||
"settings.komgaSync": "Synchronisation Komga",
|
||||
"settings.komgaDesc": "Importer le statut de lecture depuis un serveur Komga. Les livres sont associés par titre (insensible à la casse). Les identifiants ne sont pas stockés.",
|
||||
"settings.komgaUrl": "URL Komga",
|
||||
"settings.username": "Nom d'utilisateur",
|
||||
"settings.password": "Mot de passe",
|
||||
"settings.syncing": "Synchronisation...",
|
||||
"settings.syncReadBooks": "Synchroniser les livres lus",
|
||||
"settings.komgaRead": "Lus sur Komga",
|
||||
"settings.matched": "Associés",
|
||||
"settings.alreadyRead": "Déjà lus",
|
||||
"settings.newlyMarked": "Nouvellement marqués",
|
||||
"settings.matchedBooks": "{{count}} livre{{plural}} associé{{plural}}",
|
||||
"settings.unmatchedBooks": "{{count}} unmatched book{{plural}}",
|
||||
"settings.syncHistory": "Historique de synchronisation",
|
||||
"settings.read": "lus",
|
||||
"settings.new": "nouveaux",
|
||||
"settings.unmatched": "non associés",
|
||||
|
||||
// Settings - Metadata Providers
|
||||
"settings.metadataProviders": "Fournisseurs de métadonnées",
|
||||
"settings.metadataProvidersDesc": "Configurer les fournisseurs de métadonnées externes pour l'enrichissement des séries/livres. Chaque bibliothèque peut remplacer le fournisseur par défaut. Tous les fournisseurs sont disponibles pour la recherche rapide dans la modale de métadonnées.",
|
||||
"settings.defaultProvider": "Fournisseur par défaut",
|
||||
"settings.defaultProviderHelp": "Utilisé par défaut pour la recherche de métadonnées. Les bibliothèques peuvent le remplacer individuellement.",
|
||||
"settings.metadataLanguage": "Langue des métadonnées",
|
||||
"settings.metadataLanguageHelp": "Langue préférée pour les résultats de recherche et les descriptions. Secours : anglais.",
|
||||
"settings.apiKeys": "Clés API",
|
||||
"settings.googleBooksKey": "Clé API Google Books",
|
||||
"settings.googleBooksPlaceholder": "Optionnel — pour des limites de débit plus élevées",
|
||||
"settings.googleBooksHelp": "Fonctionne sans clé mais avec des limites de débit plus basses.",
|
||||
"settings.comicvineKey": "Clé API ComicVine",
|
||||
"settings.comicvinePlaceholder": "Requise pour utiliser ComicVine",
|
||||
"settings.comicvineHelp": "Obtenez votre clé sur",
|
||||
"settings.freeProviders": "sont gratuits et ne nécessitent pas de clé API.",
|
||||
|
||||
// Settings - Language
|
||||
"settings.language": "Langue",
|
||||
"settings.languageDesc": "Choisir la langue de l'interface",
|
||||
|
||||
// Pagination
|
||||
"pagination.show": "Afficher",
|
||||
"pagination.displaying": "Affichage de {{count}} éléments",
|
||||
"pagination.range": "{{start}}-{{end}} sur {{total}}",
|
||||
|
||||
// Book detail
|
||||
"bookDetail.libraries": "Bibliothèques",
|
||||
"bookDetail.coverOf": "Couverture de {{title}}",
|
||||
"bookDetail.technicalInfo": "Informations techniques",
|
||||
"bookDetail.file": "Fichier",
|
||||
"bookDetail.fileFormat": "Format fichier",
|
||||
"bookDetail.parsing": "Parsing",
|
||||
"bookDetail.updatedAt": "Mis à jour",
|
||||
|
||||
// Book preview
|
||||
"bookPreview.preview": "Aperçu",
|
||||
"bookPreview.pages": "pages {{start}}–{{end}} / {{total}}",
|
||||
"bookPreview.prev": "← Préc.",
|
||||
"bookPreview.next": "Suiv. →",
|
||||
|
||||
// Edit book form
|
||||
"editBook.editMetadata": "Modifier les métadonnées",
|
||||
"editBook.title": "Titre",
|
||||
"editBook.titlePlaceholder": "Titre du livre",
|
||||
"editBook.authors": "Auteur(s)",
|
||||
"editBook.addAuthor": "Ajouter un auteur (Entrée pour valider)",
|
||||
"editBook.language": "Langue",
|
||||
"editBook.languagePlaceholder": "ex : fr, en, jp",
|
||||
"editBook.series": "Série",
|
||||
"editBook.seriesPlaceholder": "Nom de la série",
|
||||
"editBook.volume": "Volume",
|
||||
"editBook.volumePlaceholder": "Numéro de volume",
|
||||
"editBook.isbn": "ISBN",
|
||||
"editBook.publishDate": "Date de publication",
|
||||
"editBook.publishDatePlaceholder": "ex : 2023-01-15",
|
||||
"editBook.description": "Description",
|
||||
"editBook.descriptionPlaceholder": "Résumé / description du livre",
|
||||
"editBook.lockedField": "Champ verrouillé (protégé des synchros)",
|
||||
"editBook.clickToLock": "Cliquer pour verrouiller ce champ",
|
||||
"editBook.lockedFieldsNote": "Les champs verrouillés ne seront pas écrasés par les synchros de métadonnées externes.",
|
||||
"editBook.saveError": "Erreur lors de la sauvegarde",
|
||||
"editBook.savingLabel": "Sauvegarde…",
|
||||
"editBook.saveLabel": "Sauvegarder",
|
||||
"editBook.removeAuthor": "Supprimer {{name}}",
|
||||
|
||||
// Edit series form
|
||||
"editSeries.title": "Modifier la série",
|
||||
"editSeries.name": "Nom",
|
||||
"editSeries.namePlaceholder": "Nom de la série",
|
||||
"editSeries.startYear": "Année de début",
|
||||
"editSeries.startYearPlaceholder": "ex : 1990",
|
||||
"editSeries.totalVolumes": "Nombre de volumes",
|
||||
"editSeries.status": "Statut",
|
||||
"editSeries.authors": "Auteur(s)",
|
||||
"editSeries.applyToBooks": "→ livres",
|
||||
"editSeries.applyToBooksTitle": "Appliquer auteur et langue à tous les livres de la série",
|
||||
"editSeries.bookAuthor": "Auteur (livres)",
|
||||
"editSeries.bookAuthorPlaceholder": "Écrase le champ auteur de chaque livre",
|
||||
"editSeries.bookLanguage": "Langue (livres)",
|
||||
"editSeries.publishers": "Éditeur(s)",
|
||||
"editSeries.addPublisher": "Ajouter un éditeur (Entrée pour valider)",
|
||||
"editSeries.descriptionPlaceholder": "Synopsis ou description de la série…",
|
||||
|
||||
// Convert button
|
||||
"convert.convertToCbz": "Convertir en CBZ",
|
||||
"convert.converting": "Conversion…",
|
||||
"convert.started": "Conversion lancée.",
|
||||
"convert.viewJob": "Voir la tâche →",
|
||||
"convert.failed": "Échec de la conversion",
|
||||
"convert.unknownError": "Erreur inconnue",
|
||||
|
||||
// Mark read buttons
|
||||
"markRead.markUnread": "Marquer non lu",
|
||||
"markRead.markAllRead": "Tout marquer lu",
|
||||
"markRead.markAsRead": "Marquer comme lu",
|
||||
|
||||
// Metadata search modal
|
||||
"metadata.metadataLink": "Lien métadonnées",
|
||||
"metadata.searchExternal": "Rechercher les métadonnées externes",
|
||||
"metadata.provider": "Fournisseur :",
|
||||
"metadata.searching": "Recherche de \"{{name}}\"...",
|
||||
"metadata.noResults": "Aucun résultat trouvé.",
|
||||
"metadata.resultCount": "{{count}} résultat{{plural}} trouvé{{plural}}",
|
||||
"metadata.howToSync": "Comment souhaitez-vous synchroniser ?",
|
||||
"metadata.syncSeriesOnly": "Synchroniser la série uniquement",
|
||||
"metadata.syncSeriesOnlyDesc": "Mettre à jour la description, les auteurs, les éditeurs et l'année",
|
||||
"metadata.syncSeriesAndBooks": "Synchroniser la série + les livres",
|
||||
"metadata.syncSeriesAndBooksDesc": "Récupérer aussi la liste des livres et afficher les tomes manquants",
|
||||
"metadata.backToResults": "Retour aux résultats",
|
||||
"metadata.syncingMetadata": "Synchronisation des métadonnées...",
|
||||
"metadata.syncSuccess": "Métadonnées synchronisées avec succès !",
|
||||
"metadata.seriesLabel": "Série",
|
||||
"metadata.booksLabel": "Livres",
|
||||
"metadata.booksMatched": "{{matched}} associé{{plural}}",
|
||||
"metadata.booksUnmatched": "{{count}} non associé{{plural}}",
|
||||
"metadata.external": "Externe",
|
||||
"metadata.local": "Locaux",
|
||||
"metadata.missingLabel": "Manquants",
|
||||
"metadata.missingBooks": "{{count}} livre{{plural}} manquant{{plural}}",
|
||||
"metadata.unknown": "Inconnu",
|
||||
"metadata.linkedTo": "Lié à",
|
||||
"metadata.viewExternal": "Voir sur la source externe",
|
||||
"metadata.searchAgain": "Rechercher à nouveau",
|
||||
"metadata.unlink": "Dissocier",
|
||||
"metadata.searchButton": "Rechercher les métadonnées",
|
||||
"metadata.metadataButton": "Métadonnées",
|
||||
"metadata.locked": "verrouillé",
|
||||
"metadata.searchFailed": "Échec de la recherche",
|
||||
"metadata.linkFailed": "Échec de la création du lien",
|
||||
"metadata.approveFailed": "Échec de l'approbation",
|
||||
"metadata.chapters": "chapitres",
|
||||
"metadata.volumes": "volumes",
|
||||
"metadata.inProgress": "en cours",
|
||||
"metadata.fallbackUsed": "(secours)",
|
||||
|
||||
// Field labels
|
||||
"field.description": "Description",
|
||||
"field.authors": "Auteurs",
|
||||
"field.publishers": "Éditeurs",
|
||||
"field.start_year": "Année",
|
||||
"field.total_volumes": "Nb volumes",
|
||||
"field.status": "Statut",
|
||||
"field.summary": "Résumé",
|
||||
"field.isbn": "ISBN",
|
||||
"field.publish_date": "Date de publication",
|
||||
"field.language": "Langue",
|
||||
|
||||
// Folder picker/browser
|
||||
"folder.selectFolder": "Sélectionner un dossier...",
|
||||
"folder.selectFolderTitle": "Sélectionner le dossier",
|
||||
"folder.clickToSelect": "Cliquez sur un dossier pour le sélectionner",
|
||||
"folder.noFolders": "Aucun dossier trouvé",
|
||||
|
||||
// Series filters
|
||||
"seriesFilters.all": "Tous",
|
||||
"seriesFilters.missingBooks": "Livres manquants",
|
||||
} as const;
|
||||
|
||||
export type TranslationKey = keyof typeof fr;
|
||||
export default fr;
|
||||
5
apps/backoffice/lib/i18n/index.ts
Normal file
5
apps/backoffice/lib/i18n/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { type Locale, DEFAULT_LOCALE, LOCALE_COOKIE, LOCALES } from "./types";
|
||||
export { getDictionary, getDictionarySync, createTranslateFunction, type TranslateFunction } from "./dictionaries";
|
||||
export { getServerLocale, getServerTranslations } from "./server";
|
||||
export { LocaleProvider, useTranslation } from "./context";
|
||||
export { type TranslationKey } from "./fr";
|
||||
20
apps/backoffice/lib/i18n/server.ts
Normal file
20
apps/backoffice/lib/i18n/server.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { cookies } from "next/headers";
|
||||
import type { Locale } from "./types";
|
||||
import { DEFAULT_LOCALE, LOCALE_COOKIE, LOCALES } from "./types";
|
||||
import { getDictionarySync, createTranslateFunction } from "./dictionaries";
|
||||
import type { TranslateFunction } from "./dictionaries";
|
||||
|
||||
export async function getServerLocale(): Promise<Locale> {
|
||||
const cookieStore = await cookies();
|
||||
const raw = cookieStore.get(LOCALE_COOKIE)?.value;
|
||||
if (raw && LOCALES.includes(raw as Locale)) {
|
||||
return raw as Locale;
|
||||
}
|
||||
return DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
export async function getServerTranslations(): Promise<{ t: TranslateFunction; locale: Locale }> {
|
||||
const locale = await getServerLocale();
|
||||
const dict = getDictionarySync(locale);
|
||||
return { t: createTranslateFunction(dict), locale };
|
||||
}
|
||||
5
apps/backoffice/lib/i18n/types.ts
Normal file
5
apps/backoffice/lib/i18n/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type Locale = "fr" | "en";
|
||||
|
||||
export const DEFAULT_LOCALE: Locale = "en";
|
||||
export const LOCALE_COOKIE = "locale";
|
||||
export const LOCALES: Locale[] = ["fr", "en"];
|
||||
Reference in New Issue
Block a user