@djangocfg/layouts 2.1.109 → 2.1.111

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 (53) hide show
  1. package/package.json +16 -14
  2. package/src/components/errors/ErrorBoundary.tsx +12 -6
  3. package/src/components/errors/ErrorLayout.tsx +19 -9
  4. package/src/components/errors/errorConfig.ts +28 -22
  5. package/src/layouts/AuthLayout/AuthLayout.tsx +92 -20
  6. package/src/layouts/AuthLayout/components/index.ts +11 -7
  7. package/src/layouts/AuthLayout/components/oauth/index.ts +0 -1
  8. package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +35 -0
  9. package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +56 -0
  10. package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +22 -0
  11. package/src/layouts/AuthLayout/components/shared/AuthError.tsx +26 -0
  12. package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +47 -0
  13. package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +53 -0
  14. package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +41 -0
  15. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +42 -0
  16. package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +48 -0
  17. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +57 -0
  18. package/src/layouts/AuthLayout/components/shared/index.ts +21 -0
  19. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +171 -0
  20. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +114 -0
  21. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +70 -0
  22. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +24 -0
  23. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +125 -0
  24. package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +91 -0
  25. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +92 -0
  26. package/src/layouts/AuthLayout/components/steps/index.ts +6 -0
  27. package/src/layouts/AuthLayout/constants.ts +24 -0
  28. package/src/layouts/AuthLayout/content.ts +78 -0
  29. package/src/layouts/AuthLayout/hooks/index.ts +1 -0
  30. package/src/layouts/AuthLayout/hooks/useCopyToClipboard.ts +37 -0
  31. package/src/layouts/AuthLayout/index.ts +9 -5
  32. package/src/layouts/AuthLayout/styles/auth.css +578 -0
  33. package/src/layouts/ProfileLayout/ProfileLayout.tsx +130 -58
  34. package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +2 -2
  35. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +12 -4
  36. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +6 -2
  37. package/src/layouts/_components/UserMenu.tsx +14 -6
  38. package/src/snippets/AuthDialog/AuthDialog.tsx +15 -6
  39. package/src/snippets/Breadcrumbs.tsx +19 -8
  40. package/src/snippets/McpChat/components/ChatPanel.tsx +16 -6
  41. package/src/snippets/McpChat/components/ChatSidebar.tsx +20 -8
  42. package/src/snippets/PWAInstall/components/A2HSHint.tsx +23 -10
  43. package/src/snippets/PWAInstall/components/DesktopGuide.tsx +44 -32
  44. package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +34 -25
  45. package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +34 -25
  46. package/src/snippets/PushNotifications/components/PushPrompt.tsx +16 -6
  47. package/src/layouts/AuthLayout/components/AuthHelp.tsx +0 -114
  48. package/src/layouts/AuthLayout/components/AuthSuccess.tsx +0 -101
  49. package/src/layouts/AuthLayout/components/IdentifierForm.tsx +0 -322
  50. package/src/layouts/AuthLayout/components/OTPForm.tsx +0 -174
  51. package/src/layouts/AuthLayout/components/TwoFactorForm.tsx +0 -140
  52. package/src/layouts/AuthLayout/components/TwoFactorSetup.tsx +0 -286
  53. package/src/layouts/AuthLayout/components/oauth/OAuthProviders.tsx +0 -56
@@ -2,8 +2,10 @@
2
2
 
3
3
  import { Camera, ChevronRight, LogOut, Mail, Phone, Shield, ShieldCheck, Trash2, User, Building2, Briefcase } from 'lucide-react';
4
4
  import moment from 'moment';
5
- import React, { useEffect, useState } from 'react';
5
+ import React, { useEffect, useMemo, useState } from 'react';
6
6
  import { useForm } from 'react-hook-form';
7
+
8
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
7
9
  import { toast } from '@djangocfg/ui-core/hooks';
8
10
  import { cn } from '@djangocfg/ui-core/lib';
9
11
 
@@ -28,7 +30,7 @@ import {
28
30
  } from '@djangocfg/ui-core/components';
29
31
  import { zodResolver } from '@hookform/resolvers/zod';
30
32
 
31
- import { TwoFactorSetup } from '../AuthLayout/components/TwoFactorSetup';
33
+ import { SetupStep } from '../AuthLayout/components/steps/SetupStep';
32
34
  import { profileLogger } from '../../utils/logger';
33
35
 
34
36
  // ─────────────────────────────────────────────────────────────────────────────
@@ -129,16 +131,23 @@ interface EditFieldDialogProps {
129
131
  isSaving: boolean;
130
132
  }
131
133
 
132
- const fieldLabels: Record<string, string> = {
133
- first_name: 'First Name',
134
- last_name: 'Last Name',
135
- company: 'Company',
136
- position: 'Position',
137
- phone: 'Phone',
138
- };
139
-
140
134
  const EditFieldDialog = ({ open, onOpenChange, field, currentValue, onSave, isSaving }: EditFieldDialogProps) => {
141
135
  const [value, setValue] = useState(currentValue);
136
+ const t = useTypedT<I18nTranslations>();
137
+
138
+ const labels = useMemo(() => ({
139
+ fieldLabels: {
140
+ first_name: t('layouts.profilePage.firstName'),
141
+ last_name: t('layouts.profilePage.lastName'),
142
+ company: t('layouts.profilePage.company'),
143
+ position: t('layouts.profilePage.position'),
144
+ phone: t('layouts.profilePage.phone'),
145
+ } as Record<string, string>,
146
+ save: t('layouts.profilePage.save'),
147
+ saving: t('layouts.profilePage.saving'),
148
+ cancel: t('layouts.profilePage.cancel'),
149
+ enterField: t('layouts.profilePage.enterField'),
150
+ }), [t]);
142
151
 
143
152
  useEffect(() => {
144
153
  setValue(currentValue);
@@ -152,27 +161,29 @@ const EditFieldDialog = ({ open, onOpenChange, field, currentValue, onSave, isSa
152
161
 
153
162
  if (!field) return null;
154
163
 
164
+ const fieldLabel = labels.fieldLabels[field] || field;
165
+
155
166
  return (
156
167
  <Dialog open={open} onOpenChange={onOpenChange}>
157
168
  <DialogContent className="sm:max-w-md">
158
169
  <DialogHeader>
159
- <DialogTitle>{fieldLabels[field]}</DialogTitle>
170
+ <DialogTitle>{fieldLabel}</DialogTitle>
160
171
  </DialogHeader>
161
172
  <div className="py-4">
162
173
  <Input
163
174
  value={value}
164
175
  onChange={(e) => setValue(e.target.value)}
165
- placeholder={`Enter ${fieldLabels[field].toLowerCase()}`}
176
+ placeholder={labels.enterField.replace('{field}', fieldLabel.toLowerCase())}
166
177
  autoFocus
167
178
  onKeyDown={(e) => e.key === 'Enter' && handleSave()}
168
179
  />
169
180
  </div>
170
181
  <DialogFooter>
171
182
  <Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isSaving}>
172
- Cancel
183
+ {labels.cancel}
173
184
  </Button>
174
185
  <Button onClick={handleSave} disabled={isSaving}>
175
- {isSaving ? 'Saving...' : 'Save'}
186
+ {isSaving ? labels.saving : labels.save}
176
187
  </Button>
177
188
  </DialogFooter>
178
189
  </DialogContent>
@@ -186,7 +197,7 @@ const EditFieldDialog = ({ open, onOpenChange, field, currentValue, onSave, isSa
186
197
 
187
198
  const ProfileContent = ({
188
199
  onUnauthenticated,
189
- title = 'Profile',
200
+ title,
190
201
  enable2FA = false,
191
202
  enableDeleteAccount = true,
192
203
  }: ProfileLayoutProps) => {
@@ -194,6 +205,7 @@ const ProfileContent = ({
194
205
  const [editingField, setEditingField] = useState<EditingField>(null);
195
206
  const [isSaving, setIsSaving] = useState(false);
196
207
  const [isUploading, setIsUploading] = useState(false);
208
+ const t = useTypedT<I18nTranslations>();
197
209
 
198
210
  // 2FA state
199
211
  const [show2FASetup, setShow2FASetup] = useState(false);
@@ -202,6 +214,56 @@ const ProfileContent = ({
202
214
  // Delete account state
203
215
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
204
216
 
217
+ const labels = useMemo(() => ({
218
+ title: title || t('layouts.profilePage.title'),
219
+ // Sections
220
+ account: t('layouts.profilePage.account'),
221
+ personalInfo: t('layouts.profilePage.personalInfo'),
222
+ work: t('layouts.profilePage.work'),
223
+ security: t('layouts.profilePage.security'),
224
+ actions: t('layouts.profilePage.actions'),
225
+ // Fields
226
+ fieldLabels: {
227
+ first_name: t('layouts.profilePage.firstName'),
228
+ last_name: t('layouts.profilePage.lastName'),
229
+ company: t('layouts.profilePage.company'),
230
+ position: t('layouts.profilePage.position'),
231
+ phone: t('layouts.profilePage.phone'),
232
+ } as Record<string, string>,
233
+ email: t('layouts.profilePage.email'),
234
+ // Values
235
+ notSet: t('layouts.profilePage.notSet'),
236
+ on: t('layouts.profilePage.on'),
237
+ off: t('layouts.profilePage.off'),
238
+ // Actions
239
+ save: t('layouts.profilePage.save'),
240
+ saving: t('layouts.profilePage.saving'),
241
+ cancel: t('layouts.profilePage.cancel'),
242
+ signOut: t('layouts.profilePage.signOut'),
243
+ deleteAccount: t('layouts.profilePage.deleteAccount'),
244
+ // 2FA
245
+ twoFactorAuth: t('layouts.profilePage.twoFactorAuth'),
246
+ // Avatar
247
+ avatar: t('layouts.profilePage.avatar'),
248
+ changeAvatar: t('layouts.profilePage.changeAvatar'),
249
+ // Membership
250
+ memberSince: t('layouts.profilePage.memberSince'),
251
+ // Placeholder
252
+ enterField: t('layouts.profilePage.enterField'),
253
+ // Messages
254
+ profileUpdated: t('layouts.profilePage.profileUpdated'),
255
+ avatarUpdated: t('layouts.profilePage.avatarUpdated'),
256
+ failedToUpdate: t('layouts.profilePage.failedToUpdate'),
257
+ failedToUploadAvatar: t('layouts.profilePage.failedToUploadAvatar'),
258
+ selectImageFile: t('layouts.profilePage.selectImageFile'),
259
+ fileTooLarge: t('layouts.profilePage.fileTooLarge'),
260
+ // Not authenticated
261
+ notAuthenticated: t('layouts.profilePage.notAuthenticated'),
262
+ pleaseLogIn: t('layouts.profilePage.pleaseLogIn'),
263
+ // Loading
264
+ loading: t('ui.states.loading'),
265
+ }), [t, title]);
266
+
205
267
  const form = useForm<PatchedUserProfileUpdateRequest>({
206
268
  resolver: zodResolver(PatchedUserProfileUpdateRequestSchema),
207
269
  defaultValues: {
@@ -247,20 +309,20 @@ const ProfileContent = ({
247
309
  if (!file) return;
248
310
 
249
311
  if (!file.type.startsWith('image/')) {
250
- toast.error('Please select an image file');
312
+ toast.error(labels.selectImageFile);
251
313
  return;
252
314
  }
253
315
  if (file.size > 5 * 1024 * 1024) {
254
- toast.error('File size must be less than 5MB');
316
+ toast.error(labels.fileTooLarge);
255
317
  return;
256
318
  }
257
319
 
258
320
  setIsUploading(true);
259
321
  try {
260
322
  await uploadAvatar(file);
261
- toast.success('Avatar updated');
323
+ toast.success(labels.avatarUpdated);
262
324
  } catch (error) {
263
- toast.error('Failed to upload avatar');
325
+ toast.error(labels.failedToUploadAvatar);
264
326
  profileLogger.error('Avatar upload error:', error);
265
327
  } finally {
266
328
  setIsUploading(false);
@@ -271,11 +333,11 @@ const ProfileContent = ({
271
333
  setIsSaving(true);
272
334
  try {
273
335
  await updateProfile({ [field]: value });
274
- toast.success('Profile updated');
336
+ toast.success(labels.profileUpdated);
275
337
  setEditingField(null);
276
338
  } catch (error: any) {
277
339
  profileLogger.error('Profile update error:', error);
278
- toast.error(error?.response?.data?.[field]?.[0] || 'Failed to update');
340
+ toast.error(error?.response?.data?.[field]?.[0] || labels.failedToUpdate);
279
341
  } finally {
280
342
  setIsSaving(false);
281
343
  }
@@ -290,7 +352,7 @@ const ProfileContent = ({
290
352
  return (
291
353
  <Preloader
292
354
  variant="fullscreen"
293
- text="Loading..."
355
+ text={labels.loading}
294
356
  size="lg"
295
357
  backdrop={true}
296
358
  backdropOpacity={80}
@@ -303,8 +365,8 @@ const ProfileContent = ({
303
365
  return (
304
366
  <div className="flex items-center justify-center min-h-screen">
305
367
  <div className="text-center">
306
- <h1 className="text-2xl font-bold mb-4">Not Authenticated</h1>
307
- <p className="text-muted-foreground">Please log in to view your profile.</p>
368
+ <h1 className="text-2xl font-bold mb-4">{labels.notAuthenticated}</h1>
369
+ <p className="text-muted-foreground">{labels.pleaseLogIn}</p>
308
370
  </div>
309
371
  </div>
310
372
  );
@@ -314,7 +376,7 @@ const ProfileContent = ({
314
376
  if (show2FASetup) {
315
377
  return (
316
378
  <div className="container mx-auto px-4 py-8 max-w-lg">
317
- <TwoFactorSetup
379
+ <SetupStep
318
380
  onComplete={() => {
319
381
  setShow2FASetup(false);
320
382
  fetch2FAStatus();
@@ -328,21 +390,21 @@ const ProfileContent = ({
328
390
  return (
329
391
  <div className="container mx-auto px-4 py-8 max-w-lg">
330
392
  {/* Header */}
331
- <h1 className="text-2xl font-bold text-center mb-8">{title}</h1>
393
+ <h1 className="text-2xl font-bold text-center mb-8">{labels.title}</h1>
332
394
 
333
395
  {/* Avatar & Name */}
334
396
  <div className="flex flex-col items-center mb-8">
335
397
  <div className="relative group mb-3">
336
398
  <Avatar className="w-24 h-24 text-3xl">
337
399
  {user.avatar ? (
338
- <img src={user.avatar} alt="Avatar" className="w-full h-full object-cover" />
400
+ <img src={user.avatar} alt={labels.avatar} className="w-full h-full object-cover" />
339
401
  ) : (
340
402
  <AvatarFallback className="bg-primary text-primary-foreground">
341
403
  {getInitials(user.display_username || user.email || '')}
342
404
  </AvatarFallback>
343
405
  )}
344
406
  </Avatar>
345
- <label className="absolute inset-0 rounded-full bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer">
407
+ <label className="absolute inset-0 rounded-full bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer" title={labels.changeAvatar}>
346
408
  {isUploading ? (
347
409
  <div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
348
410
  ) : (
@@ -360,7 +422,7 @@ const ProfileContent = ({
360
422
  <h2 className="text-xl font-semibold">{user.display_username || user.email}</h2>
361
423
  {user.date_joined && (
362
424
  <p className="text-sm text-muted-foreground">
363
- Member since {moment.utc(user.date_joined).local().format('MMMM YYYY')}
425
+ {labels.memberSince.replace('{date}', moment.utc(user.date_joined).local().format('MMMM YYYY'))}
364
426
  </p>
365
427
  )}
366
428
  </div>
@@ -368,11 +430,11 @@ const ProfileContent = ({
368
430
  <div className="space-y-6">
369
431
  {/* Account Info */}
370
432
  <div>
371
- <SettingsGroupHeader>Account</SettingsGroupHeader>
433
+ <SettingsGroupHeader>{labels.account}</SettingsGroupHeader>
372
434
  <SettingsGroup>
373
435
  <SettingsRow
374
436
  icon={<Mail className="w-5 h-5" />}
375
- label="Email"
437
+ label={labels.email}
376
438
  value={user.email}
377
439
  showChevron={false}
378
440
  />
@@ -381,24 +443,24 @@ const ProfileContent = ({
381
443
 
382
444
  {/* Personal Info */}
383
445
  <div>
384
- <SettingsGroupHeader>Personal Information</SettingsGroupHeader>
446
+ <SettingsGroupHeader>{labels.personalInfo}</SettingsGroupHeader>
385
447
  <SettingsGroup>
386
448
  <SettingsRow
387
449
  icon={<User className="w-5 h-5" />}
388
- label="First Name"
389
- value={user.first_name || 'Not set'}
450
+ label={labels.fieldLabels.first_name}
451
+ value={user.first_name || labels.notSet}
390
452
  onClick={() => setEditingField('first_name')}
391
453
  />
392
454
  <SettingsRow
393
455
  icon={<User className="w-5 h-5" />}
394
- label="Last Name"
395
- value={user.last_name || 'Not set'}
456
+ label={labels.fieldLabels.last_name}
457
+ value={user.last_name || labels.notSet}
396
458
  onClick={() => setEditingField('last_name')}
397
459
  />
398
460
  <SettingsRow
399
461
  icon={<Phone className="w-5 h-5" />}
400
- label="Phone"
401
- value={user.phone || 'Not set'}
462
+ label={labels.fieldLabels.phone}
463
+ value={user.phone || labels.notSet}
402
464
  onClick={() => setEditingField('phone')}
403
465
  />
404
466
  </SettingsGroup>
@@ -406,18 +468,18 @@ const ProfileContent = ({
406
468
 
407
469
  {/* Work Info */}
408
470
  <div>
409
- <SettingsGroupHeader>Work</SettingsGroupHeader>
471
+ <SettingsGroupHeader>{labels.work}</SettingsGroupHeader>
410
472
  <SettingsGroup>
411
473
  <SettingsRow
412
474
  icon={<Building2 className="w-5 h-5" />}
413
- label="Company"
414
- value={user.company || 'Not set'}
475
+ label={labels.fieldLabels.company}
476
+ value={user.company || labels.notSet}
415
477
  onClick={() => setEditingField('company')}
416
478
  />
417
479
  <SettingsRow
418
480
  icon={<Briefcase className="w-5 h-5" />}
419
- label="Position"
420
- value={user.position || 'Not set'}
481
+ label={labels.fieldLabels.position}
482
+ value={user.position || labels.notSet}
421
483
  onClick={() => setEditingField('position')}
422
484
  />
423
485
  </SettingsGroup>
@@ -426,12 +488,12 @@ const ProfileContent = ({
426
488
  {/* Security */}
427
489
  {enable2FA && (
428
490
  <div>
429
- <SettingsGroupHeader>Security</SettingsGroupHeader>
491
+ <SettingsGroupHeader>{labels.security}</SettingsGroupHeader>
430
492
  <SettingsGroup>
431
493
  <SettingsRow
432
494
  icon={has2FAEnabled ? <ShieldCheck className="w-5 h-5 text-green-500" /> : <Shield className="w-5 h-5" />}
433
- label="Two-Factor Authentication"
434
- value={has2FAEnabled ? 'On' : 'Off'}
495
+ label={labels.twoFactorAuth}
496
+ value={has2FAEnabled ? labels.on : labels.off}
435
497
  onClick={() => !has2FAEnabled && setShow2FASetup(true)}
436
498
  showChevron={!has2FAEnabled}
437
499
  />
@@ -441,17 +503,17 @@ const ProfileContent = ({
441
503
 
442
504
  {/* Actions */}
443
505
  <div>
444
- <SettingsGroupHeader>Actions</SettingsGroupHeader>
506
+ <SettingsGroupHeader>{labels.actions}</SettingsGroupHeader>
445
507
  <SettingsGroup>
446
508
  <SettingsRow
447
509
  icon={<LogOut className="w-5 h-5" />}
448
- label="Sign Out"
510
+ label={labels.signOut}
449
511
  onClick={handleLogout}
450
512
  />
451
513
  {enableDeleteAccount && (
452
514
  <SettingsRow
453
515
  icon={<Trash2 className="w-5 h-5" />}
454
- label="Delete Account"
516
+ label={labels.deleteAccount}
455
517
  onClick={() => setShowDeleteConfirm(true)}
456
518
  destructive
457
519
  />
@@ -485,12 +547,21 @@ const ProfileContent = ({
485
547
 
486
548
  import { useDeleteAccount } from '@djangocfg/api/auth';
487
549
 
488
- const CONFIRMATION_TEXT = 'DELETE';
489
-
490
550
  const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) => {
491
551
  const [confirmationInput, setConfirmationInput] = useState('');
492
552
  const { logout } = useAuth();
493
553
  const { isLoading, error, deleteAccount, clearError } = useDeleteAccount();
554
+ const t = useTypedT<I18nTranslations>();
555
+
556
+ const labels = useMemo(() => ({
557
+ title: t('layouts.profilePage.deleteAccountTitle'),
558
+ description: t('layouts.profilePage.deleteAccountDesc'),
559
+ typeToConfirm: t('layouts.profilePage.typeToConfirm'),
560
+ confirmationWord: t('layouts.profilePage.confirmationWord'),
561
+ cancel: t('layouts.profilePage.cancel'),
562
+ deleteAccount: t('layouts.profilePage.deleteAccount'),
563
+ deleting: t('layouts.profilePage.deleting'),
564
+ }), [t]);
494
565
 
495
566
  useEffect(() => {
496
567
  if (open) {
@@ -507,15 +578,15 @@ const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChan
507
578
  }
508
579
  };
509
580
 
510
- const isValid = confirmationInput.toUpperCase() === CONFIRMATION_TEXT;
581
+ const isValid = confirmationInput.toUpperCase() === labels.confirmationWord.toUpperCase();
511
582
 
512
583
  return (
513
584
  <Dialog open={open} onOpenChange={onOpenChange}>
514
585
  <DialogContent>
515
586
  <DialogHeader>
516
- <DialogTitle className="text-destructive">Delete Account</DialogTitle>
587
+ <DialogTitle className="text-destructive">{labels.title}</DialogTitle>
517
588
  <DialogDescription>
518
- This action cannot be undone. Your account will be permanently deleted.
589
+ {labels.description}
519
590
  </DialogDescription>
520
591
  </DialogHeader>
521
592
 
@@ -525,12 +596,13 @@ const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChan
525
596
  )}
526
597
  <div className="space-y-2">
527
598
  <p className="text-sm text-muted-foreground">
528
- Type <span className="font-mono font-bold">{CONFIRMATION_TEXT}</span> to confirm:
599
+ {labels.typeToConfirm.replace('{word}', '')}
600
+ <span className="font-mono font-bold">{labels.confirmationWord}</span>
529
601
  </p>
530
602
  <Input
531
603
  value={confirmationInput}
532
604
  onChange={(e) => setConfirmationInput(e.target.value)}
533
- placeholder={CONFIRMATION_TEXT}
605
+ placeholder={labels.confirmationWord}
534
606
  disabled={isLoading}
535
607
  autoComplete="off"
536
608
  />
@@ -539,10 +611,10 @@ const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChan
539
611
 
540
612
  <DialogFooter>
541
613
  <Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isLoading}>
542
- Cancel
614
+ {labels.cancel}
543
615
  </Button>
544
616
  <Button variant="destructive" onClick={handleDelete} disabled={isLoading || !isValid}>
545
- {isLoading ? 'Deleting...' : 'Delete Account'}
617
+ {isLoading ? labels.deleting : labels.deleteAccount}
546
618
  </Button>
547
619
  </DialogFooter>
548
620
  </DialogContent>
@@ -22,7 +22,7 @@ import {
22
22
  OTPInput,
23
23
  } from '@djangocfg/ui-core/components';
24
24
 
25
- import { TwoFactorSetup } from '../../AuthLayout/components/TwoFactorSetup';
25
+ import { SetupStep } from '../../AuthLayout/components/steps/SetupStep';
26
26
 
27
27
  type ViewState = 'status' | 'setup' | 'disable';
28
28
 
@@ -102,7 +102,7 @@ export const TwoFactorSection: React.FC = () => {
102
102
  </CardDescription>
103
103
  </CardHeader>
104
104
  <CardContent>
105
- <TwoFactorSetup
105
+ <SetupStep
106
106
  onComplete={handleSetupComplete}
107
107
  onSkip={handleSetupSkip}
108
108
  />
@@ -8,9 +8,10 @@
8
8
 
9
9
  import { X } from 'lucide-react';
10
10
  import Link from 'next/link';
11
- import React from 'react';
11
+ import React, { useMemo } from 'react';
12
12
 
13
13
  import { useAuth } from '@djangocfg/api/auth';
14
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
14
15
  import {
15
16
  Button, Drawer, DrawerClose, DrawerContent, DrawerHeader, DrawerTitle
16
17
  } from '@djangocfg/ui-core/components';
@@ -38,6 +39,13 @@ export function PublicMobileDrawer({
38
39
  userMenu,
39
40
  }: PublicMobileDrawerProps) {
40
41
  const { isAuthenticated } = useAuth();
42
+ const t = useTypedT<I18nTranslations>();
43
+
44
+ const labels = useMemo(() => ({
45
+ closeMenu: t('layouts.mobile.closeMenu'),
46
+ menu: t('layouts.navigation.menu'),
47
+ theme: t('layouts.theme.toggle'),
48
+ }), [t]);
41
49
 
42
50
  return (
43
51
  <Drawer open={isOpen} onOpenChange={(open) => !open && onClose()} direction="right">
@@ -58,7 +66,7 @@ export function PublicMobileDrawer({
58
66
  </div>
59
67
  <DrawerClose className="p-2 rounded-sm transition-colors hover:bg-accent/50">
60
68
  <X className="size-5" />
61
- <span className="sr-only">Close menu</span>
69
+ <span className="sr-only">{labels.closeMenu}</span>
62
70
  </DrawerClose>
63
71
  </DrawerHeader>
64
72
 
@@ -75,7 +83,7 @@ export function PublicMobileDrawer({
75
83
  <div className="space-y-1">
76
84
  <div className="px-4 py-2">
77
85
  <h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
78
- Menu
86
+ {labels.menu}
79
87
  </h3>
80
88
  </div>
81
89
  <div className="space-y-1">
@@ -95,7 +103,7 @@ export function PublicMobileDrawer({
95
103
  {/* Theme Toggle - Fixed at bottom */}
96
104
  <div className="border-t border-border/30 p-4">
97
105
  <div className="flex items-center justify-between px-4 py-3">
98
- <span className="text-sm font-medium text-foreground">Theme</span>
106
+ <span className="text-sm font-medium text-foreground">{labels.theme}</span>
99
107
  <ThemeToggle />
100
108
  </div>
101
109
  </div>
@@ -8,9 +8,10 @@
8
8
 
9
9
  import { Menu } from 'lucide-react';
10
10
  import Link from 'next/link';
11
- import React from 'react';
11
+ import React, { useMemo } from 'react';
12
12
 
13
13
  import { useAuth } from '@djangocfg/api/auth';
14
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
14
15
  import { Button } from '@djangocfg/ui-core/components';
15
16
  import { useIsMobile } from '@djangocfg/ui-core/hooks';
16
17
  import { cn } from '@djangocfg/ui-core/lib';
@@ -37,6 +38,9 @@ export function PublicNavigation({
37
38
  }: PublicNavigationProps) {
38
39
  const { isAuthenticated } = useAuth();
39
40
  const isMobile = useIsMobile();
41
+ const t = useTypedT<I18nTranslations>();
42
+
43
+ const toggleMobileLabel = useMemo(() => t('layouts.navigation.toggleMobile'), [t]);
40
44
 
41
45
  // Nav class (background blur with opacity 0.8)
42
46
  // bg-background/80 - doesnt work in tailwind4
@@ -89,7 +93,7 @@ export function PublicNavigation({
89
93
  variant="ghost"
90
94
  size="icon"
91
95
  onClick={onMobileMenuClick}
92
- aria-label="Toggle mobile menu"
96
+ aria-label={toggleMobileLabel}
93
97
  >
94
98
  <Menu className="h-5 w-5" />
95
99
  </Button>
@@ -32,9 +32,10 @@
32
32
 
33
33
  import { LogOut } from 'lucide-react';
34
34
  import Link from 'next/link';
35
- import React from 'react';
35
+ import React, { useMemo } from 'react';
36
36
 
37
37
  import { useAuth } from '@djangocfg/api/auth';
38
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
38
39
  import {
39
40
  Avatar, AvatarFallback, AvatarImage, Button, DropdownMenu, DropdownMenuContent,
40
41
  DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator,
@@ -59,6 +60,13 @@ export function UserMenu({
59
60
  }: UserMenuProps) {
60
61
  const { user, isAuthenticated, logout } = useAuth();
61
62
  const [mounted, setMounted] = React.useState(false);
63
+ const t = useTypedT<I18nTranslations>();
64
+
65
+ const labels = useMemo(() => ({
66
+ signIn: t('layouts.profile.login'),
67
+ signOut: t('layouts.profile.signOut'),
68
+ userMenu: t('layouts.profile.userMenu'),
69
+ }), [t]);
62
70
 
63
71
  React.useEffect(() => {
64
72
  setMounted(true);
@@ -78,7 +86,7 @@ export function UserMenu({
78
86
  allGroups.push({
79
87
  items: [
80
88
  {
81
- label: 'Sign Out',
89
+ label: labels.signOut,
82
90
  onClick: () => logout(),
83
91
  icon: LogOut,
84
92
  variant: 'destructive',
@@ -87,7 +95,7 @@ export function UserMenu({
87
95
  });
88
96
 
89
97
  return allGroups;
90
- }, [groups, logout]);
98
+ }, [groups, logout, labels.signOut]);
91
99
 
92
100
  if (!mounted) {
93
101
  return null;
@@ -99,7 +107,7 @@ export function UserMenu({
99
107
  return (
100
108
  <Link href={authPath}>
101
109
  <Button variant="default" className="w-full">
102
- Sign In
110
+ {labels.signIn}
103
111
  </Button>
104
112
  </Link>
105
113
  );
@@ -107,7 +115,7 @@ export function UserMenu({
107
115
  return (
108
116
  <Link href={authPath}>
109
117
  <Button variant="default" size="sm">
110
- Sign In
118
+ {labels.signIn}
111
119
  </Button>
112
120
  </Link>
113
121
  );
@@ -194,7 +202,7 @@ export function UserMenu({
194
202
  <AvatarImage src={userAvatar} alt={displayName} />
195
203
  <AvatarFallback>{userInitial}</AvatarFallback>
196
204
  </Avatar>
197
- <span className="sr-only">User menu</span>
205
+ <span className="sr-only">{labels.userMenu}</span>
198
206
  </Button>
199
207
  </DropdownMenuTrigger>
200
208
  <DropdownMenuContent align="end" className="w-56">
@@ -1,12 +1,13 @@
1
1
  'use client';
2
2
 
3
3
  import { LogIn } from 'lucide-react';
4
- import React, { useState } from 'react';
4
+ import React, { useMemo, useState } from 'react';
5
5
 
6
+ import { useCfgRouter } from '@djangocfg/api/auth';
7
+ import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
6
8
  import {
7
9
  Button, Dialog, DialogContent, DialogHeader, DialogTitle
8
10
  } from '@djangocfg/ui-core/components';
9
- import { useCfgRouter } from '@djangocfg/api/auth';
10
11
  import { useEventListener } from '@djangocfg/ui-core';
11
12
 
12
13
  // Re-export events for backwards compatibility
@@ -27,7 +28,15 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
27
28
  authPath = '/auth'
28
29
  }) => {
29
30
  const [open, setOpen] = useState(false);
30
- const [message, setMessage] = useState<string>('Please sign in to continue');
31
+ const t = useTypedT<I18nTranslations>();
32
+
33
+ const labels = useMemo(() => ({
34
+ authRequired: t('layouts.auth.authRequired'),
35
+ pleaseSignIn: t('layouts.auth.pleaseSignIn'),
36
+ goToSignIn: t('layouts.auth.goToSignIn'),
37
+ }), [t]);
38
+
39
+ const [message, setMessage] = useState<string>(labels.pleaseSignIn);
31
40
  const router = useCfgRouter();
32
41
 
33
42
  // Listen for open auth dialog event
@@ -44,7 +53,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
44
53
  });
45
54
 
46
55
  const handleClose = () => {
47
- setMessage('Please sign in to continue');
56
+ setMessage(labels.pleaseSignIn);
48
57
  setOpen(false);
49
58
  };
50
59
 
@@ -67,7 +76,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
67
76
  <Dialog open={open} onOpenChange={handleClose}>
68
77
  <DialogContent className="max-w-sm">
69
78
  <DialogHeader>
70
- <DialogTitle>Authentication Required</DialogTitle>
79
+ <DialogTitle>{labels.authRequired}</DialogTitle>
71
80
  </DialogHeader>
72
81
 
73
82
  <div className="space-y-4">
@@ -75,7 +84,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
75
84
 
76
85
  <Button onClick={handleGoToAuth} className="w-full">
77
86
  <LogIn className="h-4 w-4 mr-2" />
78
- Go to Sign In
87
+ {labels.goToSignIn}
79
88
  </Button>
80
89
  </div>
81
90
  </DialogContent>