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:
@@ -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: "Sé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>
|
||||
|
||||
Reference in New Issue
Block a user