@lastbrain/module-auth 2.0.13 → 2.0.19

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 (43) hide show
  1. package/dist/auth.build.config.d.ts.map +1 -1
  2. package/dist/auth.build.config.js +33 -47
  3. package/dist/components/AccountButton.d.ts.map +1 -1
  4. package/dist/components/AccountButton.js +9 -5
  5. package/dist/web/admin/signup-stats.d.ts.map +1 -1
  6. package/dist/web/admin/signup-stats.js +4 -2
  7. package/dist/web/admin/user-detail.d.ts.map +1 -1
  8. package/dist/web/admin/user-detail.js +42 -17
  9. package/dist/web/admin/users-by-signup-source.d.ts.map +1 -1
  10. package/dist/web/admin/users-by-signup-source.js +18 -7
  11. package/dist/web/admin/users.d.ts.map +1 -1
  12. package/dist/web/admin/users.js +11 -6
  13. package/dist/web/auth/dashboard.d.ts.map +1 -1
  14. package/dist/web/auth/dashboard.js +7 -3
  15. package/dist/web/auth/folder.d.ts.map +1 -1
  16. package/dist/web/auth/folder.js +5 -3
  17. package/dist/web/auth/profile.d.ts.map +1 -1
  18. package/dist/web/auth/profile.js +13 -6
  19. package/dist/web/auth/reglage.d.ts.map +1 -1
  20. package/dist/web/auth/reglage.js +11 -6
  21. package/dist/web/public/SignInPage.d.ts.map +1 -1
  22. package/dist/web/public/SignInPage.js +14 -56
  23. package/dist/web/public/SignUpPage.d.ts.map +1 -1
  24. package/dist/web/public/SignUpPage.js +18 -11
  25. package/package.json +4 -3
  26. package/src/auth.build.config.ts +34 -48
  27. package/src/components/AccountButton.tsx +17 -10
  28. package/src/i18n/en.json +263 -0
  29. package/src/i18n/fr.json +261 -0
  30. package/src/web/admin/signup-stats.tsx +10 -3
  31. package/src/web/admin/user-detail.tsx +135 -56
  32. package/src/web/admin/users-by-signup-source.tsx +60 -21
  33. package/src/web/admin/users.tsx +41 -18
  34. package/src/web/auth/dashboard.tsx +25 -9
  35. package/src/web/auth/folder.tsx +11 -3
  36. package/src/web/auth/profile.tsx +63 -29
  37. package/src/web/auth/reglage.tsx +43 -19
  38. package/src/web/public/SignInPage.tsx +32 -70
  39. package/src/web/public/SignUpPage.tsx +48 -26
  40. package/supabase/migrations/20251112000000_user_init.sql +35 -19
  41. package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +8 -3
  42. package/supabase/migrations/20251112000002_sync_avatars.sql +7 -1
  43. package/supabase/migrations/20251124000001_add_get_admin_user_details.sql +2 -1
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useEffect, useCallback } from "react";
4
+ import { useModuleTranslation, useLanguage } from "@lastbrain/core";
4
5
  import {
5
6
  Card,
6
7
  CardHeader,
@@ -70,6 +71,8 @@ export function UserDetailPage({
70
71
  userId,
71
72
  moduleUserTabs = [],
72
73
  }: UserDetailPageProps) {
74
+ const t = useModuleTranslation("auth");
75
+ const { t: tGlobal } = useLanguage(); // Pour les traductions des autres modules
73
76
  const { user: _currentUser } = useAuth();
74
77
  const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
75
78
  const [loading, setLoading] = useState(true);
@@ -100,7 +103,11 @@ export function UserDetailPage({
100
103
  const userDetails = await response.json();
101
104
  setUserProfile(userDetails);
102
105
  } catch (error) {
103
- console.error("Erreur lors du chargement du profil:", error);
106
+ console.error(
107
+ t("user_detail.profile_loading_error") ||
108
+ "Erreur lors du chargement du profil:",
109
+ error
110
+ );
104
111
  setUserProfile(null);
105
112
  } finally {
106
113
  setLoading(false);
@@ -116,7 +123,9 @@ export function UserDetailPage({
116
123
  if (!notificationTitle.trim() || !notificationMessage.trim()) {
117
124
  addToast({
118
125
  color: "danger",
119
- title: "Veuillez remplir le titre et le message",
126
+ title:
127
+ t("user_detail.fill_title_message") ||
128
+ "Veuillez remplir le titre et le message",
120
129
  });
121
130
  return;
122
131
  }
@@ -149,12 +158,16 @@ export function UserDetailPage({
149
158
 
150
159
  addToast({
151
160
  color: "success",
152
- title: "Notification envoyée avec succès",
161
+ title:
162
+ t("user_detail.notification_sent_success") ||
163
+ "Notification envoyée avec succès",
153
164
  });
154
165
  } catch {
155
166
  addToast({
156
167
  color: "danger",
157
- title: "Erreur lors de l'envoi de la notification",
168
+ title:
169
+ t("user_detail.notification_send_error") ||
170
+ "Erreur lors de l'envoi de la notification",
158
171
  });
159
172
  } finally {
160
173
  setSendingNotification(false);
@@ -173,7 +186,9 @@ export function UserDetailPage({
173
186
  return (
174
187
  <Card>
175
188
  <CardBody>
176
- <p className="text-center text-gray-500">Utilisateur non trouvé</p>
189
+ <p className="text-center text-gray-500">
190
+ {t("user_detail.user_not_found") || "Utilisateur non trouvé"}
191
+ </p>
177
192
  </CardBody>
178
193
  </Card>
179
194
  );
@@ -204,14 +219,16 @@ export function UserDetailPage({
204
219
  {userProfile.full_name || userProfile.email}
205
220
  </h1>
206
221
  <p className="text-sm text-default-500 ">
207
- <span className="">Dernière connexion:</span>{" "}
222
+ <span className="">
223
+ {t("user_detail.last_login") || "Dernière connexion:"}
224
+ </span>{" "}
208
225
  {userProfile.last_sign_in_at
209
226
  ? new Date(userProfile.last_sign_in_at).toLocaleDateString()
210
- : "N/A"}{" "}
211
- à{" "}
227
+ : t("user_detail.not_available") || "N/A"}{" "}
228
+ {t("user_detail.at") || "à"}{" "}
212
229
  {userProfile.last_sign_in_at
213
230
  ? new Date(userProfile.last_sign_in_at).toLocaleTimeString()
214
- : "N/A"}
231
+ : t("user_detail.not_available") || "N/A"}
215
232
  </p>
216
233
  </div>
217
234
  <p className="text-gray-500">{userProfile.email}</p>
@@ -221,7 +238,9 @@ export function UserDetailPage({
221
238
  color={isAdmin ? "danger" : "primary"}
222
239
  size="sm"
223
240
  >
224
- {isAdmin ? "Administrateur" : "Utilisateur"}
241
+ {isAdmin
242
+ ? t("user_detail.administrator") || "Administrateur"
243
+ : t("user_detail.user") || "Utilisateur"}
225
244
  </Chip>
226
245
  {userProfile.profile?.signup_source && (
227
246
  <Chip
@@ -234,8 +253,8 @@ export function UserDetailPage({
234
253
  size="sm"
235
254
  >
236
255
  {userProfile.profile.signup_source.toLowerCase() === "recipe"
237
- ? "Recipe"
238
- : "LastBrain"}
256
+ ? t("user_detail.source_recipe") || "Recipe"
257
+ : t("user_detail.source_lastbrain") || "LastBrain"}
239
258
  </Chip>
240
259
  )}
241
260
  <Snippet color="default" size="sm" symbol="#">
@@ -252,7 +271,7 @@ export function UserDetailPage({
252
271
  {/* moduleUserTabs injectés (debug retiré) */}
253
272
 
254
273
  <Tabs
255
- aria-label="Options utilisateur"
274
+ aria-label={t("user_detail.user_options") || "Options utilisateur"}
256
275
  color="primary"
257
276
  variant="underlined"
258
277
  >
@@ -262,7 +281,7 @@ export function UserDetailPage({
262
281
  title={
263
282
  <div className="flex items-center space-x-2">
264
283
  <User size={16} />
265
- <span>Profil</span>
284
+ <span>{t("user_detail.tab_profile") || "Profil"}</span>
266
285
  </div>
267
286
  }
268
287
  >
@@ -272,18 +291,24 @@ export function UserDetailPage({
272
291
  <CardBody>
273
292
  <h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
274
293
  <User size={18} />
275
- Identité
294
+ {t("user_detail.identity_section") || "Identité"}
276
295
  </h3>
277
296
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
278
297
  <div>
279
- <span className="text-default-500">Nom complet</span>
298
+ <span className="text-default-500">
299
+ {t("user_detail.full_name") || "Nom complet"}
300
+ </span>
280
301
  <p className="font-medium mt-1">
281
- {userProfile.full_name || "Non renseigné"}
302
+ {userProfile.full_name ||
303
+ t("user_detail.not_provided") ||
304
+ "Non renseigné"}
282
305
  </p>
283
306
  </div>
284
307
  {userProfile.profile?.first_name && (
285
308
  <div>
286
- <span className="text-default-500">Prénom</span>
309
+ <span className="text-default-500">
310
+ {t("user_detail.first_name") || "Prénom"}
311
+ </span>
287
312
  <p className="font-medium mt-1">
288
313
  {userProfile.profile.first_name}
289
314
  </p>
@@ -291,19 +316,25 @@ export function UserDetailPage({
291
316
  )}
292
317
  {userProfile.profile?.last_name && (
293
318
  <div>
294
- <span className="text-default-500">Nom</span>
319
+ <span className="text-default-500">
320
+ {t("user_detail.last_name") || "Nom"}
321
+ </span>
295
322
  <p className="font-medium mt-1">
296
323
  {userProfile.profile.last_name}
297
324
  </p>
298
325
  </div>
299
326
  )}
300
327
  <div>
301
- <span className="text-default-500">Email</span>
328
+ <span className="text-default-500">
329
+ {t("user_detail.email") || "Email"}
330
+ </span>
302
331
  <p className="font-medium mt-1">{userProfile.email}</p>
303
332
  </div>
304
333
  {userProfile.profile?.phone && (
305
334
  <div>
306
- <span className="text-default-500">Téléphone</span>
335
+ <span className="text-default-500">
336
+ {t("user_detail.phone") || "Téléphone"}
337
+ </span>
307
338
  <p className="font-medium mt-1">
308
339
  {userProfile.profile.phone}
309
340
  </p>
@@ -312,7 +343,9 @@ export function UserDetailPage({
312
343
  </div>
313
344
  {userProfile.profile?.bio && (
314
345
  <div className="mt-4">
315
- <span className="text-default-500 text-sm">Bio</span>
346
+ <span className="text-default-500 text-sm">
347
+ {t("user_detail.bio") || "Bio"}
348
+ </span>
316
349
  <p className="font-medium mt-1 text-sm">
317
350
  {userProfile.profile.bio}
318
351
  </p>
@@ -328,12 +361,15 @@ export function UserDetailPage({
328
361
  <Card>
329
362
  <CardBody>
330
363
  <h3 className="font-semibold text-lg mb-4">
331
- Informations professionnelles
364
+ {t("user_detail.professional_info") ||
365
+ "Informations professionnelles"}
332
366
  </h3>
333
367
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
334
368
  {userProfile.profile?.company && (
335
369
  <div>
336
- <span className="text-default-500">Entreprise</span>
370
+ <span className="text-default-500">
371
+ {t("user_detail.company") || "Entreprise"}
372
+ </span>
337
373
  <p className="font-medium mt-1">
338
374
  {userProfile.profile.company}
339
375
  </p>
@@ -341,7 +377,9 @@ export function UserDetailPage({
341
377
  )}
342
378
  {userProfile.profile?.website && (
343
379
  <div>
344
- <span className="text-default-500">Site web</span>
380
+ <span className="text-default-500">
381
+ {t("user_detail.website") || "Site web"}
382
+ </span>
345
383
  <p className="font-medium mt-1">
346
384
  <a
347
385
  href={userProfile.profile.website}
@@ -357,7 +395,7 @@ export function UserDetailPage({
357
395
  {userProfile.profile?.location && (
358
396
  <div>
359
397
  <span className="text-default-500">
360
- Localisation
398
+ {t("user_detail.location") || "Localisation"}
361
399
  </span>
362
400
  <p className="font-medium mt-1">
363
401
  {userProfile.profile.location}
@@ -375,12 +413,14 @@ export function UserDetailPage({
375
413
  <Card>
376
414
  <CardBody>
377
415
  <h3 className="font-semibold text-lg mb-4">
378
- Préférences
416
+ {t("user_detail.preferences") || "Préférences"}
379
417
  </h3>
380
418
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
381
419
  {userProfile.profile?.language && (
382
420
  <div>
383
- <span className="text-default-500">Langue</span>
421
+ <span className="text-default-500">
422
+ {t("user_detail.language") || "Langue"}
423
+ </span>
384
424
  <p className="font-medium mt-1">
385
425
  {userProfile.profile.language.toUpperCase()}
386
426
  </p>
@@ -389,7 +429,7 @@ export function UserDetailPage({
389
429
  {userProfile.profile?.timezone && (
390
430
  <div>
391
431
  <span className="text-default-500">
392
- Fuseau horaire
432
+ {t("user_detail.timezone") || "Fuseau horaire"}
393
433
  </span>
394
434
  <p className="font-medium mt-1">
395
435
  {userProfile.profile.timezone}
@@ -405,11 +445,14 @@ export function UserDetailPage({
405
445
  <Card>
406
446
  <CardBody>
407
447
  <h3 className="font-semibold text-lg mb-4">
408
- Informations du compte
448
+ {t("user_detail.account_info") ||
449
+ "Informations du compte"}
409
450
  </h3>
410
451
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
411
452
  <div>
412
- <span className="text-default-500">Rôle</span>
453
+ <span className="text-default-500">
454
+ {t("user_detail.role") || "Rôle"}
455
+ </span>
413
456
  <div className="mt-2">
414
457
  <Chip
415
458
  variant="flat"
@@ -417,14 +460,16 @@ export function UserDetailPage({
417
460
  size="sm"
418
461
  >
419
462
  {isAdmin
420
- ? "Administrateur"
421
- : "Utilisateur standard"}
463
+ ? t("user_detail.administrator") ||
464
+ "Administrateur"
465
+ : t("user_detail.standard_user") ||
466
+ "Utilisateur standard"}
422
467
  </Chip>
423
468
  </div>
424
469
  </div>
425
470
  <div>
426
471
  <span className="text-default-500">
427
- Date de création
472
+ {t("user_detail.creation_date") || "Date de création"}
428
473
  </span>
429
474
  <p className="font-medium mt-1">
430
475
  {userProfile.created_at
@@ -435,12 +480,12 @@ export function UserDetailPage({
435
480
  month: "long",
436
481
  day: "numeric",
437
482
  })
438
- : "N/A"}
483
+ : t("user_detail.not_available") || "N/A"}
439
484
  </p>
440
485
  </div>
441
486
  <div>
442
487
  <span className="text-default-500">
443
- Dernière connexion
488
+ {t("user_detail.last_login") || "Dernière connexion"}
444
489
  </span>
445
490
  <p className="font-medium mt-1">
446
491
  {userProfile.last_sign_in_at
@@ -453,7 +498,7 @@ export function UserDetailPage({
453
498
  hour: "2-digit",
454
499
  minute: "2-digit",
455
500
  })
456
- : "Jamais"}
501
+ : t("user_detail.never") || "Jamais"}
457
502
  </p>
458
503
  </div>
459
504
  </div>
@@ -468,25 +513,39 @@ export function UserDetailPage({
468
513
  title={
469
514
  <div className="flex items-center space-x-2">
470
515
  <Bell size={16} />
471
- <span>Notifications</span>
516
+ <span>
517
+ {t("user_detail.tab_notifications") || "Notifications"}
518
+ </span>
472
519
  </div>
473
520
  }
474
521
  >
475
522
  <div className="space-y-4 mt-4">
476
- <h3 className="font-semibold">Envoyer une notification</h3>
523
+ <h3 className="font-semibold">
524
+ {t("user_detail.send_notification") ||
525
+ "Envoyer une notification"}
526
+ </h3>
477
527
 
478
528
  <div className="space-y-4">
479
529
  <Input
480
- label="Titre de la notification"
481
- placeholder="Ex: Nouveau message important"
530
+ label={
531
+ t("user_detail.notification_title") ||
532
+ "Titre de la notification"
533
+ }
534
+ placeholder={
535
+ t("user_detail.notification_title_placeholder") ||
536
+ "Ex: Nouveau message important"
537
+ }
482
538
  value={notificationTitle}
483
539
  onChange={(e) => setNotificationTitle(e.target.value)}
484
540
  maxLength={100}
485
541
  />
486
542
 
487
543
  <Textarea
488
- label="Message"
489
- placeholder="Contenu de la notification..."
544
+ label={t("user_detail.notification_message") || "Message"}
545
+ placeholder={
546
+ t("user_detail.notification_message_placeholder") ||
547
+ "Contenu de la notification..."
548
+ }
490
549
  value={notificationMessage}
491
550
  onChange={(e) => setNotificationMessage(e.target.value)}
492
551
  maxLength={500}
@@ -494,16 +553,28 @@ export function UserDetailPage({
494
553
  />
495
554
 
496
555
  <Select
497
- label="Type de notification"
556
+ label={
557
+ t("user_detail.notification_type") ||
558
+ "Type de notification"
559
+ }
498
560
  selectedKeys={[notificationType]}
499
561
  onSelectionChange={(keys) =>
500
562
  setNotificationType(Array.from(keys)[0] as string)
501
563
  }
502
564
  >
503
- <SelectItem key="info">Information</SelectItem>
504
- <SelectItem key="warning">Avertissement</SelectItem>
505
- <SelectItem key="danger">Danger</SelectItem>
506
- <SelectItem key="success">Succès</SelectItem>
565
+ <SelectItem key="info">
566
+ {t("user_detail.notification_type_info") || "Information"}
567
+ </SelectItem>
568
+ <SelectItem key="warning">
569
+ {t("user_detail.notification_type_warning") ||
570
+ "Avertissement"}
571
+ </SelectItem>
572
+ <SelectItem key="danger">
573
+ {t("user_detail.notification_type_danger") || "Danger"}
574
+ </SelectItem>
575
+ <SelectItem key="success">
576
+ {t("user_detail.notification_type_success") || "Succès"}
577
+ </SelectItem>
507
578
  </Select>
508
579
 
509
580
  <Button
@@ -515,7 +586,7 @@ export function UserDetailPage({
515
586
  !notificationTitle.trim() || !notificationMessage.trim()
516
587
  }
517
588
  >
518
- Envoyer la notification
589
+ {t("user_detail.send_button") || "Envoyer la notification"}
519
590
  </Button>
520
591
  </div>
521
592
  </div>
@@ -527,29 +598,37 @@ export function UserDetailPage({
527
598
  title={
528
599
  <div className="flex items-center space-x-2">
529
600
  <Settings size={16} />
530
- <span>Paramètres</span>
601
+ <span>{t("user_detail.tab_settings") || "Paramètres"}</span>
531
602
  </div>
532
603
  }
533
604
  >
534
605
  <div className="space-y-4 mt-4">
535
- <h3 className="font-semibold">Actions administrateur</h3>
606
+ <h3 className="font-semibold">
607
+ {t("user_detail.admin_actions") || "Actions administrateur"}
608
+ </h3>
536
609
 
537
610
  <div className="space-y-3 space-x-5">
538
611
  <Button color="warning" variant="bordered" size="sm">
539
- Réinitialiser le mot de passe
612
+ {t("user_detail.reset_password_btn") ||
613
+ "Réinitialiser le mot de passe"}
540
614
  </Button>
541
615
 
542
616
  <Button color="danger" variant="bordered" size="sm">
543
- Suspendre le compte
617
+ {t("user_detail.suspend_account_btn") ||
618
+ "Suspendre le compte"}
544
619
  </Button>
545
620
 
546
621
  <Button color="secondary" variant="bordered" size="sm">
547
- Promouvoir en administrateur
622
+ {t("user_detail.promote_admin_btn") ||
623
+ "Promouvoir en administrateur"}
548
624
  </Button>
549
625
  </div>
550
626
 
551
627
  <div className="mt-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
552
- <h4 className="font-medium mb-2">Métadonnées techniques</h4>
628
+ <h4 className="font-medium mb-2">
629
+ {t("user_detail.technical_metadata") ||
630
+ "Métadonnées techniques"}
631
+ </h4>
553
632
  <pre className="text-xs text-gray-600 dark:text-gray-400 overflow-auto">
554
633
  {JSON.stringify(
555
634
  {
@@ -576,7 +655,7 @@ export function UserDetailPage({
576
655
  title={
577
656
  <div className="flex items-center space-x-2">
578
657
  {IconComponent && <IconComponent size={16} />}
579
- <span>{tab.title}</span>
658
+ <span>{tGlobal(tab.title)}</span>
580
659
  </div>
581
660
  }
582
661
  >
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect, useState } from "react";
4
+ import { useModuleTranslation } from "@lastbrain/core";
4
5
  import {
5
6
  Card,
6
7
  CardBody,
@@ -37,6 +38,7 @@ interface PaginationData {
37
38
  }
38
39
 
39
40
  export function UsersBySignupSourcePage() {
41
+ const t = useModuleTranslation("auth");
40
42
  const [users, setUsers] = useState<User[]>([]);
41
43
  const [loading, setLoading] = useState(true);
42
44
  const [error, setError] = useState<string | null>(null);
@@ -69,7 +71,10 @@ export function UsersBySignupSourcePage() {
69
71
  const response = await fetch(url);
70
72
 
71
73
  if (!response.ok) {
72
- throw new Error("Erreur lors du chargement des utilisateurs");
74
+ throw new Error(
75
+ t("users_by_source.loading_error") ||
76
+ "Erreur lors du chargement des utilisateurs"
77
+ );
73
78
  }
74
79
 
75
80
  const result = await response.json();
@@ -88,7 +93,10 @@ export function UsersBySignupSourcePage() {
88
93
  setPagination(result.pagination);
89
94
  } catch (err) {
90
95
  setError(
91
- err instanceof Error ? err.message : "Erreur lors du chargement"
96
+ err instanceof Error
97
+ ? err.message
98
+ : t("users_by_source.loading_error_generic") ||
99
+ "Erreur lors du chargement"
92
100
  );
93
101
  } finally {
94
102
  setLoading(false);
@@ -132,14 +140,18 @@ export function UsersBySignupSourcePage() {
132
140
  <div className="flex items-center gap-2 mb-8">
133
141
  <Users size={28} className="text-primary-600" />
134
142
  <h1 className="text-3xl font-bold">
135
- Utilisateurs par source d'inscription
143
+ {t("users_by_source.title") ||
144
+ "Utilisateurs par source d'inscription"}
136
145
  </h1>
137
146
  </div>
138
147
 
139
148
  {/* Filters */}
140
149
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
141
150
  <Input
142
- placeholder="Rechercher par email ou nom..."
151
+ placeholder={
152
+ t("users_by_source.search_placeholder") ||
153
+ "Rechercher par email ou nom..."
154
+ }
143
155
  value={searchQuery}
144
156
  onChange={handleSearch}
145
157
  startContent={<Search size={16} />}
@@ -151,45 +163,71 @@ export function UsersBySignupSourcePage() {
151
163
  />
152
164
 
153
165
  <Select
154
- label="Filtrer par source"
166
+ label={t("users_by_source.filter_label") || "Filtrer par source"}
155
167
  selectedKeys={[source]}
156
168
  onChange={(e) => handleSourceChange(e.target.value)}
157
169
  >
158
- <SelectItem key="">Toutes les sources</SelectItem>
159
- <SelectItem key="lastbrain">LastBrain</SelectItem>
160
- <SelectItem key="recipe">Recipe</SelectItem>
170
+ <SelectItem key="">
171
+ {t("users_by_source.all_sources") || "Toutes les sources"}
172
+ </SelectItem>
173
+ <SelectItem key="lastbrain">
174
+ {t("users_by_source.source_lastbrain") || "LastBrain"}
175
+ </SelectItem>
176
+ <SelectItem key="recipe">
177
+ {t("users_by_source.source_recipe") || "Recipe"}
178
+ </SelectItem>
161
179
  </Select>
162
180
  </div>
163
181
 
164
182
  {/* Results info */}
165
183
  <div className="text-sm text-default-600">
166
- Affichage de{" "}
184
+ {t("users_by_source.showing") || "Affichage de"}{" "}
167
185
  <span className="font-semibold">
168
186
  {(pagination.page - 1) * pagination.limit + 1}-
169
187
  {Math.min(pagination.page * pagination.limit, pagination.total)}
170
188
  </span>{" "}
171
- sur <span className="font-semibold">{pagination.total}</span>{" "}
172
- utilisateurs
189
+ {t("users_by_source.of") || "sur"}{" "}
190
+ <span className="font-semibold">{pagination.total}</span>{" "}
191
+ {t("users_by_source.users") || "utilisateurs"}
173
192
  </div>
174
193
 
175
194
  {/* Users Table */}
176
195
  <Card>
177
196
  <CardHeader>
178
- <h3 className="text-lg font-semibold">Liste des utilisateurs</h3>
197
+ <h3 className="text-lg font-semibold">
198
+ {t("users_by_source.list_title") || "Liste des utilisateurs"}
199
+ </h3>
179
200
  </CardHeader>
180
201
  <CardBody>
181
202
  {loading ? (
182
203
  <div className="flex justify-center py-8">
183
- <Spinner size="lg" label="Chargement..." />
204
+ <Spinner
205
+ size="lg"
206
+ label={t("users_by_source.loading") || "Chargement..."}
207
+ />
184
208
  </div>
185
209
  ) : (
186
210
  <>
187
- <Table aria-label="Tableau des utilisateurs">
211
+ <Table
212
+ aria-label={
213
+ t("users_by_source.table_aria_label") ||
214
+ "Tableau des utilisateurs"
215
+ }
216
+ >
188
217
  <TableHeader>
189
- <TableColumn>Nom</TableColumn>
190
- <TableColumn>Email</TableColumn>
191
- <TableColumn>Source</TableColumn>
192
- <TableColumn>Date d'inscription</TableColumn>
218
+ <TableColumn>
219
+ {t("users_by_source.column_name") || "Nom"}
220
+ </TableColumn>
221
+ <TableColumn>
222
+ {t("users_by_source.column_email") || "Email"}
223
+ </TableColumn>
224
+ <TableColumn>
225
+ {t("users_by_source.column_source") || "Source"}
226
+ </TableColumn>
227
+ <TableColumn>
228
+ {t("users_by_source.column_signup_date") ||
229
+ "Date d'inscription"}
230
+ </TableColumn>
193
231
  </TableHeader>
194
232
  <TableBody>
195
233
  {users.length > 0 ? (
@@ -210,8 +248,8 @@ export function UsersBySignupSourcePage() {
210
248
  variant="flat"
211
249
  >
212
250
  {user.signup_source.toLowerCase() === "recipe"
213
- ? "🍳 Recipe"
214
- : "🧠 LastBrain"}
251
+ ? `🍳 ${t("users_by_source.recipe") || "Recipe"}`
252
+ : `🧠 ${t("users_by_source.lastbrain") || "LastBrain"}`}
215
253
  </Chip>
216
254
  </TableCell>
217
255
  <TableCell>
@@ -234,7 +272,8 @@ export function UsersBySignupSourcePage() {
234
272
  <TableRow>
235
273
  <TableCell colSpan={4} className="text-center py-8">
236
274
  <p className="text-default-500">
237
- Aucun utilisateur trouvé
275
+ {t("users_by_source.no_users") ||
276
+ "Aucun utilisateur trouvé"}
238
277
  </p>
239
278
  </TableCell>
240
279
  </TableRow>