From 2206c8237948cf7bc52d51d63c6cca52aa86824d Mon Sep 17 00:00:00 2001 From: Puechberty Arthur Date: Mon, 20 Apr 2026 21:32:49 +0200 Subject: [PATCH] add multi language for the website --- .../web/app/{ => [locale]}/dashboard/page.tsx | 2 +- apps/web/app/[locale]/layout.tsx | 90 +++ apps/web/app/[locale]/login/page.tsx | 20 + apps/web/app/{ => [locale]}/page.tsx | 8 +- apps/web/app/globals.css | 48 ++ apps/web/app/layout.tsx | 33 - apps/web/app/login/page.tsx | 19 - apps/web/components/Features.tsx | 52 +- apps/web/components/Footer.tsx | 10 +- apps/web/components/Hero.tsx | 39 +- apps/web/components/HowItWorks.tsx | 22 +- apps/web/components/LanguageSwitcher.tsx | 49 ++ apps/web/components/Logo.tsx | 2 +- apps/web/components/dashboard-client.tsx | 85 ++- apps/web/i18n/client.ts | 5 + apps/web/i18n/navigation.ts | 6 + apps/web/i18n/next-intl.d.ts | 12 + apps/web/i18n/request.ts | 50 ++ apps/web/i18n/routing.ts | 9 + apps/web/i18n/server.ts | 5 + apps/web/messages/en.json | 135 ++++ apps/web/messages/fr.json | 135 ++++ apps/web/middleware.ts | 9 + apps/web/next.config.mjs | 6 +- apps/web/package.json | 1 + package-lock.json | 693 +++++++++++++++++- 26 files changed, 1409 insertions(+), 136 deletions(-) rename apps/web/app/{ => [locale]}/dashboard/page.tsx (83%) create mode 100644 apps/web/app/[locale]/layout.tsx create mode 100644 apps/web/app/[locale]/login/page.tsx rename apps/web/app/{ => [locale]}/page.tsx (67%) delete mode 100644 apps/web/app/layout.tsx delete mode 100644 apps/web/app/login/page.tsx create mode 100644 apps/web/components/LanguageSwitcher.tsx create mode 100644 apps/web/i18n/client.ts create mode 100644 apps/web/i18n/navigation.ts create mode 100644 apps/web/i18n/next-intl.d.ts create mode 100644 apps/web/i18n/request.ts create mode 100644 apps/web/i18n/routing.ts create mode 100644 apps/web/i18n/server.ts create mode 100644 apps/web/messages/en.json create mode 100644 apps/web/messages/fr.json create mode 100644 apps/web/middleware.ts diff --git a/apps/web/app/dashboard/page.tsx b/apps/web/app/[locale]/dashboard/page.tsx similarity index 83% rename from apps/web/app/dashboard/page.tsx rename to apps/web/app/[locale]/dashboard/page.tsx index 3bfbc08..ae92b5b 100644 --- a/apps/web/app/dashboard/page.tsx +++ b/apps/web/app/[locale]/dashboard/page.tsx @@ -1,4 +1,4 @@ -import { DashboardClient } from "../../components/dashboard-client"; +import { DashboardClient } from "../../../components/dashboard-client"; const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:4000"; diff --git a/apps/web/app/[locale]/layout.tsx b/apps/web/app/[locale]/layout.tsx new file mode 100644 index 0000000..0b0487a --- /dev/null +++ b/apps/web/app/[locale]/layout.tsx @@ -0,0 +1,90 @@ +import type { Metadata } from "next"; +import { IBM_Plex_Mono, Space_Grotesk } from "next/font/google"; +import { hasLocale, NextIntlClientProvider } from "next-intl"; +import { getMessages } from "next-intl/server"; +import { notFound } from "next/navigation"; + +import LanguageSwitcher from "../../components/LanguageSwitcher"; +import { routing, type AppLocale } from "../../i18n/routing"; +import enMessages from "../../messages/en.json"; +import frMessages from "../../messages/fr.json"; +import "../globals.css"; + +const headingFont = Space_Grotesk({ + subsets: ["latin"], + variable: "--font-heading", + weight: ["500", "700"], +}); + +const monoFont = IBM_Plex_Mono({ + subsets: ["latin"], + variable: "--font-mono", + weight: ["400", "500"], +}); + +const metadataByLocale: Record = { + en: enMessages.metadata, + fr: frMessages.metadata, +}; + +export function generateStaticParams() { + return routing.locales.map((locale) => ({ locale })); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale: candidateLocale } = await params; + const locale = hasLocale(routing.locales, candidateLocale) + ? candidateLocale + : routing.defaultLocale; + const metadata = metadataByLocale[locale]; + + const languages = routing.locales.reduce>( + (accumulator, currentLocale) => { + accumulator[currentLocale] = `/${currentLocale}`; + return accumulator; + }, + {}, + ); + + return { + title: metadata.title, + description: metadata.description, + alternates: { + canonical: `/${locale}`, + languages, + }, + }; +} + +export default async function LocaleLayout({ + children, + params, +}: Readonly<{ + children: React.ReactNode; + params: Promise<{ locale: string }>; +}>) { + const { locale } = await params; + + if (!hasLocale(routing.locales, locale)) { + notFound(); + } + + const messages = await getMessages(); + + return ( + + + +
+ +
+ {children} +
+ + + ); +} diff --git a/apps/web/app/[locale]/login/page.tsx b/apps/web/app/[locale]/login/page.tsx new file mode 100644 index 0000000..1ef04ce --- /dev/null +++ b/apps/web/app/[locale]/login/page.tsx @@ -0,0 +1,20 @@ +import { getT } from "../../../i18n/server"; + +const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:4000"; + +export default async function LoginPage() { + const t = await getT(); + + return ( +
+
+

{t("login.eyebrow")}

+

{t("login.title")}

+

{t("login.description")}

+ + {t("login.cta")} + +
+
+ ); +} diff --git a/apps/web/app/page.tsx b/apps/web/app/[locale]/page.tsx similarity index 67% rename from apps/web/app/page.tsx rename to apps/web/app/[locale]/page.tsx index 2df66f8..3122b63 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/[locale]/page.tsx @@ -1,7 +1,7 @@ -import Hero from "../components/Hero"; -import Features from "../components/Features"; -import HowItWorks from "../components/HowItWorks"; -import Footer from "../components/Footer"; +import Features from "../../components/Features"; +import Footer from "../../components/Footer"; +import Hero from "../../components/Hero"; +import HowItWorks from "../../components/HowItWorks"; export default function HomePage() { return ( diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index d868d21..dacac51 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -37,6 +37,43 @@ body { linear-gradient(140deg, var(--sand-50), #f8f7f4 55%, #fff3e3); } +.locale-switcher-shell { + position: fixed; + top: 1rem; + right: 1rem; + z-index: 80; +} + +.locale-switcher { + display: inline-flex; + align-items: center; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.22); + background: rgba(23, 20, 16, 0.74); + padding: 0.2rem 0.55rem; + backdrop-filter: blur(8px); + box-shadow: 0 12px 36px rgba(0, 0, 0, 0.2); +} + +.locale-switcher select { + border: 0; + background: transparent; + color: #fff; + font-size: 0.84rem; + font-weight: 700; + font-family: var(--font-heading), sans-serif; + cursor: pointer; +} + +.locale-switcher select:focus { + outline: none; +} + +.locale-switcher select:disabled { + opacity: 0.7; + cursor: progress; +} + .page-shell { min-height: 100vh; display: grid; @@ -283,3 +320,14 @@ input:focus { padding: 2rem; } } + +@media (max-width: 640px) { + .locale-switcher-shell { + top: 0.7rem; + right: 0.7rem; + } + + .locale-switcher select { + font-size: 0.78rem; + } +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx deleted file mode 100644 index 85ddcb1..0000000 --- a/apps/web/app/layout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { Metadata } from "next"; -import { IBM_Plex_Mono, Space_Grotesk } from "next/font/google"; - -import "./globals.css"; - -const headingFont = Space_Grotesk({ - subsets: ["latin"], - variable: "--font-heading", - weight: ["500", "700"], -}); - -const monoFont = IBM_Plex_Mono({ - subsets: ["latin"], - variable: "--font-mono", - weight: ["400", "500"], -}); - -export const metadata: Metadata = { - title: "Discord Bot SaaS", - description: "Multi-tenant Discord bot management platform", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - {children} - - ); -} diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx deleted file mode 100644 index 94d0099..0000000 --- a/apps/web/app/login/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:4000"; - -export default function LoginPage() { - return ( -
-
-

Discord Bot SaaS Platform

-

Connecte ton compte Discord

-

- Authentifie-toi via OAuth2 pour accéder à ton espace multi-tenant et piloter les bots de ton - organisation. -

- - Continuer avec Discord - -
-
- ); -} diff --git a/apps/web/components/Features.tsx b/apps/web/components/Features.tsx index e9c1874..e2b8730 100644 --- a/apps/web/components/Features.tsx +++ b/apps/web/components/Features.tsx @@ -1,11 +1,13 @@ import FeatureCard from "./FeatureCard"; +import { getT } from "../i18n/server"; + +export default async function Features() { + const t = await getT(); -export default function Features() { const items = [ { - title: "Multi-bot", - description: - "Gérez plusieurs bots indépendants depuis un tableau de bord unique — configurations et logs centralisés.", + title: t("features.items.autoCommands.title"), + description: t("features.items.autoCommands.description"), icon: ( @@ -14,8 +16,8 @@ export default function Features() { ), }, { - title: "Gestion simple", - description: "Démarrez, arrêtez, et configurez en quelques clics — interface claire et rapide.", + title: t("features.items.componentsV2.title"), + description: t("features.items.componentsV2.description"), icon: ( @@ -24,8 +26,38 @@ export default function Features() { ), }, { - title: "Scalable", - description: "Scale automatique des instances pour absorber la charge et garder la disponibilité.", + title: t("features.items.autoHelp.title"), + description: t("features.items.autoHelp.description"), + icon: ( + + + + + ), + }, + { + title: t("features.items.multiLanguage.title"), + description: t("features.items.multiLanguage.description"), + icon: ( + + + + + ), + }, + { + title: t("features.items.dynamicVariables.title"), + description: t("features.items.dynamicVariables.description"), + icon: ( + + + + + ), + }, + { + title: t("features.items.hosting.title"), + description: t("features.items.hosting.description"), icon: ( @@ -37,8 +69,8 @@ export default function Features() { return (
-

Fonctionnalités principales

-

Toutes les fonctionnalités essentielles pour piloter vos bots en production.

+

{t("features.title")}

+

{t("features.subtitle")}

{items.map((it, idx) => ( diff --git a/apps/web/components/Footer.tsx b/apps/web/components/Footer.tsx index 0f2fcd8..7ca598d 100644 --- a/apps/web/components/Footer.tsx +++ b/apps/web/components/Footer.tsx @@ -1,8 +1,14 @@ -export default function Footer() { +import { getT } from "../i18n/server"; + +export default async function Footer() { + const t = await getT(); + return (
-
© {new Date().getFullYear()} Shadow
+
+ Copyright {new Date().getFullYear()} Shadow - {t("footer.tagline")} +
diff --git a/apps/web/components/Hero.tsx b/apps/web/components/Hero.tsx index 020cbaa..58a9b9d 100644 --- a/apps/web/components/Hero.tsx +++ b/apps/web/components/Hero.tsx @@ -1,11 +1,15 @@ -import Link from "next/link"; +import { Link } from "../i18n/navigation"; +import { getT } from "../i18n/server"; + import Logo from "./Logo"; -export default function Hero() { +export default async function Hero() { + const t = await getT(); + const bots = [ - { name: "Moderation", status: "online" }, - { name: "Welcome", status: "stopped" }, - { name: "Analytics", status: "online" }, + { name: t("hero.bots.moderation"), status: "online" as const }, + { name: t("hero.bots.welcome"), status: "stopped" as const }, + { name: t("hero.bots.analytics"), status: "online" as const }, ]; return ( @@ -13,28 +17,29 @@ export default function Hero() {

- Shadow — Gérez vos bots Discord simplement + {t("hero.title")}

- Centralisez, déployez et contrôlez tous vos bots depuis un tableau de bord moderne. Démarrez, - stoppez et scalez en temps réel, en toute simplicité. + {t("hero.description")}

- - Accéder au dashboard + + {t("hero.ctaStart")} - Se connecter + {t("hero.ctaLogin")}
@@ -46,20 +51,22 @@ export default function Hero() {
{b.name}
-
Instances: 1 · CPU 35%
+
{t("hero.card.instanceMetrics")}
- {b.status === "online" ? "Online" : "Stopped"} + {b.status === "online" ? t("hero.status.online") : t("hero.status.stopped")} - +
))}
-
Aperçu en temps réel du statut de vos bots
+
{t("hero.card.preview")}
diff --git a/apps/web/components/HowItWorks.tsx b/apps/web/components/HowItWorks.tsx index 2d46d48..e63a50a 100644 --- a/apps/web/components/HowItWorks.tsx +++ b/apps/web/components/HowItWorks.tsx @@ -1,23 +1,27 @@ -export default function HowItWorks() { +import { getT } from "../i18n/server"; + +export default async function HowItWorks() { + const t = await getT(); + const steps = [ { - title: "Ajouter un bot", - desc: "Connectez votre application Discord via OAuth et enregistrez-la dans Shadow.", + title: t("howItWorks.steps.addBot.title"), + desc: t("howItWorks.steps.addBot.description"), }, { - title: "Gérer depuis le dashboard", - desc: "Accédez aux commandes, aux logs et aux paramètres depuis une interface centralisée.", + title: t("howItWorks.steps.dashboard.title"), + desc: t("howItWorks.steps.dashboard.description"), }, { - title: "Contrôler en temps réel", - desc: "Démarrez, arrêtez et scalez vos bots instantanément selon la demande.", + title: t("howItWorks.steps.realtime.title"), + desc: t("howItWorks.steps.realtime.description"), }, ]; return (
-

Comment ça marche

-

Trois étapes simples pour prendre le contrôle de vos bots.

+

{t("howItWorks.title")}

+

{t("howItWorks.subtitle")}

{steps.map((s, i) => ( diff --git a/apps/web/components/LanguageSwitcher.tsx b/apps/web/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..df0bd8f --- /dev/null +++ b/apps/web/components/LanguageSwitcher.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useLocale } from "next-intl"; +import { useSearchParams } from "next/navigation"; +import { useTransition } from "react"; + +import { useT } from "../i18n/client"; +import { usePathname, useRouter } from "../i18n/navigation"; +import { routing, type AppLocale } from "../i18n/routing"; + +export default function LanguageSwitcher() { + const t = useT(); + const locale = useLocale() as AppLocale; + const pathname = usePathname(); + const router = useRouter(); + const searchParams = useSearchParams(); + const [isPending, startTransition] = useTransition(); + + const localeLabels: Record = { + en: t("localeSwitcher.locales.en"), + fr: t("localeSwitcher.locales.fr"), + }; + + const handleLocaleChange = (nextLocale: AppLocale) => { + const query = Object.fromEntries(searchParams.entries()); + + startTransition(() => { + router.replace({ pathname, query }, { locale: nextLocale }); + }); + }; + + return ( + + ); +} diff --git a/apps/web/components/Logo.tsx b/apps/web/components/Logo.tsx index 2ddf591..fe2c95a 100644 --- a/apps/web/components/Logo.tsx +++ b/apps/web/components/Logo.tsx @@ -1,4 +1,4 @@ -import Link from "next/link"; +import { Link } from "../i18n/navigation"; export default function Logo({ className = "" }: { className?: string }) { return ( diff --git a/apps/web/components/dashboard-client.tsx b/apps/web/components/dashboard-client.tsx index e1ecf92..8cf647b 100644 --- a/apps/web/components/dashboard-client.tsx +++ b/apps/web/components/dashboard-client.tsx @@ -1,6 +1,7 @@ "use client"; import { type FormEvent, useCallback, useEffect, useState } from "react"; +import { useT } from "../i18n/client"; type User = { id: string; @@ -27,15 +28,25 @@ type DashboardClientProps = { apiBaseUrl: string; }; -const statusLabel: Record = { - stopped: "Arrete", - starting: "Demarrage", - running: "En ligne", - stopping: "Arret", - error: "Erreur", -}; +type BotAction = "start" | "stop" | "restart"; export function DashboardClient({ apiBaseUrl }: DashboardClientProps) { + const t = useT(); + + const statusLabel: Record = { + stopped: t("dashboard.status.stopped"), + starting: t("dashboard.status.starting"), + running: t("dashboard.status.running"), + stopping: t("dashboard.status.stopping"), + error: t("dashboard.status.error"), + }; + + const actionLabel: Record = { + start: t("dashboard.actions.start"), + stop: t("dashboard.actions.stop"), + restart: t("dashboard.actions.restart"), + }; + const [user, setUser] = useState(null); const [bots, setBots] = useState([]); const [token, setToken] = useState(""); @@ -61,7 +72,7 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) { } if (!meResponse.ok) { - throw new Error("Impossible de recuperer la session utilisateur"); + throw new Error(t("dashboard.errors.fetchSession")); } const meJson = await meResponse.json(); @@ -72,18 +83,18 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) { }); if (!botsResponse.ok) { - throw new Error("Impossible de recuperer la liste des bots"); + throw new Error(t("dashboard.errors.fetchBots")); } const botsJson = await botsResponse.json(); setBots((botsJson.bots ?? []) as Bot[]); } catch (cause) { - const message = cause instanceof Error ? cause.message : "Erreur inattendue"; + const message = cause instanceof Error ? cause.message : t("dashboard.errors.unexpected"); setError(message); } finally { setLoading(false); } - }, [apiBaseUrl]); + }, [apiBaseUrl, t]); useEffect(() => { void refreshData(); @@ -119,21 +130,21 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) { if (!response.ok) { const errorJson = await response.json().catch(() => ({})); - throw new Error(errorJson.error ?? "Ajout du bot impossible"); + throw new Error(errorJson.error ?? t("dashboard.errors.addBot")); } setToken(""); setDisplayName(""); await refreshData(); } catch (cause) { - const message = cause instanceof Error ? cause.message : "Erreur inattendue"; + const message = cause instanceof Error ? cause.message : t("dashboard.errors.unexpected"); setError(message); } finally { setSubmitting(false); } }; - const triggerAction = async (botId: string, action: "start" | "stop" | "restart") => { + const triggerAction = async (botId: string, action: BotAction) => { setError(null); try { @@ -144,28 +155,28 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) { if (!response.ok) { const errorJson = await response.json().catch(() => ({})); - throw new Error(errorJson.error ?? `Action ${action} impossible`); + throw new Error(errorJson.error ?? t("dashboard.errors.actionFailed", { action: actionLabel[action] })); } await refreshData(); } catch (cause) { - const message = cause instanceof Error ? cause.message : "Erreur inattendue"; + const message = cause instanceof Error ? cause.message : t("dashboard.errors.unexpected"); setError(message); } }; if (loading) { - return

Chargement du dashboard...

; + return

{t("dashboard.loading")}

; } if (!user) { return (
-

Session requise

-

Connexion necessaire

-

Ton dashboard est protege. Connecte-toi via Discord pour administrer tes bots.

+

{t("dashboard.sessionRequired")}

+

{t("dashboard.loginRequired")}

+

{t("dashboard.loginDescription")}

- Se connecter + {t("dashboard.loginCta")}
); @@ -175,12 +186,12 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) {
-

Tenant {user.tenantId}

+

{t("dashboard.tenant", { tenantId: user.tenantId })}

{user.username}

-

Role: {user.role}

+

{t("dashboard.role", { role: user.role })}

@@ -188,11 +199,11 @@ export function DashboardClient({ apiBaseUrl }: DashboardClientProps) {
-

Ajouter un bot

-

Le token est verifie cote API puis chiffre avant stockage en base.

+

{t("dashboard.addBot.title")}

+

{t("dashboard.addBot.description")}

-

Mes bots

+

{t("dashboard.bots.title")}

{bots.length === 0 ? ( -

Aucun bot pour ce tenant.

+

{t("dashboard.bots.empty")}

) : (
    {bots.map((bot) => (
  • {bot.displayName}

    -

    Discord ID: {bot.discordBotId}

    +

    {t("dashboard.bots.discordId", { discordBotId: bot.discordBotId })}

    {statusLabel[bot.status]}

    - {bot.lastError ?

    Derniere erreur: {bot.lastError}

    : null} + {bot.lastError ?

    {t("dashboard.bots.lastError", { message: bot.lastError })}

    : null}
  • diff --git a/apps/web/i18n/client.ts b/apps/web/i18n/client.ts new file mode 100644 index 0000000..954e200 --- /dev/null +++ b/apps/web/i18n/client.ts @@ -0,0 +1,5 @@ +import { useTranslations } from "next-intl"; + +export function useT() { + return useTranslations(); +} \ No newline at end of file diff --git a/apps/web/i18n/navigation.ts b/apps/web/i18n/navigation.ts new file mode 100644 index 0000000..91d9571 --- /dev/null +++ b/apps/web/i18n/navigation.ts @@ -0,0 +1,6 @@ +import { createNavigation } from "next-intl/navigation"; + +import { routing } from "./routing"; + +export const { Link, redirect, usePathname, useRouter, getPathname } = + createNavigation(routing); \ No newline at end of file diff --git a/apps/web/i18n/next-intl.d.ts b/apps/web/i18n/next-intl.d.ts new file mode 100644 index 0000000..b6f9695 --- /dev/null +++ b/apps/web/i18n/next-intl.d.ts @@ -0,0 +1,12 @@ +import "next-intl"; + +import enMessages from "../messages/en.json"; + +import { routing } from "./routing"; + +declare module "next-intl" { + interface AppConfig { + Locale: (typeof routing.locales)[number]; + Messages: typeof enMessages; + } +} \ No newline at end of file diff --git a/apps/web/i18n/request.ts b/apps/web/i18n/request.ts new file mode 100644 index 0000000..048fdbc --- /dev/null +++ b/apps/web/i18n/request.ts @@ -0,0 +1,50 @@ +import { hasLocale } from "next-intl"; +import { getRequestConfig } from "next-intl/server"; + +import { routing, type AppLocale } from "./routing"; + +type Messages = Record; + +const messageLoaders: Record Promise> = { + en: async () => (await import("../messages/en.json")).default as Messages, + fr: async () => (await import("../messages/fr.json")).default as Messages, +}; + +const isObject = (value: unknown): value is Record => { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +}; + +const mergeMessages = (base: Messages, override: Messages): Messages => { + const merged: Messages = { ...base }; + + for (const [key, value] of Object.entries(override)) { + const current = merged[key]; + + if (isObject(current) && isObject(value)) { + merged[key] = mergeMessages(current, value); + continue; + } + + merged[key] = value; + } + + return merged; +}; + +export default getRequestConfig(async ({ requestLocale }) => { + const requestedLocale = await requestLocale; + const locale = hasLocale(routing.locales, requestedLocale) + ? requestedLocale + : routing.defaultLocale; + + const defaultMessages = await messageLoaders[routing.defaultLocale](); + const localeMessages = await messageLoaders[locale](); + + return { + locale, + messages: + locale === routing.defaultLocale + ? defaultMessages + : mergeMessages(defaultMessages, localeMessages), + }; +}); \ No newline at end of file diff --git a/apps/web/i18n/routing.ts b/apps/web/i18n/routing.ts new file mode 100644 index 0000000..88696e2 --- /dev/null +++ b/apps/web/i18n/routing.ts @@ -0,0 +1,9 @@ +import { defineRouting } from "next-intl/routing"; + +export const routing = defineRouting({ + locales: ["en", "fr"], + defaultLocale: "en", + localePrefix: "always", +}); + +export type AppLocale = (typeof routing.locales)[number]; \ No newline at end of file diff --git a/apps/web/i18n/server.ts b/apps/web/i18n/server.ts new file mode 100644 index 0000000..a99b9f5 --- /dev/null +++ b/apps/web/i18n/server.ts @@ -0,0 +1,5 @@ +import { getTranslations } from "next-intl/server"; + +export async function getT() { + return getTranslations(); +} \ No newline at end of file diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json new file mode 100644 index 0000000..cdf0a52 --- /dev/null +++ b/apps/web/messages/en.json @@ -0,0 +1,135 @@ +{ + "metadata": { + "title": "Shadow - Discord bot hosting (EUR 2/month per bot)", + "description": "Multi-tenant SaaS platform to host and manage Discord bots with auto commands, components v2, multi-language support and dynamic variables." + }, + "localeSwitcher": { + "label": "Language", + "locales": { + "en": "English", + "fr": "Francais" + } + }, + "hero": { + "navLogin": "Sign in", + "title": "Shadow - Host your Discord bots (from EUR 2/month per bot)", + "description": "Host and manage your bots with auto commands (prefix and slash), components v2, automatic help, multi-language support and dynamic variables.", + "ctaStart": "Get started - EUR 2/month", + "ctaLogin": "Sign in", + "bots": { + "moderation": "Moderation", + "welcome": "Welcome", + "analytics": "Analytics" + }, + "status": { + "online": "Online", + "stopped": "Stopped" + }, + "card": { + "instanceMetrics": "Instances: 1 | CPU 35%", + "manage": "Manage", + "preview": "Real-time preview of your bot status" + } + }, + "features": { + "title": "Main features", + "subtitle": "Everything you need to run your bots in production.", + "items": { + "autoCommands": { + "title": "Automatic commands", + "description": "Prefix and slash commands are generated and synced automatically for each bot." + }, + "componentsV2": { + "title": "Components v2", + "description": "Full support for components v2 and reliable rich interactions." + }, + "autoHelp": { + "title": "Automatic help", + "description": "Help and command documentation generated automatically." + }, + "multiLanguage": { + "title": "Multi-language", + "description": "Translate messages and command labels, then enable multiple locales quickly." + }, + "dynamicVariables": { + "title": "Dynamic variables", + "description": "Dynamic templates with variables like bot_latency, guild_count and more." + }, + "hosting": { + "title": "Hosting and scalability", + "description": "Simple plans at EUR 2/month per bot with monitoring and scalability included." + } + } + }, + "howItWorks": { + "title": "How it works", + "subtitle": "Three simple steps to take control of your bots.", + "steps": { + "addBot": { + "title": "Add a bot", + "description": "Connect your Discord application with OAuth and register it in Shadow." + }, + "dashboard": { + "title": "Manage from dashboard", + "description": "Access commands, logs and settings from one centralized interface." + }, + "realtime": { + "title": "Control in real time", + "description": "Start, stop and scale your bots instantly based on demand." + } + } + }, + "footer": { + "tagline": "Discord bot hosting - plans from EUR 2/month per bot" + }, + "login": { + "eyebrow": "Shadow - Discord bot hosting", + "title": "Connect your Discord account", + "description": "Authenticate with OAuth2 to access your space and host your bots.", + "cta": "Continue with Discord" + }, + "dashboard": { + "loading": "Loading dashboard...", + "sessionRequired": "Session required", + "loginRequired": "Sign in required", + "loginDescription": "Your dashboard is protected. Sign in with Discord to manage your bots.", + "loginCta": "Sign in", + "tenant": "Tenant {tenantId}", + "role": "Role: {role}", + "logout": "Sign out", + "addBot": { + "title": "Add a bot", + "description": "The token is validated by the API and encrypted before it is stored.", + "tokenLabel": "Discord bot token", + "displayNameLabel": "Display name (optional)", + "submit": "Add bot", + "submitPending": "Validating..." + }, + "bots": { + "title": "My bots", + "refresh": "Refresh", + "empty": "No bot for this tenant.", + "discordId": "Discord ID: {discordBotId}", + "lastError": "Last error: {message}" + }, + "status": { + "stopped": "Stopped", + "starting": "Starting", + "running": "Online", + "stopping": "Stopping", + "error": "Error" + }, + "actions": { + "start": "Start", + "stop": "Stop", + "restart": "Restart" + }, + "errors": { + "fetchSession": "Unable to fetch user session", + "fetchBots": "Unable to fetch bot list", + "unexpected": "Unexpected error", + "addBot": "Unable to add bot", + "actionFailed": "Action failed: {action}" + } + } +} diff --git a/apps/web/messages/fr.json b/apps/web/messages/fr.json new file mode 100644 index 0000000..1c5dee7 --- /dev/null +++ b/apps/web/messages/fr.json @@ -0,0 +1,135 @@ +{ + "metadata": { + "title": "Shadow - Hebergement de bots Discord (a partir de 2 EUR/mois par bot)", + "description": "Plateforme SaaS multi-tenant pour heberger et gerer vos bots Discord avec commandes automatiques, components v2, multi-langue et variables dynamiques." + }, + "localeSwitcher": { + "label": "Langue", + "locales": { + "en": "English", + "fr": "Francais" + } + }, + "hero": { + "navLogin": "Se connecter", + "title": "Shadow - Hebergez vos bots Discord (a partir de 2 EUR/mois par bot)", + "description": "Hebergez et gerez vos bots avec commandes automatiques (prefix et slash), components v2, aide automatique, support multi-langue et variables dynamiques.", + "ctaStart": "Commencer - 2 EUR/mois", + "ctaLogin": "Se connecter", + "bots": { + "moderation": "Moderation", + "welcome": "Bienvenue", + "analytics": "Analytics" + }, + "status": { + "online": "En ligne", + "stopped": "Arrete" + }, + "card": { + "instanceMetrics": "Instances: 1 | CPU 35%", + "manage": "Gerer", + "preview": "Apercu en temps reel du statut de vos bots" + } + }, + "features": { + "title": "Fonctionnalites principales", + "subtitle": "Tout ce quil faut pour piloter vos bots en production.", + "items": { + "autoCommands": { + "title": "Commandes automatiques", + "description": "Les commandes prefix et slash sont generees et synchronisees automatiquement pour chaque bot." + }, + "componentsV2": { + "title": "Components v2", + "description": "Support complet des components v2 et des interactions riches fiables." + }, + "autoHelp": { + "title": "Aide automatique", + "description": "Aide et documentation des commandes generees automatiquement." + }, + "multiLanguage": { + "title": "Multi-langue", + "description": "Traduisez les messages et les labels de commandes puis activez plusieurs langues facilement." + }, + "dynamicVariables": { + "title": "Variables dynamiques", + "description": "Templates dynamiques avec des variables comme bot_latency, guild_count et plus." + }, + "hosting": { + "title": "Hebergement et scalabilite", + "description": "Plans simples a 2 EUR/mois par bot avec monitoring et scalabilite inclus." + } + } + }, + "howItWorks": { + "title": "Comment ca marche", + "subtitle": "Trois etapes simples pour prendre le controle de vos bots.", + "steps": { + "addBot": { + "title": "Ajouter un bot", + "description": "Connectez votre application Discord via OAuth et enregistrez-la dans Shadow." + }, + "dashboard": { + "title": "Gerer depuis le dashboard", + "description": "Accedez aux commandes, aux logs et aux parametres depuis une interface centralisee." + }, + "realtime": { + "title": "Controler en temps reel", + "description": "Demarrez, arretez et scalez vos bots instantanement selon la demande." + } + } + }, + "footer": { + "tagline": "Hebergement de bots Discord - plans des 2 EUR/mois par bot" + }, + "login": { + "eyebrow": "Shadow - Hebergement de bots Discord", + "title": "Connectez votre compte Discord", + "description": "Authentifiez-vous via OAuth2 pour acceder a votre espace et heberger vos bots.", + "cta": "Continuer avec Discord" + }, + "dashboard": { + "loading": "Chargement du dashboard...", + "sessionRequired": "Session requise", + "loginRequired": "Connexion necessaire", + "loginDescription": "Votre dashboard est protege. Connectez-vous via Discord pour administrer vos bots.", + "loginCta": "Se connecter", + "tenant": "Tenant {tenantId}", + "role": "Role: {role}", + "logout": "Se deconnecter", + "addBot": { + "title": "Ajouter un bot", + "description": "Le token est verifie cote API puis chiffre avant stockage.", + "tokenLabel": "Token bot Discord", + "displayNameLabel": "Nom daffichage (optionnel)", + "submit": "Ajouter le bot", + "submitPending": "Validation en cours..." + }, + "bots": { + "title": "Mes bots", + "refresh": "Rafraichir", + "empty": "Aucun bot pour ce tenant.", + "discordId": "Discord ID: {discordBotId}", + "lastError": "Derniere erreur: {message}" + }, + "status": { + "stopped": "Arrete", + "starting": "Demarrage", + "running": "En ligne", + "stopping": "Arret", + "error": "Erreur" + }, + "actions": { + "start": "Demarrer", + "stop": "Arreter", + "restart": "Redemarrer" + }, + "errors": { + "fetchSession": "Impossible de recuperer la session utilisateur", + "fetchBots": "Impossible de recuperer la liste des bots", + "unexpected": "Erreur inattendue", + "addBot": "Ajout du bot impossible", + "actionFailed": "Action impossible: {action}" + } + } +} diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts new file mode 100644 index 0000000..090305d --- /dev/null +++ b/apps/web/middleware.ts @@ -0,0 +1,9 @@ +import createMiddleware from "next-intl/middleware"; + +import { routing } from "./i18n/routing"; + +export default createMiddleware(routing); + +export const config = { + matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"], +}; \ No newline at end of file diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 01761fd..6259226 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,7 +1,11 @@ +import createNextIntlPlugin from "next-intl/plugin"; + +const withNextIntl = createNextIntlPlugin("./i18n/request.ts"); + /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, output: "standalone", }; -export default nextConfig; +export default withNextIntl(nextConfig); diff --git a/apps/web/package.json b/apps/web/package.json index cee7fe7..39cdbbb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "next": "15.1.2", + "next-intl": "^4.9.1", "react": "19.0.0", "react-dom": "19.0.0" }, diff --git a/package-lock.json b/package-lock.json index ed005b5..45c7f35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,6 +68,7 @@ "version": "1.0.0", "dependencies": { "next": "15.1.2", + "next-intl": "^4.9.1", "react": "19.0.0", "react-dom": "19.0.0" }, @@ -681,6 +682,36 @@ "node": ">=18" } }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.2.tgz", + "integrity": "sha512-vPnriihkfK0lzoQGaXq+qXH23VsYyansRTkTgo2aTG0k1NjLFyZimFVdfj4C9JkSE5dm7CEngcQ5TTc1yAyBfQ==", + "license": "MIT" + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.4.tgz", + "integrity": "sha512-JVY39ROgLt+pIYngo6piyj4OVfZmXs/2FkC4wLS+ql1Eig/sGJKB7YwDO/5bkJFkfwaFAeIpgEiJc8hiYxNalw==", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.4" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.4.tgz", + "integrity": "sha512-8bSFZbrlvGX11ywMZxtgkPBt5Q8/etyts7j7j+GWpOVK1g43zwMIH3LZxk43HAtEP7L/jtZ+OZaMiFTOiBj9CA==", + "license": "MIT" + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.3.tgz", + "integrity": "sha512-pHUjWb9NuhnMs8+PxQdzBtZRFJHlGhrURGAbm6Ltwl82BFajeuiIR3jblSa7ia3r62rXe/0YtVpUG3xWr5bFCA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.2" + } + }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", @@ -1586,6 +1617,313 @@ "node": ">= 8" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", @@ -1641,6 +1979,204 @@ "npm": ">=7.0.0" } }, + "node_modules/@schummar/icu-type-parser": { + "version": "1.21.5", + "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", + "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", + "license": "MIT" + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.30", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.30.tgz", + "integrity": "sha512-VvpP+vq08HmGYewMWvrdsxh9s2lthz/808zXm8Yu5kaqeR8Yia2b0eYXleHQ3VAjoStUDk6LzTheBW9KXYQdMA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.30", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.30.tgz", + "integrity": "sha512-WiJA0hiZI3nwQAO6mu5RqigtWGDtth4Hiq6rbZxAaQyhIcqKIg5IoMRc1Y071lrNJn29eEDMC86Rq58xgUxlDg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.30", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.30.tgz", + "integrity": "sha512-YANuFUo48kIT6plJgCD0keae9HFXfjxsbvsgevqc0hr/07X/p7sAWTFOGYEc2SXcASaK7UvuQqzlbW8pr7R79g==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.30", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.30.tgz", + "integrity": "sha512-VndG8jaR4ugY6u+iVOT0Q+d2fZd7sLgjPgN8W/Le+3EbZKl+cRfFxV7Eoz4gfLqhmneZPdcIzf9T3LkgkmqNLg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.30", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.30.tgz", + "integrity": "sha512-1SYGs2l0Yyyi0pR/P/NKz/x0kqxkoiw+BXeJjLUdecSk/KasncWlJrc6hOvFSgKHOBrzgM5jwuluKtlT8dnrcA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.30", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.30.tgz", + "integrity": "sha512-TXREtiXeRhbfDFbmhnkIsXpKfzbfT73YkV2ZF6w0sfxgjC5zI2ZAbaCOq25qxvegofj2K93DtOpm9RLaBgqR2g==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.30", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.30.tgz", + "integrity": "sha512-DCR2YYeyd6DQE4OuDhImouuNcjXEiEdnn1Y0DyGteugPEDvVuvYk8Xddi+4o2SgWH6jiW8/I+3emZvbep1NC+g==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.30", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.30.tgz", + "integrity": "sha512-5Pizw3NgfOJ5BJOBK8TIRa59xFW2avESTOBDPTAYwZYa1JNDs+KMF9lUfjJiJLM5HiMs/wPheA9eiT0q9m2AoA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.30", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.30.tgz", + "integrity": "sha512-qyqydP/wyH8alcIP4a2hnGSjHLJjm9H7yDFup+CPy9oTahFgLLwnNcv5UHXqO2Qs3AIND+cls5f/Bb6hqpxdgA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.30", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.30.tgz", + "integrity": "sha512-CaQENgDHVGOg1mSF5sQVgvfFHG9kjMor2rkLMLeLOkfZYNj13ppnJ9+lfaBZLZUMMbnlGQnavCJb8PVBUOso7Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.30", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.30.tgz", + "integrity": "sha512-30VdLeGk6fugiUs/kUdJ/pAg7z/zpvVbR11RH60jZ0Z42WIeIniYx0rLEWN7h/pKJ3CopqsQ3RsogCAkRKiA2g==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.30", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.30.tgz", + "integrity": "sha512-4iObHPR+Q4oDY110EF5SF5eIaaVJNpMdG9C0q3Q92BsJ5y467uHz7sYQhP60WYlLFsLQ1el2YrIPUItUAQGOKg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -1656,6 +2192,15 @@ "tslib": "^2.8.0" } }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -2402,7 +2947,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -2942,12 +3486,37 @@ "node": ">=0.10.0" } }, + "node_modules/icu-minify": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.9.1.tgz", + "integrity": "sha512-6NkfF9GHHFouqnz+wuiLjCWQiyxoEyJ5liUv4Jxxo/8wyhV7MY0L0iTEGDAVEa4aAD58WqTxFMa20S5nyMjwNw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "^3.4.0" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/intl-messageformat": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.1.tgz", + "integrity": "sha512-1gAVEUt3wEPvTqML4Fsw9klZV5j0vszQxayP/fi6gUroAc8AUHiNaisBKLWxybL1AdWq1mP07YV1q8v4N92ilQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/fast-memoize": "3.1.2", + "@formatjs/icu-messageformat-parser": "3.5.4" + } + }, "node_modules/ioredis": { "version": "5.10.1", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", @@ -3021,7 +3590,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3031,7 +3599,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -3304,6 +3871,7 @@ "integrity": "sha512-nLJDV7peNy+0oHlmY2JZjzMfJ8Aj0/dd3jCwSZS8ZiO5nkQfcZRqDrRN3U5rJtqVTQneIOGZzb6LCNrk7trMCQ==", "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "15.1.2", "@swc/counter": "0.1.3", @@ -3353,12 +3921,104 @@ } } }, + "node_modules/next-intl": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.9.1.tgz", + "integrity": "sha512-N7ga0CjtYcdxNvaKNIi6eJ2mmatlHK5hp8rt0YO2Omoc1m0gean242/Ukdj6+gJNiReBVcYIjK0HZeNx7CV1ug==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.8.1", + "@parcel/watcher": "^2.4.1", + "@swc/core": "^1.15.2", + "icu-minify": "^4.9.1", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "^4.9.1", + "po-parser": "^2.1.1", + "use-intl": "^4.9.1" + }, + "peerDependencies": { + "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/next-intl-swc-plugin-extractor": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.9.1.tgz", + "integrity": "sha512-8whJJ6oxJz8JqkHarggmmuEDyXgC7nEnaPhZD91CJwEWW4xp0AST3Mw17YxvHyP2vAF3taWfFbs1maD+WWtz3w==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.30", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.30.tgz", + "integrity": "sha512-R8VQbQY1BZcbIF2p3gjlTCwAQzx1A194ugWfwld5y+WgVVWqVKm7eURGGOVbQVubgKWzidP2agomBbg96rZilQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.30", + "@swc/core-darwin-x64": "1.15.30", + "@swc/core-linux-arm-gnueabihf": "1.15.30", + "@swc/core-linux-arm64-gnu": "1.15.30", + "@swc/core-linux-arm64-musl": "1.15.30", + "@swc/core-linux-ppc64-gnu": "1.15.30", + "@swc/core-linux-s390x-gnu": "1.15.30", + "@swc/core-linux-x64-gnu": "1.15.30", + "@swc/core-linux-x64-musl": "1.15.30", + "@swc/core-win32-arm64-msvc": "1.15.30", + "@swc/core-win32-ia32-msvc": "1.15.30", + "@swc/core-win32-x64-msvc": "1.15.30" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/next-intl/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-gyp-build-optional-packages": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", @@ -3631,6 +4291,12 @@ "node": ">= 6" } }, + "node_modules/po-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", + "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -4710,6 +5376,27 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-intl": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.9.1.tgz", + "integrity": "sha512-iGVV/xFYlhe3btafRlL8RPLD2Jsuet4yqn9DR6LWWbMhULsJnXgLonDkzDmsAIBIwFtk02oJuX/Ox2vwHKF+UQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^3.1.0", + "@schummar/icu-type-parser": "1.21.5", + "icu-minify": "^4.9.1", + "intl-messageformat": "^11.1.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",