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

@@ -8,6 +8,9 @@ import { ThemeToggle } from "./theme-toggle";
import { JobsIndicator } from "./components/JobsIndicator";
import { NavIcon, Icon } from "./components/ui";
import { MobileNav } from "./components/MobileNav";
import { LocaleProvider } from "../lib/i18n/context";
import { getServerLocale, getServerTranslations } from "../lib/i18n/server";
import type { TranslationKey } from "../lib/i18n/fr";
export const metadata: Metadata = {
title: "StripStream Backoffice",
@@ -16,37 +19,41 @@ export const metadata: Metadata = {
type NavItem = {
href: "/" | "/books" | "/series" | "/libraries" | "/jobs" | "/tokens" | "/settings";
label: string;
labelKey: TranslationKey;
icon: "dashboard" | "books" | "series" | "libraries" | "jobs" | "tokens" | "settings";
};
const navItems: NavItem[] = [
{ href: "/", label: "Tableau de bord", icon: "dashboard" },
{ href: "/books", label: "Livres", icon: "books" },
{ href: "/series", label: "ries", icon: "series" },
{ href: "/libraries", label: "Bibliothèques", icon: "libraries" },
{ href: "/jobs", label: "Tâches", icon: "jobs" },
{ href: "/tokens", label: "Jetons", icon: "tokens" },
{ href: "/", labelKey: "nav.dashboard", icon: "dashboard" },
{ href: "/books", labelKey: "nav.books", icon: "books" },
{ href: "/series", labelKey: "nav.series", icon: "series" },
{ href: "/libraries", labelKey: "nav.libraries", icon: "libraries" },
{ href: "/jobs", labelKey: "nav.jobs", icon: "jobs" },
{ href: "/tokens", labelKey: "nav.tokens", icon: "tokens" },
];
export default function RootLayout({ children }: { children: ReactNode }) {
export default async function RootLayout({ children }: { children: ReactNode }) {
const locale = await getServerLocale();
const { t } = await getServerTranslations();
return (
<html lang="fr" suppressHydrationWarning>
<html lang={locale} suppressHydrationWarning>
<body className="min-h-screen bg-background text-foreground font-sans antialiased bg-grain">
<ThemeProvider>
<LocaleProvider initialLocale={locale}>
{/* Header avec effet glassmorphism */}
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/70 backdrop-blur-xl backdrop-saturate-150 supports-[backdrop-filter]:bg-background/60">
<nav className="container mx-auto flex h-16 items-center justify-between px-4">
{/* Brand */}
<Link
href="/"
<Link
href="/"
className="flex items-center gap-3 hover:opacity-80 transition-opacity duration-200"
>
<Image
src="/logo.png"
alt="StripStream"
width={36}
height={36}
<Image
src="/logo.png"
alt="StripStream"
width={36}
height={36}
className="rounded-lg"
/>
<div className="flex items-baseline gap-2">
@@ -54,7 +61,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
StripStream
</span>
<span className="text-sm text-muted-foreground font-medium hidden md:inline">
backoffice
{t("common.backoffice")}
</span>
</div>
</Link>
@@ -63,9 +70,9 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<div className="flex items-center gap-2">
<div className="hidden md:flex items-center gap-1">
{navItems.map((item) => (
<NavLink key={item.href} href={item.href} title={item.label}>
<NavLink key={item.href} href={item.href} title={t(item.labelKey)}>
<NavIcon name={item.icon} />
<span className="ml-2 hidden lg:inline">{item.label}</span>
<span className="ml-2 hidden lg:inline">{t(item.labelKey)}</span>
</NavLink>
))}
</div>
@@ -76,12 +83,12 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<Link
href="/settings"
className="hidden md:flex p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
title="Paramètres"
title={t("nav.settings")}
>
<Icon name="settings" size="md" />
</Link>
<ThemeToggle />
<MobileNav navItems={navItems} />
<MobileNav navItems={navItems.map(item => ({ ...item, label: t(item.labelKey) }))} />
</div>
</div>
</nav>
@@ -91,6 +98,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 pb-16">
{children}
</main>
</LocaleProvider>
</ThemeProvider>
</body>
</html>