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:
2026-03-18 19:39:01 +01:00
parent 055c376222
commit d4f87c4044
43 changed files with 2024 additions and 693 deletions

View File

@@ -3,6 +3,7 @@
import { useState, useRef, useEffect, useTransition } from "react";
import { Button } from "../components/ui";
import { ProviderIcon } from "../components/ProviderIcon";
import { useTranslation } from "../../lib/i18n/context";
interface LibraryActionsProps {
libraryId: string;
@@ -23,6 +24,7 @@ export function LibraryActions({
fallbackMetadataProvider,
onUpdate
}: LibraryActionsProps) {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [isPending, startTransition] = useTransition();
const [saveError, setSaveError] = useState<string | null>(null);
@@ -109,7 +111,7 @@ export function LibraryActions({
defaultChecked={monitorEnabled}
className="w-4 h-4 rounded border-border text-primary focus:ring-ring"
/>
Scan auto
{t("libraryActions.autoScan")}
</label>
</div>
@@ -122,36 +124,36 @@ export function LibraryActions({
defaultChecked={watcherEnabled}
className="w-4 h-4 rounded border-border text-primary focus:ring-ring"
/>
Surveillance fichiers
{t("libraryActions.fileWatch")}
</label>
</div>
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">📅 Planification</label>
<label className="text-sm font-medium text-foreground">{t("libraryActions.schedule")}</label>
<select
name="scan_mode"
defaultValue={scanMode}
className="text-sm border border-border rounded-lg px-2 py-1 bg-background"
>
<option value="manual">Manuel</option>
<option value="hourly">Toutes les heures</option>
<option value="daily">Quotidien</option>
<option value="weekly">Hebdomadaire</option>
<option value="manual">{t("monitoring.manual")}</option>
<option value="hourly">{t("monitoring.hourly")}</option>
<option value="daily">{t("monitoring.daily")}</option>
<option value="weekly">{t("monitoring.weekly")}</option>
</select>
</div>
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground flex items-center gap-1.5">
{metadataProvider && <ProviderIcon provider={metadataProvider} size={16} />}
Fournisseur
{t("libraryActions.provider")}
</label>
<select
name="metadata_provider"
defaultValue={metadataProvider || ""}
className="text-sm border border-border rounded-lg px-2 py-1 bg-background"
>
<option value="">Par défaut</option>
<option value="none">Aucun</option>
<option value="">{t("libraryActions.default")}</option>
<option value="none">{t("libraryActions.none")}</option>
<option value="google_books">Google Books</option>
<option value="comicvine">ComicVine</option>
<option value="open_library">Open Library</option>
@@ -163,14 +165,14 @@ export function LibraryActions({
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground flex items-center gap-1.5">
{fallbackMetadataProvider && fallbackMetadataProvider !== "none" && <ProviderIcon provider={fallbackMetadataProvider} size={16} />}
Secours
{t("libraryActions.fallback")}
</label>
<select
name="fallback_metadata_provider"
defaultValue={fallbackMetadataProvider || ""}
className="text-sm border border-border rounded-lg px-2 py-1 bg-background"
>
<option value="">Aucun</option>
<option value="">{t("libraryActions.none")}</option>
<option value="google_books">Google Books</option>
<option value="comicvine">ComicVine</option>
<option value="open_library">Open Library</option>
@@ -191,7 +193,7 @@ export function LibraryActions({
className="w-full"
disabled={isPending}
>
{isPending ? "Enregistrement..." : "Enregistrer"}
{isPending ? t("libraryActions.saving") : t("common.save")}
</Button>
</div>
</form>