feat: first shoot on translation
This commit is contained in:
@@ -4,6 +4,8 @@ import "@/styles/globals.css";
|
||||
import { cn } from "@/lib/utils";
|
||||
import ClientLayout from "@/components/layout/ClientLayout";
|
||||
import { PreferencesProvider } from "@/contexts/PreferencesContext";
|
||||
import { I18nProvider } from "@/components/providers/I18nProvider";
|
||||
import "@/i18n/i18n"; // Import i18next configuration
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
@@ -114,9 +116,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
/>
|
||||
</head>
|
||||
<body className={cn("min-h-screen bg-background font-sans antialiased", inter.className)}>
|
||||
<PreferencesProvider>
|
||||
<ClientLayout>{children}</ClientLayout>
|
||||
</PreferencesProvider>
|
||||
<I18nProvider>
|
||||
<PreferencesProvider>
|
||||
<ClientLayout>{children}</ClientLayout>
|
||||
</PreferencesProvider>
|
||||
</I18nProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { LoginForm } from "@/components/auth/LoginForm";
|
||||
import { RegisterForm } from "@/components/auth/RegisterForm";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
|
||||
interface LoginContentProps {
|
||||
searchParams: {
|
||||
@@ -12,6 +13,7 @@ interface LoginContentProps {
|
||||
}
|
||||
|
||||
export function LoginContent({ searchParams }: LoginContentProps) {
|
||||
const { t } = useTranslate();
|
||||
const defaultTab = searchParams.tab || "login";
|
||||
|
||||
return (
|
||||
@@ -41,25 +43,20 @@ export function LoginContent({ searchParams }: LoginContentProps) {
|
||||
</div>
|
||||
<div className="relative z-20 mt-auto">
|
||||
<blockquote className="space-y-2">
|
||||
<p className="text-lg">
|
||||
Profitez de vos BD, mangas et comics préférés avec une expérience de lecture moderne
|
||||
et fluide.
|
||||
</p>
|
||||
<p className="text-lg">{t("login.description")}</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:p-8">
|
||||
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
||||
<div className="flex flex-col space-y-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Bienvenue sur StripStream</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Connectez-vous ou créez un compte pour commencer
|
||||
</p>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{t("login.title")}</h1>
|
||||
<p className="text-sm text-muted-foreground">{t("login.subtitle")}</p>
|
||||
</div>
|
||||
<Tabs defaultValue={defaultTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="login">Connexion</TabsTrigger>
|
||||
<TabsTrigger value="register">Inscription</TabsTrigger>
|
||||
<TabsTrigger value="login">{t("login.tabs.login")}</TabsTrigger>
|
||||
<TabsTrigger value="register">{t("login.tabs.register")}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="login">
|
||||
<LoginForm from={searchParams.from} />
|
||||
|
||||
43
src/components/LanguageSelector.tsx
Normal file
43
src/components/LanguageSelector.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Languages } from "lucide-react";
|
||||
|
||||
export default function LanguageSelector() {
|
||||
const { t, i18n } = useTranslation("common");
|
||||
|
||||
const handleLanguageChange = (newLocale: string) => {
|
||||
i18n.changeLanguage(newLocale);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-9 w-9" aria-label={t("language.select")}>
|
||||
<Languages className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleLanguageChange("fr")}
|
||||
className={i18n.language === "fr" ? "bg-accent" : ""}
|
||||
>
|
||||
{t("language.fr")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleLanguageChange("en")}
|
||||
className={i18n.language === "en" ? "bg-accent" : ""}
|
||||
>
|
||||
{t("language.en")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Cover } from "@/components/ui/cover";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
|
||||
interface OptimizedHeroSeries {
|
||||
id: string;
|
||||
@@ -12,6 +15,8 @@ interface HeroSectionProps {
|
||||
}
|
||||
|
||||
export function HeroSection({ series }: HeroSectionProps) {
|
||||
const { t } = useTranslate();
|
||||
|
||||
// console.log("HeroSection - Séries reçues:", {
|
||||
// count: series?.length || 0,
|
||||
// firstSeries: series?.[0],
|
||||
@@ -29,7 +34,7 @@ export function HeroSection({ series }: HeroSectionProps) {
|
||||
<Cover
|
||||
type="series"
|
||||
id={series.id}
|
||||
alt={`Couverture de ${series.metadata.title}`}
|
||||
alt={t("home.hero.coverAlt", { title: series.metadata.title })}
|
||||
quality={25}
|
||||
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 16.666vw"
|
||||
/>
|
||||
@@ -43,10 +48,10 @@ export function HeroSection({ series }: HeroSectionProps) {
|
||||
{/* Contenu */}
|
||||
<div className="relative h-full container flex flex-col items-center justify-center text-center space-y-2 sm:space-y-4">
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-6xl font-bold tracking-tight">
|
||||
Bienvenue sur StripStream
|
||||
{t("home.hero.title")}
|
||||
</h1>
|
||||
<p className="text-lg sm:text-xl text-muted-foreground max-w-[600px]">
|
||||
Votre bibliothèque numérique pour lire vos BD, mangas et comics préférés.
|
||||
{t("home.hero.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { HeroSection } from "./HeroSection";
|
||||
import { MediaRow } from "./MediaRow";
|
||||
import { KomgaBook, KomgaSeries } from "@/types/komga";
|
||||
import { RefreshButton } from "@/components/library/RefreshButton";
|
||||
import { History, Sparkles, Clock, LibraryBig, BookOpen } from "lucide-react";
|
||||
import { HomeData } from "@/lib/services/home.service";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
|
||||
interface HomeContentProps {
|
||||
data: HomeData;
|
||||
@@ -11,6 +14,8 @@ interface HomeContentProps {
|
||||
}
|
||||
|
||||
export function HomeContent({ data, refreshHome }: HomeContentProps) {
|
||||
const { t } = useTranslate();
|
||||
|
||||
// Vérification des données pour le debug
|
||||
// console.log("HomeContent - Données reçues:", {
|
||||
// ongoingCount: data.ongoing?.length || 0,
|
||||
@@ -51,7 +56,7 @@ export function HomeContent({ data, refreshHome }: HomeContentProps) {
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-8 space-y-12">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold">Accueil</h1>
|
||||
<h1 className="text-3xl font-bold">{t("home.title")}</h1>
|
||||
<RefreshButton libraryId="home" refreshLibrary={refreshHome} />
|
||||
</div>
|
||||
{/* Hero Section - Afficher uniquement si nous avons des séries en cours */}
|
||||
@@ -63,7 +68,7 @@ export function HomeContent({ data, refreshHome }: HomeContentProps) {
|
||||
<div className="space-y-12">
|
||||
{data.ongoing && data.ongoing.length > 0 && (
|
||||
<MediaRow
|
||||
title="Continuer la série"
|
||||
title={t("home.sections.continue_series")}
|
||||
items={optimizeSeriesData(data.ongoing)}
|
||||
icon={<LibraryBig className="w-6 h-6" />}
|
||||
/>
|
||||
@@ -71,7 +76,7 @@ export function HomeContent({ data, refreshHome }: HomeContentProps) {
|
||||
|
||||
{data.ongoingBooks && data.ongoingBooks.length > 0 && (
|
||||
<MediaRow
|
||||
title="Continuer la lecture"
|
||||
title={t("home.sections.continue_reading")}
|
||||
items={optimizeBookData(data.ongoingBooks)}
|
||||
icon={<BookOpen className="w-6 h-6" />}
|
||||
/>
|
||||
@@ -79,7 +84,7 @@ export function HomeContent({ data, refreshHome }: HomeContentProps) {
|
||||
|
||||
{data.onDeck && data.onDeck.length > 0 && (
|
||||
<MediaRow
|
||||
title="À suivre"
|
||||
title={t("home.sections.up_next")}
|
||||
items={optimizeBookData(data.onDeck)}
|
||||
icon={<Clock className="w-6 h-6" />}
|
||||
/>
|
||||
@@ -87,7 +92,7 @@ export function HomeContent({ data, refreshHome }: HomeContentProps) {
|
||||
|
||||
{data.latestSeries && data.latestSeries.length > 0 && (
|
||||
<MediaRow
|
||||
title="Dernières séries"
|
||||
title={t("home.sections.latest_series")}
|
||||
items={optimizeSeriesData(data.latestSeries)}
|
||||
icon={<Sparkles className="w-6 h-6" />}
|
||||
/>
|
||||
@@ -95,7 +100,7 @@ export function HomeContent({ data, refreshHome }: HomeContentProps) {
|
||||
|
||||
{data.recentlyRead && data.recentlyRead.length > 0 && (
|
||||
<MediaRow
|
||||
title="Ajouts récents"
|
||||
title={t("home.sections.recently_added")}
|
||||
items={optimizeBookData(data.recentlyRead)}
|
||||
icon={<History className="w-6 h-6" />}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Menu, Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import LanguageSelector from "@/components/LanguageSelector";
|
||||
|
||||
interface HeaderProps {
|
||||
onToggleSidebar: () => void;
|
||||
@@ -33,7 +34,8 @@ export function Header({ onToggleSidebar }: HeaderProps) {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center justify-between space-x-2 md:justify-end">
|
||||
<nav className="flex items-center">
|
||||
<nav className="flex items-center space-x-2">
|
||||
<LanguageSelector />
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="px-2 py-1.5 hover:bg-accent hover:text-accent-foreground rounded-md"
|
||||
|
||||
8
src/components/providers/I18nProvider.tsx
Normal file
8
src/components/providers/I18nProvider.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { PropsWithChildren, useEffect } from "react";
|
||||
import "@/i18n/i18n";
|
||||
|
||||
export function I18nProvider({ children }: PropsWithChildren) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
187
src/components/ui/dropdown-menu.tsx
Normal file
187
src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
16
src/hooks/useTranslate.ts
Normal file
16
src/hooks/useTranslate.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function useTranslate() {
|
||||
const { t, i18n } = useTranslation("common");
|
||||
|
||||
const changeLanguage = (lang: string) => {
|
||||
i18n.changeLanguage(lang);
|
||||
};
|
||||
|
||||
return {
|
||||
t,
|
||||
i18n,
|
||||
changeLanguage,
|
||||
currentLanguage: i18n.language,
|
||||
};
|
||||
}
|
||||
42
src/i18n/i18n.ts
Normal file
42
src/i18n/i18n.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
// Importation des traductions
|
||||
import frCommon from "./messages/fr/common.json";
|
||||
import enCommon from "./messages/en/common.json";
|
||||
|
||||
// Ne pas initialiser i18next plus d'une fois
|
||||
if (!i18n.isInitialized) {
|
||||
i18n
|
||||
.use(LanguageDetector) // Détecte la langue du navigateur
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
fr: {
|
||||
common: frCommon,
|
||||
},
|
||||
en: {
|
||||
common: enCommon,
|
||||
},
|
||||
},
|
||||
defaultNS: "common",
|
||||
fallbackLng: "fr",
|
||||
interpolation: {
|
||||
escapeValue: false, // React gère déjà l'échappement
|
||||
},
|
||||
detection: {
|
||||
order: ["cookie", "localStorage", "navigator"],
|
||||
lookupCookie: "NEXT_LOCALE",
|
||||
caches: ["cookie"],
|
||||
cookieOptions: {
|
||||
path: "/",
|
||||
maxAge: 365 * 24 * 60 * 60, // 1 an
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default i18n;
|
||||
31
src/i18n/messages/en/common.json
Normal file
31
src/i18n/messages/en/common.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"language": {
|
||||
"select": "Change language",
|
||||
"fr": "French",
|
||||
"en": "English"
|
||||
},
|
||||
"login": {
|
||||
"title": "Welcome to StripStream",
|
||||
"subtitle": "Sign in or create an account to get started",
|
||||
"tabs": {
|
||||
"login": "Sign in",
|
||||
"register": "Sign up"
|
||||
},
|
||||
"description": "Enjoy your favorite comics, manga and graphic novels with a modern and smooth reading experience."
|
||||
},
|
||||
"home": {
|
||||
"hero": {
|
||||
"title": "Welcome to StripStream",
|
||||
"subtitle": "Your digital library to read your favorite comics, manga and graphic novels.",
|
||||
"coverAlt": "Cover of {title}"
|
||||
},
|
||||
"title": "Home",
|
||||
"sections": {
|
||||
"continue_series": "Continue series",
|
||||
"continue_reading": "Continue reading",
|
||||
"up_next": "Up next",
|
||||
"latest_series": "Latest series",
|
||||
"recently_added": "Recently added"
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/i18n/messages/fr/common.json
Normal file
31
src/i18n/messages/fr/common.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"language": {
|
||||
"select": "Changer la langue",
|
||||
"fr": "Français",
|
||||
"en": "Anglais"
|
||||
},
|
||||
"login": {
|
||||
"title": "Bienvenue sur StripStream",
|
||||
"subtitle": "Connectez-vous ou créez un compte pour commencer",
|
||||
"tabs": {
|
||||
"login": "Connexion",
|
||||
"register": "Inscription"
|
||||
},
|
||||
"description": "Profitez de vos BD, mangas et comics préférés avec une expérience de lecture moderne et fluide."
|
||||
},
|
||||
"home": {
|
||||
"hero": {
|
||||
"title": "Bienvenue sur StripStream",
|
||||
"subtitle": "Votre bibliothèque numérique pour lire vos BD, mangas et comics préférés.",
|
||||
"coverAlt": "Couverture de {title}"
|
||||
},
|
||||
"title": "Accueil",
|
||||
"sections": {
|
||||
"continue_series": "Continuer la série",
|
||||
"continue_reading": "Continuer la lecture",
|
||||
"up_next": "À suivre",
|
||||
"latest_series": "Dernières séries",
|
||||
"recently_added": "Ajouts récents"
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/types/json.d.ts
vendored
Normal file
4
src/types/json.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "*.json" {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
Reference in New Issue
Block a user