@lastbrain/app 2.0.18 → 2.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-shell/not-found.js +2 -2
- package/dist/components/LanguageSwitcher.d.ts +7 -0
- package/dist/components/LanguageSwitcher.d.ts.map +1 -0
- package/dist/components/LanguageSwitcher.js +62 -0
- package/dist/config/version.d.ts.map +1 -1
- package/dist/config/version.js +19 -21
- package/dist/i18n/LanguageProvider.d.ts +4 -0
- package/dist/i18n/LanguageProvider.d.ts.map +1 -0
- package/dist/i18n/LanguageProvider.js +6 -0
- package/dist/i18n/TranslationsScript.d.ts +8 -0
- package/dist/i18n/TranslationsScript.d.ts.map +1 -0
- package/dist/i18n/TranslationsScript.js +10 -0
- package/dist/i18n/cookies.d.ts +6 -0
- package/dist/i18n/cookies.d.ts.map +1 -0
- package/dist/i18n/cookies.js +24 -0
- package/dist/i18n/langHrefHelper.d.ts +36 -0
- package/dist/i18n/langHrefHelper.d.ts.map +1 -0
- package/dist/i18n/langHrefHelper.js +41 -0
- package/dist/i18n/server-helpers.d.ts +33 -0
- package/dist/i18n/server-helpers.d.ts.map +1 -0
- package/dist/i18n/server-helpers.js +39 -0
- package/dist/i18n/server-lang.d.ts +10 -0
- package/dist/i18n/server-lang.d.ts.map +1 -0
- package/dist/i18n/server-lang.js +42 -0
- package/dist/i18n/server.d.ts +10 -0
- package/dist/i18n/server.d.ts.map +1 -0
- package/dist/i18n/server.js +8 -0
- package/dist/i18n/types.d.ts +38 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +4 -0
- package/dist/i18n/useLangRouter.d.ts +12 -0
- package/dist/i18n/useLangRouter.d.ts.map +1 -0
- package/dist/i18n/useLangRouter.js +18 -0
- package/dist/i18n/useLink.d.ts +34 -0
- package/dist/i18n/useLink.d.ts.map +1 -0
- package/dist/i18n/useLink.js +58 -0
- package/dist/i18n/useModuleTranslation.d.ts +11 -0
- package/dist/i18n/useModuleTranslation.d.ts.map +1 -0
- package/dist/i18n/useModuleTranslation.js +23 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/layouts/AdminLayout.js +1 -1
- package/dist/layouts/AppProviders.d.ts +4 -1
- package/dist/layouts/AppProviders.d.ts.map +1 -1
- package/dist/layouts/AppProviders.js +18 -8
- package/dist/layouts/AuthLayout.js +1 -1
- package/dist/layouts/AuthLayoutWithSidebar.d.ts.map +1 -1
- package/dist/layouts/AuthLayoutWithSidebar.js +5 -3
- package/dist/layouts/RootLayout.d.ts +4 -1
- package/dist/layouts/RootLayout.d.ts.map +1 -1
- package/dist/layouts/RootLayout.js +2 -2
- package/dist/scripts/i18n-build.d.ts +7 -0
- package/dist/scripts/i18n-build.d.ts.map +1 -0
- package/dist/scripts/i18n-build.js +100 -0
- package/dist/scripts/init-app.js +12 -12
- package/dist/scripts/module-build.d.ts.map +1 -1
- package/dist/scripts/module-build.js +384 -81
- package/dist/styles.css +1 -1
- package/dist/templates/DefaultDoc.d.ts.map +1 -1
- package/dist/templates/DefaultDoc.js +90 -2
- package/dist/templates/middleware-i18n.example.d.ts +16 -0
- package/dist/templates/middleware-i18n.example.d.ts.map +1 -0
- package/dist/templates/middleware-i18n.example.js +72 -0
- package/package.json +14 -3
- package/src/app-shell/not-found.tsx +2 -2
- package/src/components/LanguageSwitcher.tsx +156 -0
- package/src/config/version.ts +19 -21
- package/src/i18n/LanguageProvider.tsx +7 -0
- package/src/i18n/README_LANG_HELPERS.md +187 -0
- package/src/i18n/TranslationsScript.tsx +17 -0
- package/src/i18n/cookies.ts +24 -0
- package/src/i18n/langHrefHelper.ts +51 -0
- package/src/i18n/server-helpers.ts +48 -0
- package/src/i18n/server-lang.ts +51 -0
- package/src/i18n/server.ts +13 -0
- package/src/i18n/types.ts +39 -0
- package/src/i18n/useLangRouter.ts +21 -0
- package/src/i18n/useLink.ts +60 -0
- package/src/i18n/useModuleTranslation.ts +27 -0
- package/src/index.ts +20 -0
- package/src/layouts/AdminLayout.tsx +1 -1
- package/src/layouts/AppProviders.tsx +35 -16
- package/src/layouts/AuthLayout.tsx +1 -1
- package/src/layouts/AuthLayoutWithSidebar.tsx +5 -3
- package/src/layouts/RootLayout.tsx +12 -5
- package/src/scripts/i18n-build.ts +122 -0
- package/src/scripts/init-app.ts +12 -12
- package/src/scripts/module-build.ts +476 -94
- package/src/templates/DefaultDoc.tsx +511 -1
- package/src/templates/middleware-i18n.example.ts +92 -0
- package/dist/app-shell/(admin)/dashboard/page.d.ts +0 -2
- package/dist/app-shell/(admin)/dashboard/page.d.ts.map +0 -1
- package/dist/app-shell/(admin)/dashboard/page.js +0 -5
- package/dist/app-shell/(admin)/page.d.ts +0 -2
- package/dist/app-shell/(admin)/page.d.ts.map +0 -1
- package/dist/app-shell/(admin)/page.js +0 -5
- package/dist/app-shell/(auth)/dashboard/page.d.ts +0 -2
- package/dist/app-shell/(auth)/dashboard/page.d.ts.map +0 -1
- package/dist/app-shell/(auth)/dashboard/page.js +0 -5
- package/dist/app-shell/(auth)/page.d.ts +0 -2
- package/dist/app-shell/(auth)/page.d.ts.map +0 -1
- package/dist/app-shell/(auth)/page.js +0 -5
- package/dist/components/TableStructure.d.ts +0 -8
- package/dist/components/TableStructure.d.ts.map +0 -1
- package/dist/components/TableStructure.js +0 -37
- package/dist/hooks/useNotificationsSimple.d.ts +0 -20
- package/dist/hooks/useNotificationsSimple.d.ts.map +0 -1
- package/dist/hooks/useNotificationsSimple.js +0 -37
- package/dist/module-build.d.ts +0 -2
- package/dist/module-build.d.ts.map +0 -1
- package/dist/module-build.js +0 -50
- package/dist/modules/index.d.ts +0 -3
- package/dist/modules/index.d.ts.map +0 -1
- package/dist/modules/index.js +0 -2
- package/dist/src/__tests__/module-registry.test.d.ts +0 -2
- package/dist/src/__tests__/module-registry.test.d.ts.map +0 -1
- package/dist/src/__tests__/module-registry.test.js +0 -53
- package/dist/src/app-shell/(admin)/layout.d.ts +0 -4
- package/dist/src/app-shell/(admin)/layout.d.ts.map +0 -1
- package/dist/src/app-shell/(admin)/layout.js +0 -5
- package/dist/src/app-shell/(auth)/layout.d.ts +0 -4
- package/dist/src/app-shell/(auth)/layout.d.ts.map +0 -1
- package/dist/src/app-shell/(auth)/layout.js +0 -5
- package/dist/src/app-shell/(public)/page.d.ts +0 -2
- package/dist/src/app-shell/(public)/page.d.ts.map +0 -1
- package/dist/src/app-shell/(public)/page.js +0 -5
- package/dist/src/app-shell/layout.d.ts +0 -3
- package/dist/src/app-shell/layout.d.ts.map +0 -1
- package/dist/src/app-shell/layout.js +0 -3
- package/dist/src/app-shell/not-found.d.ts +0 -2
- package/dist/src/app-shell/not-found.d.ts.map +0 -1
- package/dist/src/app-shell/not-found.js +0 -10
- package/dist/src/auth/authHelpers.d.ts +0 -7
- package/dist/src/auth/authHelpers.d.ts.map +0 -1
- package/dist/src/auth/authHelpers.js +0 -19
- package/dist/src/auth/useAuthSession.d.ts +0 -7
- package/dist/src/auth/useAuthSession.d.ts.map +0 -1
- package/dist/src/auth/useAuthSession.js +0 -49
- package/dist/src/cli.d.ts +0 -3
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -143
- package/dist/src/components/NotificationContainer.d.ts +0 -2
- package/dist/src/components/NotificationContainer.d.ts.map +0 -1
- package/dist/src/components/NotificationContainer.js +0 -8
- package/dist/src/hooks/useNotifications.d.ts +0 -30
- package/dist/src/hooks/useNotifications.d.ts.map +0 -1
- package/dist/src/hooks/useNotifications.js +0 -165
- package/dist/src/index.d.ts +0 -22
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -22
- package/dist/src/layouts/AdminLayout.d.ts +0 -4
- package/dist/src/layouts/AdminLayout.d.ts.map +0 -1
- package/dist/src/layouts/AdminLayout.js +0 -4
- package/dist/src/layouts/AdminLayoutWithSidebar.d.ts +0 -10
- package/dist/src/layouts/AdminLayoutWithSidebar.d.ts.map +0 -1
- package/dist/src/layouts/AdminLayoutWithSidebar.js +0 -62
- package/dist/src/layouts/AppProviders.d.ts +0 -27
- package/dist/src/layouts/AppProviders.d.ts.map +0 -1
- package/dist/src/layouts/AppProviders.js +0 -48
- package/dist/src/layouts/AuthLayout.d.ts +0 -4
- package/dist/src/layouts/AuthLayout.d.ts.map +0 -1
- package/dist/src/layouts/AuthLayout.js +0 -4
- package/dist/src/layouts/AuthLayoutWithSidebar.d.ts +0 -12
- package/dist/src/layouts/AuthLayoutWithSidebar.d.ts.map +0 -1
- package/dist/src/layouts/AuthLayoutWithSidebar.js +0 -60
- package/dist/src/layouts/PublicLayout.d.ts +0 -8
- package/dist/src/layouts/PublicLayout.d.ts.map +0 -1
- package/dist/src/layouts/PublicLayout.js +0 -6
- package/dist/src/layouts/PublicLayoutWithSidebar.d.ts +0 -9
- package/dist/src/layouts/PublicLayoutWithSidebar.d.ts.map +0 -1
- package/dist/src/layouts/PublicLayoutWithSidebar.js +0 -60
- package/dist/src/layouts/RootLayout.d.ts +0 -6
- package/dist/src/layouts/RootLayout.d.ts.map +0 -1
- package/dist/src/layouts/RootLayout.js +0 -9
- package/dist/src/modules/module-loader.d.ts +0 -5
- package/dist/src/modules/module-loader.d.ts.map +0 -1
- package/dist/src/modules/module-loader.js +0 -10
- package/dist/src/scripts/db-init.d.ts +0 -2
- package/dist/src/scripts/db-init.d.ts.map +0 -1
- package/dist/src/scripts/db-init.js +0 -300
- package/dist/src/scripts/db-migrations-sync.d.ts +0 -2
- package/dist/src/scripts/db-migrations-sync.d.ts.map +0 -1
- package/dist/src/scripts/db-migrations-sync.js +0 -84
- package/dist/src/scripts/dev-sync.d.ts +0 -2
- package/dist/src/scripts/dev-sync.d.ts.map +0 -1
- package/dist/src/scripts/dev-sync.js +0 -194
- package/dist/src/scripts/init-app.d.ts +0 -12
- package/dist/src/scripts/init-app.d.ts.map +0 -1
- package/dist/src/scripts/init-app.js +0 -2175
- package/dist/src/scripts/module-add.d.ts +0 -2
- package/dist/src/scripts/module-add.d.ts.map +0 -1
- package/dist/src/scripts/module-add.js +0 -232
- package/dist/src/scripts/module-build.d.ts +0 -2
- package/dist/src/scripts/module-build.d.ts.map +0 -1
- package/dist/src/scripts/module-build.js +0 -1280
- package/dist/src/scripts/module-create.d.ts +0 -28
- package/dist/src/scripts/module-create.d.ts.map +0 -1
- package/dist/src/scripts/module-create.js +0 -1429
- package/dist/src/scripts/module-delete.d.ts +0 -6
- package/dist/src/scripts/module-delete.d.ts.map +0 -1
- package/dist/src/scripts/module-delete.js +0 -147
- package/dist/src/scripts/module-list.d.ts +0 -2
- package/dist/src/scripts/module-list.d.ts.map +0 -1
- package/dist/src/scripts/module-list.js +0 -61
- package/dist/src/scripts/module-remove.d.ts +0 -2
- package/dist/src/scripts/module-remove.d.ts.map +0 -1
- package/dist/src/scripts/module-remove.js +0 -311
- package/dist/src/scripts/readme-build.d.ts +0 -2
- package/dist/src/scripts/readme-build.d.ts.map +0 -1
- package/dist/src/scripts/readme-build.js +0 -39
- package/dist/src/scripts/script-runner.d.ts +0 -5
- package/dist/src/scripts/script-runner.d.ts.map +0 -1
- package/dist/src/scripts/script-runner.js +0 -25
- package/dist/src/templates/AuthGuidePage.d.ts +0 -2
- package/dist/src/templates/AuthGuidePage.d.ts.map +0 -1
- package/dist/src/templates/AuthGuidePage.js +0 -9
- package/dist/src/templates/DefaultDoc.d.ts +0 -2
- package/dist/src/templates/DefaultDoc.d.ts.map +0 -1
- package/dist/src/templates/DefaultDoc.js +0 -240
- package/dist/src/templates/DocPage.d.ts +0 -17
- package/dist/src/templates/DocPage.d.ts.map +0 -1
- package/dist/src/templates/DocPage.js +0 -193
- package/dist/src/templates/DocsPageWithModules.d.ts +0 -2
- package/dist/src/templates/DocsPageWithModules.d.ts.map +0 -1
- package/dist/src/templates/DocsPageWithModules.js +0 -8
- package/dist/src/templates/MigrationsGuidePage.d.ts +0 -2
- package/dist/src/templates/MigrationsGuidePage.d.ts.map +0 -1
- package/dist/src/templates/MigrationsGuidePage.js +0 -11
- package/dist/src/templates/ModuleGuidePage.d.ts +0 -2
- package/dist/src/templates/ModuleGuidePage.d.ts.map +0 -1
- package/dist/src/templates/ModuleGuidePage.js +0 -14
- package/dist/src/templates/SimpleDocPage.d.ts +0 -2
- package/dist/src/templates/SimpleDocPage.d.ts.map +0 -1
- package/dist/src/templates/SimpleDocPage.js +0 -28
- package/dist/src/templates/SimpleHomePage.d.ts +0 -6
- package/dist/src/templates/SimpleHomePage.d.ts.map +0 -1
- package/dist/src/templates/SimpleHomePage.js +0 -7
- package/dist/src/types/menu.d.ts +0 -23
- package/dist/src/types/menu.d.ts.map +0 -1
- package/dist/src/types/menu.js +0 -1
- package/dist/templates/HomePage.d.ts +0 -6
- package/dist/templates/HomePage.d.ts.map +0 -1
- package/dist/templates/HomePage.js +0 -6
- package/dist/templates/components/AppAside.d.ts +0 -6
- package/dist/templates/components/AppAside.d.ts.map +0 -1
- package/dist/templates/components/AppAside.js +0 -9
- package/dist/templates/layouts/admin-layout.d.ts +0 -4
- package/dist/templates/layouts/admin-layout.d.ts.map +0 -1
- package/dist/templates/layouts/admin-layout.js +0 -6
- package/dist/templates/layouts/auth-layout.d.ts +0 -4
- package/dist/templates/layouts/auth-layout.d.ts.map +0 -1
- package/dist/templates/layouts/auth-layout.js +0 -6
- package/dist/templates/migrations/20201010100000_init.sql +0 -123
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { cookies } from "next/headers";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export type Language = "fr" | "en";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Récupère la langue depuis les cookies côté serveur
|
|
9
|
+
*/
|
|
10
|
+
export async function getServerLanguage(): Promise<Language> {
|
|
11
|
+
const cookieStore = await cookies();
|
|
12
|
+
const lang = cookieStore.get("NEXT_LOCALE")?.value as Language | undefined;
|
|
13
|
+
return lang || "fr";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Charge les traductions pour une langue donnée côté serveur
|
|
18
|
+
*/
|
|
19
|
+
export async function loadTranslations(
|
|
20
|
+
lang: Language
|
|
21
|
+
): Promise<Record<string, string>> {
|
|
22
|
+
try {
|
|
23
|
+
// Charger depuis l'app final (source unique = concaténation de tous les modules)
|
|
24
|
+
// En développement et production, chercher dans apps/recipe/i18n (ou apps/lastbrain/i18n)
|
|
25
|
+
const appPaths = [
|
|
26
|
+
path.join(process.cwd(), "apps", "recipe", "i18n", `${lang}.json`),
|
|
27
|
+
path.join(process.cwd(), "apps", "lastbrain", "i18n", `${lang}.json`),
|
|
28
|
+
path.join(process.cwd(), "i18n", `${lang}.json`),
|
|
29
|
+
path.join("/app", "i18n", `${lang}.json`),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
let translationPath = "";
|
|
33
|
+
for (const appPath of appPaths) {
|
|
34
|
+
if (fs.existsSync(appPath)) {
|
|
35
|
+
translationPath = appPath;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(translationPath)) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const content = fs.readFileSync(translationPath, "utf-8");
|
|
45
|
+
const parsed = JSON.parse(content);
|
|
46
|
+
|
|
47
|
+
return parsed;
|
|
48
|
+
} catch (_error) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-only exports for i18n
|
|
3
|
+
*
|
|
4
|
+
* Import from "@lastbrain/app/i18n/server-lang" in Server Components
|
|
5
|
+
* Ne pas importer depuis "@lastbrain/app" pour éviter les erreurs côté client
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { getServerLanguage, loadTranslations } from "./server-lang";
|
|
9
|
+
export {
|
|
10
|
+
createServerTranslator,
|
|
11
|
+
createNamespacedTranslator,
|
|
12
|
+
} from "./server-helpers";
|
|
13
|
+
export type { Language } from "./LanguageProvider";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types pour le système i18n
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type Language = "fr" | "en";
|
|
6
|
+
|
|
7
|
+
export type TranslationKey = string;
|
|
8
|
+
|
|
9
|
+
export interface Translations {
|
|
10
|
+
[key: string]: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface I18nConfig {
|
|
14
|
+
defaultLocale: Language;
|
|
15
|
+
locales: Language[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Type pour les paramètres de page avec langue
|
|
20
|
+
*/
|
|
21
|
+
export interface LangParams {
|
|
22
|
+
params: Promise<{ lang: Language }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Type pour les paramètres de page avec langue et ID
|
|
27
|
+
*/
|
|
28
|
+
export interface LangIdParams {
|
|
29
|
+
params: Promise<{ lang: Language; id: string }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Helper type pour extraire les clés de traduction d'un module
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* type AuthTranslations = ModuleTranslations<"module-auth">;
|
|
37
|
+
* // "module-auth.welcome" | "module-auth.signin" | ...
|
|
38
|
+
*/
|
|
39
|
+
export type ModuleTranslations<T extends string> = `${T}.${string}`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useLocalizedRouter } from "@lastbrain/core";
|
|
4
|
+
import { useLink } from "./useLink";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wrapper autour de router.push/replace avec injection automatique du préfixe [lang].
|
|
8
|
+
*/
|
|
9
|
+
export function useLangRouter() {
|
|
10
|
+
const router = useLocalizedRouter();
|
|
11
|
+
const link = useLink();
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
push: (href: string) => router.push(link(href)),
|
|
15
|
+
replace: (href: string) => router.replace(link(href)),
|
|
16
|
+
prefetch: (href: string) => router.prefetch(link(href)),
|
|
17
|
+
back: () => router.back(),
|
|
18
|
+
forward: () => router.forward(),
|
|
19
|
+
refresh: () => router.refresh(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useLanguage } from "./LanguageProvider";
|
|
4
|
+
import type { Language } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper pour transformer un URL avec le paramètre [lang]
|
|
8
|
+
* Utile pour les transformations statiques avant le rendu React
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const href = langHref("/settings", "fr"); // "/fr/settings"
|
|
13
|
+
* const href = langHref("/api/users", "en"); // "/api/users" (API non modifiée)
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function langHref(href: string, lang: Language): string {
|
|
17
|
+
const normalizedHref = String(href);
|
|
18
|
+
const isAbsolute = normalizedHref.startsWith("http");
|
|
19
|
+
const hasLangPrefix = normalizedHref.startsWith(`/${lang}/`);
|
|
20
|
+
|
|
21
|
+
if (isAbsolute) {
|
|
22
|
+
return normalizedHref;
|
|
23
|
+
} else if (hasLangPrefix) {
|
|
24
|
+
return normalizedHref;
|
|
25
|
+
} else if (normalizedHref === "/") {
|
|
26
|
+
return `/${lang}`;
|
|
27
|
+
} else if (normalizedHref.startsWith("/api/")) {
|
|
28
|
+
return normalizedHref;
|
|
29
|
+
} else {
|
|
30
|
+
return `/${lang}${normalizedHref}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Hook pour générer des URLs avec le paramètre [lang] injecté automatiquement
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* "use client";
|
|
40
|
+
* import { useLink } from "@lastbrain/app";
|
|
41
|
+
*
|
|
42
|
+
* export function MyComponent() {
|
|
43
|
+
* const link = useLink();
|
|
44
|
+
*
|
|
45
|
+
* return (
|
|
46
|
+
* <>
|
|
47
|
+
* <img src={link("/api/storage/avatar.jpg")} />
|
|
48
|
+
* <a href={link("/recipes")}>Recettes</a>
|
|
49
|
+
* </>
|
|
50
|
+
* );
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function useLink() {
|
|
55
|
+
const { lang } = useLanguage();
|
|
56
|
+
|
|
57
|
+
return (href: string): string => {
|
|
58
|
+
return langHref(href, lang);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useLanguage } from "./LanguageProvider";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook pour utiliser les traductions d'un module spécifique
|
|
7
|
+
* Préfixe automatiquement les clés avec le nom du module
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const t = useModuleTranslation("auth");
|
|
11
|
+
* t("hello") // -> cherche "module-auth.hello"
|
|
12
|
+
* t("hello", "Hello") // -> cherche "module-auth.hello", fallback à "Hello"
|
|
13
|
+
*/
|
|
14
|
+
export function useModuleTranslation(moduleName: string) {
|
|
15
|
+
const { t: globalT } = useLanguage();
|
|
16
|
+
|
|
17
|
+
return (key: string, defaultValue?: string): string => {
|
|
18
|
+
const prefixedKey = `module-${moduleName}.${key}`;
|
|
19
|
+
const translation = globalT(prefixedKey);
|
|
20
|
+
|
|
21
|
+
// Si la traduction retourne exactement la clé (non trouvée), utiliser le fallback
|
|
22
|
+
if (translation === prefixedKey && defaultValue !== undefined) {
|
|
23
|
+
return defaultValue;
|
|
24
|
+
}
|
|
25
|
+
return translation;
|
|
26
|
+
};
|
|
27
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,26 @@ export { AdminLayout } from "./layouts/AdminLayout";
|
|
|
22
22
|
export { AdminLayoutWithSidebar } from "./layouts/AdminLayoutWithSidebar";
|
|
23
23
|
export { getModuleConfigs } from "./modules/module-loader";
|
|
24
24
|
|
|
25
|
+
// i18n
|
|
26
|
+
export {
|
|
27
|
+
LanguageProvider,
|
|
28
|
+
useLanguage,
|
|
29
|
+
type Language,
|
|
30
|
+
} from "./i18n/LanguageProvider";
|
|
31
|
+
export { useLink, langHref } from "./i18n/useLink";
|
|
32
|
+
export { useLangRouter } from "./i18n/useLangRouter";
|
|
33
|
+
export { useModuleTranslation } from "@lastbrain/core";
|
|
34
|
+
export { transformLangHrefs, transformLangHref } from "./i18n/langHrefHelper";
|
|
35
|
+
export { LanguageSwitcher } from "./components/LanguageSwitcher";
|
|
36
|
+
export type {
|
|
37
|
+
TranslationKey,
|
|
38
|
+
Translations,
|
|
39
|
+
I18nConfig,
|
|
40
|
+
LangParams,
|
|
41
|
+
LangIdParams,
|
|
42
|
+
ModuleTranslations,
|
|
43
|
+
} from "./i18n/types";
|
|
44
|
+
|
|
25
45
|
// Components
|
|
26
46
|
export { AppAside } from "@lastbrain/ui";
|
|
27
47
|
export type { AppAsideMenuItem, AppAsideMenuConfig } from "@lastbrain/ui";
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { createContext, useContext, useMemo } from "react";
|
|
4
4
|
import { getModuleConfigs } from "../modules/module-loader";
|
|
5
|
-
import { ToastProvider } from "@lastbrain/ui";
|
|
5
|
+
import { ToastProvider, AppLinkProvider } from "@lastbrain/ui";
|
|
6
6
|
import { RealtimeProvider } from "@lastbrain/core";
|
|
7
7
|
import { useAuthSession } from "../auth/useAuthSession";
|
|
8
8
|
import { useNotifications as useNotificationsHook } from "../hooks/useNotifications";
|
|
9
|
+
import { LanguageProvider, type Language } from "../i18n/LanguageProvider";
|
|
9
10
|
import type { User } from "@supabase/supabase-js";
|
|
10
11
|
import type { ModuleRealtimeConfig } from "@lastbrain/core";
|
|
11
12
|
import type { NotificationsData } from "../hooks/useNotifications";
|
|
@@ -62,9 +63,13 @@ export function useAuth() {
|
|
|
62
63
|
export function AppProviders({
|
|
63
64
|
children,
|
|
64
65
|
realtimeConfig = [],
|
|
66
|
+
lang = "fr",
|
|
67
|
+
translations = {},
|
|
65
68
|
}: {
|
|
66
69
|
children: React.ReactNode;
|
|
67
70
|
realtimeConfig?: ModuleRealtimeConfig[];
|
|
71
|
+
lang?: Language;
|
|
72
|
+
translations?: Record<string, string>;
|
|
68
73
|
}) {
|
|
69
74
|
const modules = useMemo(() => getModuleConfigs(), []);
|
|
70
75
|
const { user, loading: authLoading, isSuperAdmin } = useAuthSession();
|
|
@@ -75,22 +80,36 @@ export function AppProviders({
|
|
|
75
80
|
const authValue = useMemo(
|
|
76
81
|
() => ({ user, loading: authLoading, isSuperAdmin }),
|
|
77
82
|
[user, authLoading, isSuperAdmin]
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Mémoriser les traductions pour éviter les changements
|
|
86
|
+
const memoizedTranslations = useMemo(() => {
|
|
87
|
+
// Si des traductions non-vides sont fournies, les utiliser
|
|
88
|
+
if (Object.keys(translations).length > 0) {
|
|
89
|
+
return translations;
|
|
90
|
+
}
|
|
91
|
+
// Sinon, tenter de récupérer celles du script injecté côté client
|
|
92
|
+
if (typeof window !== "undefined" && (window as any).__TRANSLATIONS__) {
|
|
93
|
+
return (window as any).__TRANSLATIONS__ as Record<string, string>;
|
|
94
|
+
}
|
|
95
|
+
// Ne retourner un objet vide que si vraiment rien n'est disponible
|
|
96
|
+
return {};
|
|
97
|
+
}, [translations]);
|
|
83
98
|
|
|
84
99
|
return (
|
|
85
|
-
<
|
|
86
|
-
<
|
|
87
|
-
<
|
|
88
|
-
<
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
<LanguageProvider initialLang={lang} translations={memoizedTranslations}>
|
|
101
|
+
<AppLinkProvider lang={lang || "fr"}>
|
|
102
|
+
<AuthContext.Provider value={authValue}>
|
|
103
|
+
<ModuleContext.Provider value={modules}>
|
|
104
|
+
<NotificationContext.Provider value={notificationsData}>
|
|
105
|
+
<RealtimeProvider userId={user?.id} config={realtimeConfig}>
|
|
106
|
+
{children}
|
|
107
|
+
<ToastProvider placement="bottom-right" toastOffset={5} />
|
|
108
|
+
</RealtimeProvider>
|
|
109
|
+
</NotificationContext.Provider>
|
|
110
|
+
</ModuleContext.Provider>
|
|
111
|
+
</AuthContext.Provider>
|
|
112
|
+
</AppLinkProvider>
|
|
113
|
+
</LanguageProvider>
|
|
95
114
|
);
|
|
96
115
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type MenuItem,
|
|
9
9
|
} from "@lastbrain/ui";
|
|
10
10
|
import { useAuthSession } from "../auth/useAuthSession";
|
|
11
|
+
import { usePathname } from "next/navigation";
|
|
11
12
|
import type { MenuIgnored, MenuCustom } from "../types/menu";
|
|
12
13
|
import { useEffect, useState } from "react";
|
|
13
14
|
|
|
@@ -29,6 +30,7 @@ export function AuthLayoutWithSidebar({
|
|
|
29
30
|
menuCustom,
|
|
30
31
|
}: AuthLayoutWithSidebarProps) {
|
|
31
32
|
const { isSuperAdmin, loading, user } = useAuthSession();
|
|
33
|
+
const pathname = usePathname();
|
|
32
34
|
const [isCollapsed, setIsCollapsed] = useState(() => {
|
|
33
35
|
if (typeof window !== "undefined") {
|
|
34
36
|
const savedState = localStorage.getItem("aside-collapsed");
|
|
@@ -77,8 +79,8 @@ export function AuthLayoutWithSidebar({
|
|
|
77
79
|
}
|
|
78
80
|
}, []);
|
|
79
81
|
|
|
80
|
-
//
|
|
81
|
-
const
|
|
82
|
+
// Détecter si on est dans la section auth pour le skeleton (avec support [lang])
|
|
83
|
+
const isAuthSection = pathname.includes("/auth");
|
|
82
84
|
|
|
83
85
|
// Créer un menuConfig sûr avec des valeurs par défaut
|
|
84
86
|
const safeMenuConfig = {
|
|
@@ -104,7 +106,7 @@ export function AuthLayoutWithSidebar({
|
|
|
104
106
|
{shouldShowSkeleton ? (
|
|
105
107
|
<AppAsideSkeleton
|
|
106
108
|
className={className}
|
|
107
|
-
isAdminSection={
|
|
109
|
+
isAdminSection={isAuthSection}
|
|
108
110
|
/>
|
|
109
111
|
) : (
|
|
110
112
|
<AppAside
|
|
@@ -3,18 +3,23 @@
|
|
|
3
3
|
import { ThemeProvider } from "next-themes";
|
|
4
4
|
import { AppProviders } from "./AppProviders";
|
|
5
5
|
import type { ModuleRealtimeConfig } from "@lastbrain/core";
|
|
6
|
+
import type { Language } from "../i18n/LanguageProvider";
|
|
6
7
|
|
|
7
8
|
// Note: L'app Next.js doit importer son propre globals.css dans son layout
|
|
8
9
|
// Note: La configuration realtime doit être fournie par l'app qui utilise ce layout
|
|
9
10
|
export function RootLayout({
|
|
10
11
|
children,
|
|
11
12
|
realtimeConfig = [],
|
|
13
|
+
lang = "fr",
|
|
14
|
+
translations = {},
|
|
12
15
|
}: {
|
|
13
16
|
children: React.ReactNode;
|
|
14
17
|
realtimeConfig?: ModuleRealtimeConfig[];
|
|
18
|
+
lang?: Language;
|
|
19
|
+
translations?: Record<string, string>;
|
|
15
20
|
}) {
|
|
16
21
|
return (
|
|
17
|
-
<html lang=
|
|
22
|
+
<html lang={lang} suppressHydrationWarning>
|
|
18
23
|
<body className="min-h-screen max-w-screen">
|
|
19
24
|
<ThemeProvider
|
|
20
25
|
attribute="class"
|
|
@@ -22,10 +27,12 @@ export function RootLayout({
|
|
|
22
27
|
enableSystem={false}
|
|
23
28
|
storageKey="lastbrain-theme"
|
|
24
29
|
>
|
|
25
|
-
<AppProviders
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
<AppProviders
|
|
31
|
+
realtimeConfig={realtimeConfig}
|
|
32
|
+
lang={lang}
|
|
33
|
+
translations={translations}
|
|
34
|
+
>
|
|
35
|
+
<div className=" min-h-screen pt-12">{children}</div>
|
|
29
36
|
</AppProviders>
|
|
30
37
|
</ThemeProvider>
|
|
31
38
|
</body>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Script pour collecter tous les fichiers i18n des modules et les concaténer
|
|
4
|
+
* dans le dossier i18n de l'application
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
|
|
11
|
+
const monorepoRoot = projectRoot.includes("/apps/")
|
|
12
|
+
? path.resolve(projectRoot, "..", "..")
|
|
13
|
+
: projectRoot;
|
|
14
|
+
|
|
15
|
+
const packagesDir = path.join(monorepoRoot, "packages");
|
|
16
|
+
const appI18nDir = path.join(projectRoot, "i18n");
|
|
17
|
+
|
|
18
|
+
interface TranslationMap {
|
|
19
|
+
[key: string]: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Collecte tous les fichiers i18n des modules
|
|
24
|
+
*/
|
|
25
|
+
async function collectTranslations(): Promise<TranslationMap> {
|
|
26
|
+
const translations: TranslationMap = {
|
|
27
|
+
fr: {},
|
|
28
|
+
en: {},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Importer glob dynamiquement
|
|
33
|
+
const { glob } = await import("glob");
|
|
34
|
+
|
|
35
|
+
// Rechercher tous les fichiers i18n dans packages/**/src/i18n/*.json
|
|
36
|
+
const pattern = path.join(packagesDir, "**/src/i18n/*.json");
|
|
37
|
+
const files = await glob(pattern, {
|
|
38
|
+
ignore: ["**/node_modules/**", "**/dist/**"],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
console.log(`\n🔍 Found ${files.length} i18n files\n`);
|
|
42
|
+
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
const fileName = path.basename(file, ".json");
|
|
45
|
+
const lang = fileName; // fr.json -> fr, en.json -> en
|
|
46
|
+
|
|
47
|
+
if (!translations[lang]) {
|
|
48
|
+
translations[lang] = {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const content = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
53
|
+
const moduleName = extractModuleName(file);
|
|
54
|
+
|
|
55
|
+
console.log(` ✓ ${moduleName} (${lang})`);
|
|
56
|
+
|
|
57
|
+
// Préfixer les clés avec le nom du module pour éviter les collisions
|
|
58
|
+
for (const [key, value] of Object.entries(content)) {
|
|
59
|
+
const prefixedKey = `${moduleName}.${key}`;
|
|
60
|
+
translations[lang][prefixedKey] = value as string;
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.warn(` ⚠️ Error reading ${file}:`, error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("❌ Error collecting translations:", error);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return translations;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extrait le nom du module depuis le chemin du fichier
|
|
75
|
+
*/
|
|
76
|
+
function extractModuleName(filePath: string): string {
|
|
77
|
+
// packages/module-auth/src/i18n/fr.json -> module-auth
|
|
78
|
+
const parts = filePath.split(path.sep);
|
|
79
|
+
const packagesIndex = parts.indexOf("packages");
|
|
80
|
+
if (packagesIndex !== -1 && packagesIndex < parts.length - 1) {
|
|
81
|
+
return parts[packagesIndex + 1];
|
|
82
|
+
}
|
|
83
|
+
return "unknown";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Écrit les traductions concaténées dans le dossier i18n de l'app
|
|
88
|
+
*/
|
|
89
|
+
function writeTranslations(translations: TranslationMap) {
|
|
90
|
+
// Créer le dossier i18n s'il n'existe pas
|
|
91
|
+
if (!fs.existsSync(appI18nDir)) {
|
|
92
|
+
fs.mkdirSync(appI18nDir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const [lang, content] of Object.entries(translations)) {
|
|
96
|
+
const filePath = path.join(appI18nDir, `${lang}.json`);
|
|
97
|
+
fs.writeFileSync(filePath, JSON.stringify(content, null, 2), "utf-8");
|
|
98
|
+
const keyCount = Object.keys(content).length;
|
|
99
|
+
console.log(`\n✅ Generated ${filePath} (${keyCount} keys)`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Point d'entrée principal
|
|
105
|
+
*/
|
|
106
|
+
async function buildI18n() {
|
|
107
|
+
console.log("🌍 Building i18n files...\n");
|
|
108
|
+
console.log(`📁 Project root: ${projectRoot}`);
|
|
109
|
+
console.log(`📦 Packages dir: ${packagesDir}`);
|
|
110
|
+
console.log(`🎯 App i18n dir: ${appI18nDir}\n`);
|
|
111
|
+
|
|
112
|
+
const translations = await collectTranslations();
|
|
113
|
+
writeTranslations(translations);
|
|
114
|
+
|
|
115
|
+
console.log("\n✨ i18n build complete!\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Exécuter
|
|
119
|
+
buildI18n().catch((error) => {
|
|
120
|
+
console.error("\n❌ Error:", error);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
package/src/scripts/init-app.ts
CHANGED
|
@@ -504,38 +504,36 @@ async function createNextStructure(
|
|
|
504
504
|
let layoutContent = "";
|
|
505
505
|
|
|
506
506
|
if (useHeroUI) {
|
|
507
|
-
// Layout avec HeroUI - Server Component
|
|
507
|
+
// Layout avec HeroUI - Server Component (juste HTML, providers gérés par [lang]/layout.tsx)
|
|
508
508
|
layoutContent = `// GENERATED BY LASTBRAIN APP-SHELL
|
|
509
509
|
// Server Component pour permettre le SSR des pages enfants
|
|
510
510
|
|
|
511
511
|
import "../styles/globals.css";
|
|
512
|
-
import { ClientLayout } from "../components/ClientLayout";
|
|
513
512
|
import type { PropsWithChildren } from "react";
|
|
514
513
|
|
|
515
514
|
export default function RootLayout({ children }: PropsWithChildren<{}>) {
|
|
516
515
|
return (
|
|
517
516
|
<html lang="fr" suppressHydrationWarning>
|
|
518
517
|
<body className="min-h-screen">
|
|
519
|
-
|
|
518
|
+
{children}
|
|
520
519
|
</body>
|
|
521
520
|
</html>
|
|
522
521
|
);
|
|
523
522
|
}
|
|
524
523
|
`;
|
|
525
524
|
} else {
|
|
526
|
-
// Layout Tailwind CSS uniquement - Server Component
|
|
525
|
+
// Layout Tailwind CSS uniquement - Server Component (juste HTML, providers gérés par [lang]/layout.tsx)
|
|
527
526
|
layoutContent = `// GENERATED BY LASTBRAIN APP-SHELL
|
|
528
527
|
// Server Component pour permettre le SSR des pages enfants
|
|
529
528
|
|
|
530
529
|
import "../styles/globals.css";
|
|
531
|
-
import { ClientLayout } from "../components/ClientLayout";
|
|
532
530
|
import type { PropsWithChildren } from "react";
|
|
533
531
|
|
|
534
532
|
export default function RootLayout({ children }: PropsWithChildren<{}>) {
|
|
535
533
|
return (
|
|
536
534
|
<html lang="fr" suppressHydrationWarning>
|
|
537
535
|
<body className="min-h-screen">
|
|
538
|
-
|
|
536
|
+
{children}
|
|
539
537
|
</body>
|
|
540
538
|
</html>
|
|
541
539
|
);
|
|
@@ -563,7 +561,7 @@ export default function RootLayout({ children }: PropsWithChildren<{}>) {
|
|
|
563
561
|
}
|
|
564
562
|
|
|
565
563
|
// Créer la page d'accueil publique (racine)
|
|
566
|
-
const homePagePath = path.join(appDir, "page.tsx");
|
|
564
|
+
const homePagePath = path.join(appDir, "[lang]", "page.tsx");
|
|
567
565
|
if (!fs.existsSync(homePagePath) || force) {
|
|
568
566
|
const homePageContent = `// GENERATED BY LASTBRAIN APP-SHELL
|
|
569
567
|
|
|
@@ -589,10 +587,10 @@ export default function RootPage() {
|
|
|
589
587
|
if (!fs.existsSync(notFoundPath) || force) {
|
|
590
588
|
const notFoundContent = `"use client";
|
|
591
589
|
import { Button } from "@lastbrain/ui";
|
|
592
|
-
import {
|
|
590
|
+
import { useLocalizedRouter } from "@lastbrain/core";
|
|
593
591
|
|
|
594
592
|
export default function NotFound() {
|
|
595
|
-
const router =
|
|
593
|
+
const router = useLocalizedRouter();
|
|
596
594
|
return (
|
|
597
595
|
<div className="flex min-h-screen items-center justify-center bg-background">
|
|
598
596
|
<div className="mx-auto max-w-md text-center">
|
|
@@ -663,7 +661,7 @@ import { AppHeader } from "./AppHeader";
|
|
|
663
661
|
import type { ReactNode } from "react";
|
|
664
662
|
|
|
665
663
|
export function ClientLayout({ children }: { children: ReactNode }) {
|
|
666
|
-
const router =
|
|
664
|
+
const router = useLocalizedRouter();
|
|
667
665
|
|
|
668
666
|
return (
|
|
669
667
|
<HeroUIProvider navigate={router.push}>
|
|
@@ -863,7 +861,7 @@ import { Header, type MenuItem } from "@lastbrain/ui";
|
|
|
863
861
|
import { menuConfig } from "../config/menu";
|
|
864
862
|
import { supabaseBrowserClient } from "@lastbrain/core";
|
|
865
863
|
import { useRouter } from "next/navigation";
|
|
866
|
-
import { useAuthSession, useNotificationsContext } from "@lastbrain/app";
|
|
864
|
+
import { useAuthSession, useNotificationsContext,LanguageSwitcher } from "@lastbrain/app";
|
|
867
865
|
import { useState, useEffect } from "react";
|
|
868
866
|
|
|
869
867
|
interface MenuIgnored {
|
|
@@ -872,7 +870,7 @@ interface MenuIgnored {
|
|
|
872
870
|
}
|
|
873
871
|
|
|
874
872
|
export function AppHeader() {
|
|
875
|
-
const router =
|
|
873
|
+
const router = useLocalizedRouter();
|
|
876
874
|
const { user, isSuperAdmin } = useAuthSession();
|
|
877
875
|
const { data, loading, markAsRead, markAllAsRead, deleteNotification } =
|
|
878
876
|
useNotificationsContext();
|
|
@@ -934,6 +932,8 @@ export function AppHeader() {
|
|
|
934
932
|
{...(menuIgnored ? { menuIgnored } : {})}
|
|
935
933
|
{...(menuCustom ? { menuCustom } : {})}
|
|
936
934
|
{...{ isLoadingMenus: isLoading }}
|
|
935
|
+
languageSwitcher={<LanguageSwitcher variant="minimal" />}
|
|
936
|
+
|
|
937
937
|
/>
|
|
938
938
|
);
|
|
939
939
|
}
|