@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.
Files changed (254) hide show
  1. package/dist/app-shell/not-found.js +2 -2
  2. package/dist/components/LanguageSwitcher.d.ts +7 -0
  3. package/dist/components/LanguageSwitcher.d.ts.map +1 -0
  4. package/dist/components/LanguageSwitcher.js +62 -0
  5. package/dist/config/version.d.ts.map +1 -1
  6. package/dist/config/version.js +19 -21
  7. package/dist/i18n/LanguageProvider.d.ts +4 -0
  8. package/dist/i18n/LanguageProvider.d.ts.map +1 -0
  9. package/dist/i18n/LanguageProvider.js +6 -0
  10. package/dist/i18n/TranslationsScript.d.ts +8 -0
  11. package/dist/i18n/TranslationsScript.d.ts.map +1 -0
  12. package/dist/i18n/TranslationsScript.js +10 -0
  13. package/dist/i18n/cookies.d.ts +6 -0
  14. package/dist/i18n/cookies.d.ts.map +1 -0
  15. package/dist/i18n/cookies.js +24 -0
  16. package/dist/i18n/langHrefHelper.d.ts +36 -0
  17. package/dist/i18n/langHrefHelper.d.ts.map +1 -0
  18. package/dist/i18n/langHrefHelper.js +41 -0
  19. package/dist/i18n/server-helpers.d.ts +33 -0
  20. package/dist/i18n/server-helpers.d.ts.map +1 -0
  21. package/dist/i18n/server-helpers.js +39 -0
  22. package/dist/i18n/server-lang.d.ts +10 -0
  23. package/dist/i18n/server-lang.d.ts.map +1 -0
  24. package/dist/i18n/server-lang.js +42 -0
  25. package/dist/i18n/server.d.ts +10 -0
  26. package/dist/i18n/server.d.ts.map +1 -0
  27. package/dist/i18n/server.js +8 -0
  28. package/dist/i18n/types.d.ts +38 -0
  29. package/dist/i18n/types.d.ts.map +1 -0
  30. package/dist/i18n/types.js +4 -0
  31. package/dist/i18n/useLangRouter.d.ts +12 -0
  32. package/dist/i18n/useLangRouter.d.ts.map +1 -0
  33. package/dist/i18n/useLangRouter.js +18 -0
  34. package/dist/i18n/useLink.d.ts +34 -0
  35. package/dist/i18n/useLink.d.ts.map +1 -0
  36. package/dist/i18n/useLink.js +58 -0
  37. package/dist/i18n/useModuleTranslation.d.ts +11 -0
  38. package/dist/i18n/useModuleTranslation.d.ts.map +1 -0
  39. package/dist/i18n/useModuleTranslation.js +23 -0
  40. package/dist/index.d.ts +7 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +7 -0
  43. package/dist/layouts/AdminLayout.js +1 -1
  44. package/dist/layouts/AppProviders.d.ts +4 -1
  45. package/dist/layouts/AppProviders.d.ts.map +1 -1
  46. package/dist/layouts/AppProviders.js +18 -8
  47. package/dist/layouts/AuthLayout.js +1 -1
  48. package/dist/layouts/AuthLayoutWithSidebar.d.ts.map +1 -1
  49. package/dist/layouts/AuthLayoutWithSidebar.js +5 -3
  50. package/dist/layouts/RootLayout.d.ts +4 -1
  51. package/dist/layouts/RootLayout.d.ts.map +1 -1
  52. package/dist/layouts/RootLayout.js +2 -2
  53. package/dist/scripts/i18n-build.d.ts +7 -0
  54. package/dist/scripts/i18n-build.d.ts.map +1 -0
  55. package/dist/scripts/i18n-build.js +100 -0
  56. package/dist/scripts/init-app.js +12 -12
  57. package/dist/scripts/module-build.d.ts.map +1 -1
  58. package/dist/scripts/module-build.js +384 -81
  59. package/dist/styles.css +1 -1
  60. package/dist/templates/DefaultDoc.d.ts.map +1 -1
  61. package/dist/templates/DefaultDoc.js +90 -2
  62. package/dist/templates/middleware-i18n.example.d.ts +16 -0
  63. package/dist/templates/middleware-i18n.example.d.ts.map +1 -0
  64. package/dist/templates/middleware-i18n.example.js +72 -0
  65. package/package.json +14 -3
  66. package/src/app-shell/not-found.tsx +2 -2
  67. package/src/components/LanguageSwitcher.tsx +156 -0
  68. package/src/config/version.ts +19 -21
  69. package/src/i18n/LanguageProvider.tsx +7 -0
  70. package/src/i18n/README_LANG_HELPERS.md +187 -0
  71. package/src/i18n/TranslationsScript.tsx +17 -0
  72. package/src/i18n/cookies.ts +24 -0
  73. package/src/i18n/langHrefHelper.ts +51 -0
  74. package/src/i18n/server-helpers.ts +48 -0
  75. package/src/i18n/server-lang.ts +51 -0
  76. package/src/i18n/server.ts +13 -0
  77. package/src/i18n/types.ts +39 -0
  78. package/src/i18n/useLangRouter.ts +21 -0
  79. package/src/i18n/useLink.ts +60 -0
  80. package/src/i18n/useModuleTranslation.ts +27 -0
  81. package/src/index.ts +20 -0
  82. package/src/layouts/AdminLayout.tsx +1 -1
  83. package/src/layouts/AppProviders.tsx +35 -16
  84. package/src/layouts/AuthLayout.tsx +1 -1
  85. package/src/layouts/AuthLayoutWithSidebar.tsx +5 -3
  86. package/src/layouts/RootLayout.tsx +12 -5
  87. package/src/scripts/i18n-build.ts +122 -0
  88. package/src/scripts/init-app.ts +12 -12
  89. package/src/scripts/module-build.ts +476 -94
  90. package/src/templates/DefaultDoc.tsx +511 -1
  91. package/src/templates/middleware-i18n.example.ts +92 -0
  92. package/dist/app-shell/(admin)/dashboard/page.d.ts +0 -2
  93. package/dist/app-shell/(admin)/dashboard/page.d.ts.map +0 -1
  94. package/dist/app-shell/(admin)/dashboard/page.js +0 -5
  95. package/dist/app-shell/(admin)/page.d.ts +0 -2
  96. package/dist/app-shell/(admin)/page.d.ts.map +0 -1
  97. package/dist/app-shell/(admin)/page.js +0 -5
  98. package/dist/app-shell/(auth)/dashboard/page.d.ts +0 -2
  99. package/dist/app-shell/(auth)/dashboard/page.d.ts.map +0 -1
  100. package/dist/app-shell/(auth)/dashboard/page.js +0 -5
  101. package/dist/app-shell/(auth)/page.d.ts +0 -2
  102. package/dist/app-shell/(auth)/page.d.ts.map +0 -1
  103. package/dist/app-shell/(auth)/page.js +0 -5
  104. package/dist/components/TableStructure.d.ts +0 -8
  105. package/dist/components/TableStructure.d.ts.map +0 -1
  106. package/dist/components/TableStructure.js +0 -37
  107. package/dist/hooks/useNotificationsSimple.d.ts +0 -20
  108. package/dist/hooks/useNotificationsSimple.d.ts.map +0 -1
  109. package/dist/hooks/useNotificationsSimple.js +0 -37
  110. package/dist/module-build.d.ts +0 -2
  111. package/dist/module-build.d.ts.map +0 -1
  112. package/dist/module-build.js +0 -50
  113. package/dist/modules/index.d.ts +0 -3
  114. package/dist/modules/index.d.ts.map +0 -1
  115. package/dist/modules/index.js +0 -2
  116. package/dist/src/__tests__/module-registry.test.d.ts +0 -2
  117. package/dist/src/__tests__/module-registry.test.d.ts.map +0 -1
  118. package/dist/src/__tests__/module-registry.test.js +0 -53
  119. package/dist/src/app-shell/(admin)/layout.d.ts +0 -4
  120. package/dist/src/app-shell/(admin)/layout.d.ts.map +0 -1
  121. package/dist/src/app-shell/(admin)/layout.js +0 -5
  122. package/dist/src/app-shell/(auth)/layout.d.ts +0 -4
  123. package/dist/src/app-shell/(auth)/layout.d.ts.map +0 -1
  124. package/dist/src/app-shell/(auth)/layout.js +0 -5
  125. package/dist/src/app-shell/(public)/page.d.ts +0 -2
  126. package/dist/src/app-shell/(public)/page.d.ts.map +0 -1
  127. package/dist/src/app-shell/(public)/page.js +0 -5
  128. package/dist/src/app-shell/layout.d.ts +0 -3
  129. package/dist/src/app-shell/layout.d.ts.map +0 -1
  130. package/dist/src/app-shell/layout.js +0 -3
  131. package/dist/src/app-shell/not-found.d.ts +0 -2
  132. package/dist/src/app-shell/not-found.d.ts.map +0 -1
  133. package/dist/src/app-shell/not-found.js +0 -10
  134. package/dist/src/auth/authHelpers.d.ts +0 -7
  135. package/dist/src/auth/authHelpers.d.ts.map +0 -1
  136. package/dist/src/auth/authHelpers.js +0 -19
  137. package/dist/src/auth/useAuthSession.d.ts +0 -7
  138. package/dist/src/auth/useAuthSession.d.ts.map +0 -1
  139. package/dist/src/auth/useAuthSession.js +0 -49
  140. package/dist/src/cli.d.ts +0 -3
  141. package/dist/src/cli.d.ts.map +0 -1
  142. package/dist/src/cli.js +0 -143
  143. package/dist/src/components/NotificationContainer.d.ts +0 -2
  144. package/dist/src/components/NotificationContainer.d.ts.map +0 -1
  145. package/dist/src/components/NotificationContainer.js +0 -8
  146. package/dist/src/hooks/useNotifications.d.ts +0 -30
  147. package/dist/src/hooks/useNotifications.d.ts.map +0 -1
  148. package/dist/src/hooks/useNotifications.js +0 -165
  149. package/dist/src/index.d.ts +0 -22
  150. package/dist/src/index.d.ts.map +0 -1
  151. package/dist/src/index.js +0 -22
  152. package/dist/src/layouts/AdminLayout.d.ts +0 -4
  153. package/dist/src/layouts/AdminLayout.d.ts.map +0 -1
  154. package/dist/src/layouts/AdminLayout.js +0 -4
  155. package/dist/src/layouts/AdminLayoutWithSidebar.d.ts +0 -10
  156. package/dist/src/layouts/AdminLayoutWithSidebar.d.ts.map +0 -1
  157. package/dist/src/layouts/AdminLayoutWithSidebar.js +0 -62
  158. package/dist/src/layouts/AppProviders.d.ts +0 -27
  159. package/dist/src/layouts/AppProviders.d.ts.map +0 -1
  160. package/dist/src/layouts/AppProviders.js +0 -48
  161. package/dist/src/layouts/AuthLayout.d.ts +0 -4
  162. package/dist/src/layouts/AuthLayout.d.ts.map +0 -1
  163. package/dist/src/layouts/AuthLayout.js +0 -4
  164. package/dist/src/layouts/AuthLayoutWithSidebar.d.ts +0 -12
  165. package/dist/src/layouts/AuthLayoutWithSidebar.d.ts.map +0 -1
  166. package/dist/src/layouts/AuthLayoutWithSidebar.js +0 -60
  167. package/dist/src/layouts/PublicLayout.d.ts +0 -8
  168. package/dist/src/layouts/PublicLayout.d.ts.map +0 -1
  169. package/dist/src/layouts/PublicLayout.js +0 -6
  170. package/dist/src/layouts/PublicLayoutWithSidebar.d.ts +0 -9
  171. package/dist/src/layouts/PublicLayoutWithSidebar.d.ts.map +0 -1
  172. package/dist/src/layouts/PublicLayoutWithSidebar.js +0 -60
  173. package/dist/src/layouts/RootLayout.d.ts +0 -6
  174. package/dist/src/layouts/RootLayout.d.ts.map +0 -1
  175. package/dist/src/layouts/RootLayout.js +0 -9
  176. package/dist/src/modules/module-loader.d.ts +0 -5
  177. package/dist/src/modules/module-loader.d.ts.map +0 -1
  178. package/dist/src/modules/module-loader.js +0 -10
  179. package/dist/src/scripts/db-init.d.ts +0 -2
  180. package/dist/src/scripts/db-init.d.ts.map +0 -1
  181. package/dist/src/scripts/db-init.js +0 -300
  182. package/dist/src/scripts/db-migrations-sync.d.ts +0 -2
  183. package/dist/src/scripts/db-migrations-sync.d.ts.map +0 -1
  184. package/dist/src/scripts/db-migrations-sync.js +0 -84
  185. package/dist/src/scripts/dev-sync.d.ts +0 -2
  186. package/dist/src/scripts/dev-sync.d.ts.map +0 -1
  187. package/dist/src/scripts/dev-sync.js +0 -194
  188. package/dist/src/scripts/init-app.d.ts +0 -12
  189. package/dist/src/scripts/init-app.d.ts.map +0 -1
  190. package/dist/src/scripts/init-app.js +0 -2175
  191. package/dist/src/scripts/module-add.d.ts +0 -2
  192. package/dist/src/scripts/module-add.d.ts.map +0 -1
  193. package/dist/src/scripts/module-add.js +0 -232
  194. package/dist/src/scripts/module-build.d.ts +0 -2
  195. package/dist/src/scripts/module-build.d.ts.map +0 -1
  196. package/dist/src/scripts/module-build.js +0 -1280
  197. package/dist/src/scripts/module-create.d.ts +0 -28
  198. package/dist/src/scripts/module-create.d.ts.map +0 -1
  199. package/dist/src/scripts/module-create.js +0 -1429
  200. package/dist/src/scripts/module-delete.d.ts +0 -6
  201. package/dist/src/scripts/module-delete.d.ts.map +0 -1
  202. package/dist/src/scripts/module-delete.js +0 -147
  203. package/dist/src/scripts/module-list.d.ts +0 -2
  204. package/dist/src/scripts/module-list.d.ts.map +0 -1
  205. package/dist/src/scripts/module-list.js +0 -61
  206. package/dist/src/scripts/module-remove.d.ts +0 -2
  207. package/dist/src/scripts/module-remove.d.ts.map +0 -1
  208. package/dist/src/scripts/module-remove.js +0 -311
  209. package/dist/src/scripts/readme-build.d.ts +0 -2
  210. package/dist/src/scripts/readme-build.d.ts.map +0 -1
  211. package/dist/src/scripts/readme-build.js +0 -39
  212. package/dist/src/scripts/script-runner.d.ts +0 -5
  213. package/dist/src/scripts/script-runner.d.ts.map +0 -1
  214. package/dist/src/scripts/script-runner.js +0 -25
  215. package/dist/src/templates/AuthGuidePage.d.ts +0 -2
  216. package/dist/src/templates/AuthGuidePage.d.ts.map +0 -1
  217. package/dist/src/templates/AuthGuidePage.js +0 -9
  218. package/dist/src/templates/DefaultDoc.d.ts +0 -2
  219. package/dist/src/templates/DefaultDoc.d.ts.map +0 -1
  220. package/dist/src/templates/DefaultDoc.js +0 -240
  221. package/dist/src/templates/DocPage.d.ts +0 -17
  222. package/dist/src/templates/DocPage.d.ts.map +0 -1
  223. package/dist/src/templates/DocPage.js +0 -193
  224. package/dist/src/templates/DocsPageWithModules.d.ts +0 -2
  225. package/dist/src/templates/DocsPageWithModules.d.ts.map +0 -1
  226. package/dist/src/templates/DocsPageWithModules.js +0 -8
  227. package/dist/src/templates/MigrationsGuidePage.d.ts +0 -2
  228. package/dist/src/templates/MigrationsGuidePage.d.ts.map +0 -1
  229. package/dist/src/templates/MigrationsGuidePage.js +0 -11
  230. package/dist/src/templates/ModuleGuidePage.d.ts +0 -2
  231. package/dist/src/templates/ModuleGuidePage.d.ts.map +0 -1
  232. package/dist/src/templates/ModuleGuidePage.js +0 -14
  233. package/dist/src/templates/SimpleDocPage.d.ts +0 -2
  234. package/dist/src/templates/SimpleDocPage.d.ts.map +0 -1
  235. package/dist/src/templates/SimpleDocPage.js +0 -28
  236. package/dist/src/templates/SimpleHomePage.d.ts +0 -6
  237. package/dist/src/templates/SimpleHomePage.d.ts.map +0 -1
  238. package/dist/src/templates/SimpleHomePage.js +0 -7
  239. package/dist/src/types/menu.d.ts +0 -23
  240. package/dist/src/types/menu.d.ts.map +0 -1
  241. package/dist/src/types/menu.js +0 -1
  242. package/dist/templates/HomePage.d.ts +0 -6
  243. package/dist/templates/HomePage.d.ts.map +0 -1
  244. package/dist/templates/HomePage.js +0 -6
  245. package/dist/templates/components/AppAside.d.ts +0 -6
  246. package/dist/templates/components/AppAside.d.ts.map +0 -1
  247. package/dist/templates/components/AppAside.js +0 -9
  248. package/dist/templates/layouts/admin-layout.d.ts +0 -4
  249. package/dist/templates/layouts/admin-layout.d.ts.map +0 -1
  250. package/dist/templates/layouts/admin-layout.js +0 -6
  251. package/dist/templates/layouts/auth-layout.d.ts +0 -4
  252. package/dist/templates/layouts/auth-layout.d.ts.map +0 -1
  253. package/dist/templates/layouts/auth-layout.js +0 -6
  254. 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";
@@ -1,3 +1,3 @@
1
1
  export function AdminLayout({ children }: { children: React.ReactNode }) {
2
- return <div className="max-w-screen pt-12 px-2 md:px-5">{children}</div>;
2
+ return <div className="max-w-screen px-2 md:px-5">{children}</div>;
3
3
  }
@@ -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
- ); // const handleLogout = async () => {
79
- // const { signOut } = await import("../auth/authHelpers.js");
80
- // await signOut();
81
- // router.push("/auth/signin");
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
- <AuthContext.Provider value={authValue}>
86
- <ModuleContext.Provider value={modules}>
87
- <NotificationContext.Provider value={notificationsData}>
88
- <RealtimeProvider userId={user?.id} config={realtimeConfig}>
89
- {children}
90
- <ToastProvider placement="bottom-right" toastOffset={5} />
91
- </RealtimeProvider>
92
- </NotificationContext.Provider>
93
- </ModuleContext.Provider>
94
- </AuthContext.Provider>
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
  }
@@ -1,3 +1,3 @@
1
1
  export function AuthLayout({ children }: { children: React.ReactNode }) {
2
- return <div className="pt-18 px-2 md:px-5 max-w-screen">{children}</div>;
2
+ return <div className="pt-4 px-2 md:px-5 max-w-screen">{children}</div>;
3
3
  }
@@ -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
- // Pour la section auth, isAdminSection sera false
81
- const isAdminSection = false;
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={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="fr" suppressHydrationWarning>
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 realtimeConfig={realtimeConfig}>
26
- <div className=" min-h-screen bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-white">
27
- {children}
28
- </div>
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
+ });
@@ -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
- <ClientLayout>{children}</ClientLayout>
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
- <ClientLayout>{children}</ClientLayout>
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 { useRouter } from "next/navigation";
590
+ import { useLocalizedRouter } from "@lastbrain/core";
593
591
 
594
592
  export default function NotFound() {
595
- const router = useRouter();
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 = useRouter();
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 = useRouter();
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
  }