strapi-plugin-magic-sessionmanager 4.0.1 → 4.0.3

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 (57) hide show
  1. package/README.md +2 -2
  2. package/admin/src/components/OnlineUsersWidget.jsx +11 -7
  3. package/admin/src/components/SessionDetailModal.jsx +42 -38
  4. package/admin/src/components/SessionInfoPanel.jsx +29 -20
  5. package/admin/src/index.js +9 -0
  6. package/admin/src/pages/HomePage.jsx +128 -161
  7. package/admin/src/pages/License.jsx +3 -3
  8. package/admin/src/pages/Settings.jsx +139 -135
  9. package/admin/src/pages/UpgradePage.jsx +448 -0
  10. package/admin/src/pluginId.js +1 -0
  11. package/admin/src/translations/de.json +294 -15
  12. package/admin/src/translations/en.json +293 -14
  13. package/admin/src/translations/es.json +284 -18
  14. package/admin/src/translations/fr.json +284 -18
  15. package/admin/src/translations/pt.json +284 -18
  16. package/admin/src/utils/theme.js +85 -0
  17. package/dist/_chunks/{Analytics-Dv9f_0eZ.mjs → Analytics-DTE_zmRV.mjs} +2 -2
  18. package/dist/_chunks/{Analytics-BBdv1I5y.js → Analytics-lw_JaOVy.js} +2 -2
  19. package/dist/_chunks/{App-CJaZPNjt.js → App-DDKYCjKw.js} +216 -206
  20. package/dist/_chunks/{App-CIQ-7sa7.mjs → App-DJW1ZNl5.mjs} +216 -206
  21. package/dist/_chunks/{License-nrmFxoBm.mjs → License-DaOFuImm.mjs} +4 -8
  22. package/dist/_chunks/{License-D24rgaZQ.js → License-Tk-6UfPl.js} +4 -8
  23. package/dist/_chunks/{OnlineUsersWidget-B8JS1xZu.js → OnlineUsersWidget-C1qTpsws.js} +11 -7
  24. package/dist/_chunks/{OnlineUsersWidget-ArMl0nen.mjs → OnlineUsersWidget-CADphbXG.mjs} +11 -7
  25. package/dist/_chunks/{Settings-D5dLEGc_.mjs → Settings-C9xvckgq.mjs} +191 -179
  26. package/dist/_chunks/{Settings-CqxgjU0y.js → Settings-DyEAuTNQ.js} +191 -179
  27. package/dist/_chunks/UpgradePage-Dssk8A0Z.js +354 -0
  28. package/dist/_chunks/UpgradePage-cINvE9zY.mjs +352 -0
  29. package/dist/_chunks/de-CDA1V0rF.mjs +292 -0
  30. package/dist/_chunks/de-I-Q-pWqu.js +292 -0
  31. package/dist/_chunks/en-Bd7_h-4e.js +292 -0
  32. package/dist/_chunks/en-DzmOCyzQ.mjs +292 -0
  33. package/dist/_chunks/es-BcAx18XG.js +277 -0
  34. package/dist/_chunks/es-Cx-SN6qV.mjs +277 -0
  35. package/dist/_chunks/fr-DCzYMuJ-.js +277 -0
  36. package/dist/_chunks/fr-DXlXE5Eo.mjs +277 -0
  37. package/dist/_chunks/{index-WH04CS1c.js → index-CWcvrfXc.js} +45 -42
  38. package/dist/_chunks/{index-Duk1_Wrz.mjs → index-DQO9bNP7.mjs} +45 -42
  39. package/dist/_chunks/pt-21-MAb72.js +277 -0
  40. package/dist/_chunks/pt-zsdTSjba.mjs +277 -0
  41. package/dist/_chunks/{useLicense-BwOlCyhc.js → useLicense-DtvJOszr.js} +1 -1
  42. package/dist/_chunks/{useLicense-Ce8GaxB0.mjs → useLicense-DxbD4Wf8.mjs} +1 -1
  43. package/dist/admin/index.js +1 -1
  44. package/dist/admin/index.mjs +1 -1
  45. package/dist/server/index.js +1 -1
  46. package/dist/server/index.mjs +1 -1
  47. package/package.json +1 -1
  48. package/dist/_chunks/de-BxFx1pwE.js +0 -23
  49. package/dist/_chunks/de-CdO3s01z.mjs +0 -23
  50. package/dist/_chunks/en-CsPpPJL3.mjs +0 -23
  51. package/dist/_chunks/en-RqmpDHdS.js +0 -23
  52. package/dist/_chunks/es-CuLHazN1.js +0 -23
  53. package/dist/_chunks/es-Dkmjhy9c.mjs +0 -23
  54. package/dist/_chunks/fr-BAJp2yhI.js +0 -23
  55. package/dist/_chunks/fr-Bssg_3UF.mjs +0 -23
  56. package/dist/_chunks/pt-BAP9cKs3.js +0 -23
  57. package/dist/_chunks/pt-BVNoNcuY.mjs +0 -23
@@ -1,4 +1,5 @@
1
1
  import { useState, useEffect } from 'react';
2
+ import { useIntl } from 'react-intl';
2
3
  import {
3
4
  Box,
4
5
  Typography,
@@ -23,6 +24,7 @@ import { Check, Information, Duplicate, Trash, Mail, Code, Cog, Shield, Clock }
23
24
  import styled, { keyframes, css } from 'styled-components';
24
25
  import pluginId from '../pluginId';
25
26
  import { useLicense } from '../hooks/useLicense';
27
+ import { getTranslation } from '../utils/getTranslation';
26
28
 
27
29
  // ================ THEME ================
28
30
  const theme = {
@@ -59,9 +61,9 @@ const StickySaveBar = styled(Box)`
59
61
  position: sticky;
60
62
  top: 0;
61
63
  z-index: 10;
62
- background: white;
63
- border-bottom: 1px solid ${theme.colors.neutral[200]};
64
- box-shadow: ${theme.shadows.sm};
64
+ background: ${props => props.theme.colors.neutral0};
65
+ border-bottom: 1px solid ${props => props.theme.colors.neutral200};
66
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
65
67
  `;
66
68
 
67
69
  const ToggleCard = styled(Box)`
@@ -380,9 +382,11 @@ const generateSecureKey = () => {
380
382
  };
381
383
 
382
384
  const SettingsPage = () => {
385
+ const { formatMessage } = useIntl();
383
386
  const { get, post, put } = useFetchClient();
384
387
  const { toggleNotification } = useNotification();
385
388
  const { isPremium, isAdvanced, isEnterprise } = useLicense();
389
+ const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
386
390
  const [loading, setLoading] = useState(true);
387
391
  const [saving, setSaving] = useState(false);
388
392
  const [hasChanges, setHasChanges] = useState(false);
@@ -459,7 +463,7 @@ const SettingsPage = () => {
459
463
  console.error('[Settings] Error loading from backend:', err);
460
464
  toggleNotification({
461
465
  type: 'warning',
462
- message: 'Could not load settings from server. Using defaults.',
466
+ message: t('notifications.warning.settingsLoad', 'Could not load settings from server. Using defaults.'),
463
467
  });
464
468
  // Fallback to default settings
465
469
  setSettings(prev => ({ ...prev, emailTemplates: getDefaultTemplates() }));
@@ -486,7 +490,7 @@ const SettingsPage = () => {
486
490
  if (response?.data?.success) {
487
491
  toggleNotification({
488
492
  type: 'success',
489
- message: 'Settings saved successfully to database!',
493
+ message: t('notifications.success.saved', 'Settings saved successfully to database!'),
490
494
  });
491
495
 
492
496
  setHasChanges(false);
@@ -504,7 +508,7 @@ const SettingsPage = () => {
504
508
  console.error('[Settings] Error saving:', err);
505
509
  toggleNotification({
506
510
  type: 'danger',
507
- message: 'Failed to save settings to server',
511
+ message: t('notifications.error.save', 'Failed to save settings to server'),
508
512
  });
509
513
  } finally {
510
514
  setSaving(false);
@@ -517,7 +521,7 @@ const SettingsPage = () => {
517
521
  };
518
522
 
519
523
  const handleCleanInactive = async () => {
520
- if (!confirm('[WARNING] This will permanently delete ALL inactive sessions.\n\nContinue?')) {
524
+ if (!confirm(t('settings.general.danger.confirm', '[WARNING] This will permanently delete ALL inactive sessions.\n\nContinue?'))) {
521
525
  return;
522
526
  }
523
527
 
@@ -527,12 +531,12 @@ const SettingsPage = () => {
527
531
 
528
532
  toggleNotification({
529
533
  type: 'success',
530
- message: `Successfully deleted ${data.deletedCount} inactive sessions!`,
534
+ message: t('notifications.success.cleaned', 'Successfully deleted {count} inactive sessions!', { count: data.deletedCount }),
531
535
  });
532
536
  } catch (err) {
533
537
  toggleNotification({
534
538
  type: 'danger',
535
- message: 'Failed to delete inactive sessions',
539
+ message: t('notifications.error.clean', 'Failed to delete inactive sessions'),
536
540
  });
537
541
  } finally {
538
542
  setCleaning(false);
@@ -542,7 +546,7 @@ const SettingsPage = () => {
542
546
  if (loading) {
543
547
  return (
544
548
  <Flex justifyContent="center" padding={8}>
545
- <Loader>Loading settings...</Loader>
549
+ <Loader>{t('common.loading', 'Loading...')}</Loader>
546
550
  </Flex>
547
551
  );
548
552
  }
@@ -554,16 +558,16 @@ const SettingsPage = () => {
554
558
  <Flex justifyContent="space-between" alignItems="center">
555
559
  <Flex direction="column" gap={1} alignItems="flex-start">
556
560
  <Typography variant="alpha" fontWeight="bold" style={{ fontSize: '24px' }}>
557
- ⚙️ Session Manager Settings
561
+ ⚙️ {t('settings.title', 'Session Manager Settings')}
558
562
  </Typography>
559
563
  <Typography variant="epsilon" textColor="neutral600">
560
- Configure session tracking, security, and email notifications
564
+ {t('settings.subtitle', 'Configure session tracking, security, and email notifications')}
561
565
  </Typography>
562
566
  </Flex>
563
567
  <Flex gap={2}>
564
568
  {hasChanges && (
565
569
  <Button onClick={handleReset} variant="tertiary" size="L">
566
- Reset
570
+ {t('settings.reset', 'Reset')}
567
571
  </Button>
568
572
  )}
569
573
  <Button
@@ -594,7 +598,7 @@ const SettingsPage = () => {
594
598
  e.currentTarget.style.boxShadow = hasChanges && !saving ? '0 4px 12px rgba(102, 126, 234, 0.4)' : 'none';
595
599
  }}
596
600
  >
597
- {saving ? 'Saving...' : hasChanges ? 'Save Changes' : 'No Changes'}
601
+ {saving ? t('settings.saving', 'Saving...') : hasChanges ? t('settings.save', 'Save Changes') : t('settings.noChanges', 'No Changes')}
598
602
  </Button>
599
603
  </Flex>
600
604
  </Flex>
@@ -609,17 +613,17 @@ const SettingsPage = () => {
609
613
  <Information style={{ width: '20px', height: '20px', color: '#0284C7' }} />
610
614
  <Box>
611
615
  <Typography variant="omega" fontWeight="bold" textColor="primary700" style={{ marginBottom: '4px' }}>
612
- Current License Status
616
+ {t('settings.license.title', 'Current License Status')}
613
617
  </Typography>
614
618
  <Flex gap={3}>
615
619
  <Badge backgroundColor={isPremium ? "success100" : "neutral100"} textColor={isPremium ? "success700" : "neutral600"}>
616
- {isPremium ? '✓' : '✗'} Premium
620
+ {isPremium ? '✓' : '✗'} {t('settings.license.premium', 'Premium')}
617
621
  </Badge>
618
622
  <Badge backgroundColor={isAdvanced ? "primary100" : "neutral100"} textColor={isAdvanced ? "primary700" : "neutral600"}>
619
- {isAdvanced ? '✓' : '✗'} Advanced
623
+ {isAdvanced ? '✓' : '✗'} {t('settings.license.advanced', 'Advanced')}
620
624
  </Badge>
621
625
  <Badge backgroundColor={isEnterprise ? "secondary100" : "neutral100"} textColor={isEnterprise ? "secondary700" : "neutral600"}>
622
- {isEnterprise ? '✓' : '✗'} Enterprise
626
+ {isEnterprise ? '✓' : '✗'} {t('settings.license.enterprise', 'Enterprise')}
623
627
  </Badge>
624
628
  </Flex>
625
629
  </Box>
@@ -634,9 +638,9 @@ const SettingsPage = () => {
634
638
  <Accordion.Header>
635
639
  <Accordion.Trigger
636
640
  icon={Cog}
637
- description="Basic session tracking configuration"
641
+ description={t('settings.general.description', 'Basic session tracking configuration')}
638
642
  >
639
- General Settings
643
+ {t('settings.general.title', 'General Settings')}
640
644
  </Accordion.Trigger>
641
645
  </Accordion.Header>
642
646
  <Accordion.Content>
@@ -644,27 +648,27 @@ const SettingsPage = () => {
644
648
 
645
649
  {/* Session Timeout */}
646
650
  <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
647
- SESSION TIMEOUT
651
+ {t('settings.general.timeout.title', 'SESSION TIMEOUT')}
648
652
  </Typography>
649
653
  <Grid.Root gap={6} style={{ marginBottom: '32px' }}>
650
654
  <Grid.Item col={6} s={12}>
651
655
  <Box>
652
656
  <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
653
- Inactivity Timeout
657
+ {t('settings.general.timeout.inactivity', 'Inactivity Timeout')}
654
658
  </Typography>
655
659
  <SingleSelect
656
660
  value={String(settings.inactivityTimeout)}
657
661
  onChange={(value) => handleChange('inactivityTimeout', parseInt(value))}
658
662
  >
659
- <SingleSelectOption value="5">5 minutes (Very Strict)</SingleSelectOption>
660
- <SingleSelectOption value="10">10 minutes (Strict)</SingleSelectOption>
661
- <SingleSelectOption value="15">15 minutes (Recommended)</SingleSelectOption>
662
- <SingleSelectOption value="30">30 minutes (Moderate)</SingleSelectOption>
663
- <SingleSelectOption value="60">1 hour (Relaxed)</SingleSelectOption>
664
- <SingleSelectOption value="120">2 hours (Very Relaxed)</SingleSelectOption>
663
+ <SingleSelectOption value="5">{t('settings.general.timeout.5min', '5 minutes (Very Strict)')}</SingleSelectOption>
664
+ <SingleSelectOption value="10">{t('settings.general.timeout.10min', '10 minutes (Strict)')}</SingleSelectOption>
665
+ <SingleSelectOption value="15">{t('settings.general.timeout.15min', '15 minutes (Recommended)')}</SingleSelectOption>
666
+ <SingleSelectOption value="30">{t('settings.general.timeout.30min', '30 minutes (Moderate)')}</SingleSelectOption>
667
+ <SingleSelectOption value="60">{t('settings.general.timeout.1hour', '1 hour (Relaxed)')}</SingleSelectOption>
668
+ <SingleSelectOption value="120">{t('settings.general.timeout.2hours', '2 hours (Very Relaxed)')}</SingleSelectOption>
665
669
  </SingleSelect>
666
670
  <Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
667
- Sessions inactive for more than {settings.inactivityTimeout} minutes will be marked as offline
671
+ {t('settings.general.timeout.inactivityHint', 'Sessions inactive for more than {minutes} minutes will be marked as offline', { minutes: settings.inactivityTimeout })}
668
672
  </Typography>
669
673
  </Box>
670
674
  </Grid.Item>
@@ -672,20 +676,20 @@ const SettingsPage = () => {
672
676
  <Grid.Item col={6} s={12}>
673
677
  <Box>
674
678
  <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
675
- Last Seen Rate Limit
679
+ {t('settings.general.rateLimit.title', 'Last Seen Rate Limit')}
676
680
  </Typography>
677
681
  <SingleSelect
678
682
  value={String(settings.lastSeenRateLimit)}
679
683
  onChange={(value) => handleChange('lastSeenRateLimit', parseInt(value))}
680
684
  >
681
- <SingleSelectOption value="10">10 seconds</SingleSelectOption>
682
- <SingleSelectOption value="30">30 seconds (Recommended)</SingleSelectOption>
683
- <SingleSelectOption value="60">1 minute</SingleSelectOption>
684
- <SingleSelectOption value="120">2 minutes</SingleSelectOption>
685
- <SingleSelectOption value="300">5 minutes</SingleSelectOption>
685
+ <SingleSelectOption value="10">{t('settings.general.rateLimit.10sec', '10 seconds')}</SingleSelectOption>
686
+ <SingleSelectOption value="30">{t('settings.general.rateLimit.30sec', '30 seconds (Recommended)')}</SingleSelectOption>
687
+ <SingleSelectOption value="60">{t('settings.general.rateLimit.1min', '1 minute')}</SingleSelectOption>
688
+ <SingleSelectOption value="120">{t('settings.general.rateLimit.2min', '2 minutes')}</SingleSelectOption>
689
+ <SingleSelectOption value="300">{t('settings.general.rateLimit.5min', '5 minutes')}</SingleSelectOption>
686
690
  </SingleSelect>
687
691
  <Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
688
- Prevents excessive database writes. Updates throttled to once every {settings.lastSeenRateLimit} seconds
692
+ {t('settings.general.rateLimit.hint', 'Prevents excessive database writes. Updates throttled to once every {seconds} seconds', { seconds: settings.lastSeenRateLimit })}
689
693
  </Typography>
690
694
  </Box>
691
695
  </Grid.Item>
@@ -694,25 +698,25 @@ const SettingsPage = () => {
694
698
  {/* Cleanup & Retention */}
695
699
  <Divider style={{ marginBottom: '24px' }} />
696
700
  <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
697
- 🧹 AUTO-CLEANUP & RETENTION
701
+ 🧹 {t('settings.general.cleanup.title', 'AUTO-CLEANUP & RETENTION')}
698
702
  </Typography>
699
703
  <Grid.Root gap={6}>
700
704
  <Grid.Item col={6} s={12}>
701
705
  <Box>
702
706
  <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
703
- Cleanup Interval
707
+ {t('settings.general.cleanup.interval', 'Cleanup Interval')}
704
708
  </Typography>
705
709
  <SingleSelect
706
710
  value={String(settings.cleanupInterval)}
707
711
  onChange={(value) => handleChange('cleanupInterval', parseInt(value))}
708
712
  >
709
- <SingleSelectOption value="15">15 minutes</SingleSelectOption>
710
- <SingleSelectOption value="30">30 minutes (Recommended)</SingleSelectOption>
711
- <SingleSelectOption value="60">1 hour</SingleSelectOption>
712
- <SingleSelectOption value="120">2 hours</SingleSelectOption>
713
+ <SingleSelectOption value="15">{t('settings.general.cleanup.15min', '15 minutes')}</SingleSelectOption>
714
+ <SingleSelectOption value="30">{t('settings.general.cleanup.30min', '30 minutes (Recommended)')}</SingleSelectOption>
715
+ <SingleSelectOption value="60">{t('settings.general.cleanup.1hour', '1 hour')}</SingleSelectOption>
716
+ <SingleSelectOption value="120">{t('settings.general.cleanup.2hours', '2 hours')}</SingleSelectOption>
713
717
  </SingleSelect>
714
718
  <Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
715
- Inactive sessions are automatically cleaned every {settings.cleanupInterval} minutes
719
+ {t('settings.general.cleanup.intervalHint', 'Inactive sessions are automatically cleaned every {minutes} minutes', { minutes: settings.cleanupInterval })}
716
720
  </Typography>
717
721
  </Box>
718
722
  </Grid.Item>
@@ -720,22 +724,25 @@ const SettingsPage = () => {
720
724
  <Grid.Item col={6} s={12}>
721
725
  <Box>
722
726
  <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
723
- Retention Period
727
+ {t('settings.general.retention.title', 'Retention Period')}
724
728
  </Typography>
725
729
  <SingleSelect
726
730
  value={String(settings.retentionDays)}
727
731
  onChange={(value) => handleChange('retentionDays', parseInt(value))}
728
732
  >
729
- <SingleSelectOption value="7">7 days</SingleSelectOption>
730
- <SingleSelectOption value="30">30 days</SingleSelectOption>
731
- <SingleSelectOption value="60">60 days</SingleSelectOption>
732
- <SingleSelectOption value="90">90 days (Recommended)</SingleSelectOption>
733
- <SingleSelectOption value="180">180 days</SingleSelectOption>
734
- <SingleSelectOption value="365">1 year</SingleSelectOption>
735
- <SingleSelectOption value="-1">Forever</SingleSelectOption>
733
+ <SingleSelectOption value="7">{t('settings.general.retention.7days', '7 days')}</SingleSelectOption>
734
+ <SingleSelectOption value="30">{t('settings.general.retention.30days', '30 days')}</SingleSelectOption>
735
+ <SingleSelectOption value="60">{t('settings.general.retention.60days', '60 days')}</SingleSelectOption>
736
+ <SingleSelectOption value="90">{t('settings.general.retention.90days', '90 days (Recommended)')}</SingleSelectOption>
737
+ <SingleSelectOption value="180">{t('settings.general.retention.180days', '180 days')}</SingleSelectOption>
738
+ <SingleSelectOption value="365">{t('settings.general.retention.1year', '1 year')}</SingleSelectOption>
739
+ <SingleSelectOption value="-1">{t('settings.general.retention.forever', 'Forever')}</SingleSelectOption>
736
740
  </SingleSelect>
737
741
  <Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
738
- Old sessions deleted after {settings.retentionDays === -1 ? 'never' : `${settings.retentionDays} days`}
742
+ {settings.retentionDays === -1
743
+ ? t('settings.general.retention.hintNever', 'Old sessions deleted after never')
744
+ : t('settings.general.retention.hint', 'Old sessions deleted after {days}', { days: `${settings.retentionDays} days` })
745
+ }
739
746
  </Typography>
740
747
  </Box>
741
748
  </Grid.Item>
@@ -746,10 +753,10 @@ const SettingsPage = () => {
746
753
  <Trash style={{ width: '18px', height: '18px', color: theme.colors.danger[600], flexShrink: 0, marginTop: '2px' }} />
747
754
  <Box style={{ flex: 1 }}>
748
755
  <Typography variant="omega" fontWeight="bold" textColor="danger700" style={{ marginBottom: '8px', display: 'block' }}>
749
- Danger Zone
756
+ {t('settings.general.danger.title', 'Danger Zone')}
750
757
  </Typography>
751
758
  <Typography variant="pi" textColor="danger600" style={{ fontSize: '13px', lineHeight: '1.7' }}>
752
- <strong>Clean All Inactive:</strong> Permanently deletes all inactive sessions. This cannot be undone.
759
+ {t('settings.general.danger.description', 'Clean All Inactive: Permanently deletes all inactive sessions. This cannot be undone.')}
753
760
  </Typography>
754
761
  </Box>
755
762
  <Button
@@ -760,7 +767,7 @@ const SettingsPage = () => {
760
767
  size="S"
761
768
  style={{ flexShrink: 0 }}
762
769
  >
763
- Clean Now
770
+ {t('settings.general.danger.cleanNow', 'Clean Now')}
764
771
  </Button>
765
772
  </Flex>
766
773
  </Box>
@@ -776,16 +783,16 @@ const SettingsPage = () => {
776
783
  <Accordion.Header>
777
784
  <Accordion.Trigger
778
785
  icon={Shield}
779
- description="Security policies and threat protection"
786
+ description={t('settings.security.description', 'Security policies and threat protection')}
780
787
  >
781
- Security Settings
788
+ {t('settings.security.title', 'Security Settings')}
782
789
  </Accordion.Trigger>
783
790
  </Accordion.Header>
784
791
  <Accordion.Content>
785
792
  <Box padding={6}>
786
793
 
787
794
  <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
788
- SECURITY OPTIONS
795
+ {t('settings.security.options', 'SECURITY OPTIONS')}
789
796
  </Typography>
790
797
 
791
798
  {/* Encryption Key Generator */}
@@ -803,30 +810,29 @@ const SettingsPage = () => {
803
810
  <Flex alignItems="center" gap={3}>
804
811
  <Shield style={{ width: 24, height: 24, color: theme.colors.primary[600] }} />
805
812
  <Typography variant="delta" fontWeight="bold">
806
- JWT Encryption Key Generator
813
+ {t('settings.security.encryption.title', 'JWT Encryption Key Generator')}
807
814
  </Typography>
808
815
  </Flex>
809
816
 
810
817
  <Typography variant="omega" textColor="neutral600" style={{ lineHeight: 1.6 }}>
811
- Generate a secure 32-character encryption key for JWT token storage.
812
- This key is used to encrypt tokens before saving them to the database.
818
+ {t('settings.security.encryption.description', 'Generate a secure 32-character encryption key for JWT token storage. This key is used to encrypt tokens before saving them to the database.')}
813
819
  </Typography>
814
820
 
815
821
  <Alert
816
822
  variant="default"
817
- title="Important"
823
+ title={t('settings.security.encryption.important', 'Important')}
818
824
  style={{ marginTop: 8 }}
819
825
  >
820
- Add this key to your <code>.env</code> file as <strong>SESSION_ENCRYPTION_KEY</strong> for production.
826
+ {t('settings.security.encryption.envHint', 'Add this key to your .env file as SESSION_ENCRYPTION_KEY for production.')}
821
827
  </Alert>
822
828
 
823
829
  <Flex gap={3} alignItems="flex-end">
824
830
  <Box style={{ flex: 1 }}>
825
831
  <TextInput
826
- label="Generated Encryption Key"
832
+ label={t('settings.security.encryption.label', 'Generated Encryption Key')}
827
833
  value={encryptionKey}
828
834
  onChange={(e) => setEncryptionKey(e.target.value)}
829
- placeholder="Click 'Generate Key' to create a secure key"
835
+ placeholder={t('settings.security.encryption.placeholder', "Click 'Generate Key' to create a secure key")}
830
836
  type={showEncryptionKey ? 'text' : 'password'}
831
837
  />
832
838
  </Box>
@@ -835,7 +841,7 @@ const SettingsPage = () => {
835
841
  onClick={() => setShowEncryptionKey(!showEncryptionKey)}
836
842
  size="L"
837
843
  >
838
- {showEncryptionKey ? 'Hide' : 'Show'}
844
+ {showEncryptionKey ? t('settings.security.encryption.hide', 'Hide') : t('settings.security.encryption.show', 'Show')}
839
845
  </Button>
840
846
  </Flex>
841
847
 
@@ -849,12 +855,12 @@ const SettingsPage = () => {
849
855
  setShowEncryptionKey(true);
850
856
  toggleNotification({
851
857
  type: 'success',
852
- message: '32-character encryption key generated!'
858
+ message: t('notifications.success.keyGenerated', '32-character encryption key generated!')
853
859
  });
854
860
  }}
855
861
  size="L"
856
862
  >
857
- Generate Key
863
+ {t('settings.security.encryption.generate', 'Generate Key')}
858
864
  </Button>
859
865
 
860
866
  <Button
@@ -865,14 +871,14 @@ const SettingsPage = () => {
865
871
  navigator.clipboard.writeText(encryptionKey);
866
872
  toggleNotification({
867
873
  type: 'success',
868
- message: 'Encryption key copied to clipboard!'
874
+ message: t('notifications.success.keyCopied', 'Encryption key copied to clipboard!')
869
875
  });
870
876
  }
871
877
  }}
872
878
  disabled={!encryptionKey}
873
879
  size="L"
874
880
  >
875
- Copy to Clipboard
881
+ {t('settings.security.encryption.copy', 'Copy to Clipboard')}
876
882
  </Button>
877
883
 
878
884
  <Button
@@ -884,14 +890,14 @@ const SettingsPage = () => {
884
890
  navigator.clipboard.writeText(envLine);
885
891
  toggleNotification({
886
892
  type: 'success',
887
- message: 'Copied as .env format!'
893
+ message: t('notifications.success.envCopied', 'Copied as .env format!')
888
894
  });
889
895
  }
890
896
  }}
891
897
  disabled={!encryptionKey}
892
898
  size="L"
893
899
  >
894
- Copy for .env
900
+ {t('settings.security.encryption.copyEnv', 'Copy for .env')}
895
901
  </Button>
896
902
  </Flex>
897
903
 
@@ -908,7 +914,7 @@ const SettingsPage = () => {
908
914
  }}
909
915
  >
910
916
  <Typography variant="omega" fontWeight="bold" style={{ marginBottom: 8, display: 'block' }}>
911
- Add to .env file:
917
+ {t('settings.security.encryption.envLabel', 'Add to .env file:')}
912
918
  </Typography>
913
919
  <code style={{ color: theme.colors.primary[700] }}>
914
920
  SESSION_ENCRYPTION_KEY={encryptionKey}
@@ -940,10 +946,10 @@ const SettingsPage = () => {
940
946
  textColor={settings.blockSuspiciousSessions ? 'success700' : 'neutral800'}
941
947
  style={{ fontSize: '16px' }}
942
948
  >
943
- Block Suspicious Sessions
949
+ {t('settings.security.blockSuspicious.title', 'Block Suspicious Sessions')}
944
950
  </Typography>
945
951
  <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
946
- Automatically block sessions from VPNs, proxies, or threat IPs
952
+ {t('settings.security.blockSuspicious.description', 'Automatically block sessions from VPNs, proxies, or threat IPs')}
947
953
  </Typography>
948
954
  </Flex>
949
955
  </Flex>
@@ -971,10 +977,10 @@ const SettingsPage = () => {
971
977
  textColor={settings.enableGeolocation ? 'success700' : 'neutral800'}
972
978
  style={{ fontSize: '16px' }}
973
979
  >
974
- IP Geolocation
980
+ {t('settings.security.geolocation.title', 'IP Geolocation')}
975
981
  </Typography>
976
982
  <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
977
- Fetch location data for each session (Premium)
983
+ {t('settings.security.geolocation.description', 'Fetch location data for each session (Premium)')}
978
984
  </Typography>
979
985
  </Flex>
980
986
  </Flex>
@@ -1000,10 +1006,10 @@ const SettingsPage = () => {
1000
1006
  textColor={settings.enableSecurityScoring ? 'success700' : 'neutral800'}
1001
1007
  style={{ fontSize: '16px' }}
1002
1008
  >
1003
- Security Scoring
1009
+ {t('settings.security.scoring.title', 'Security Scoring')}
1004
1010
  </Typography>
1005
1011
  <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
1006
- Calculate security scores and detect threats (Premium)
1012
+ {t('settings.security.scoring.description', 'Calculate security scores and detect threats (Premium)')}
1007
1013
  </Typography>
1008
1014
  </Flex>
1009
1015
  </Flex>
@@ -1019,7 +1025,7 @@ const SettingsPage = () => {
1019
1025
  <Grid.Item col={6} s={12}>
1020
1026
  <Box>
1021
1027
  <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
1022
- 🚫 Max Failed Login Attempts
1028
+ 🚫 {t('settings.security.maxFailed.title', 'Max Failed Login Attempts')}
1023
1029
  </Typography>
1024
1030
  <NumberInput
1025
1031
  value={settings.maxFailedLogins}
@@ -1029,7 +1035,7 @@ const SettingsPage = () => {
1029
1035
  />
1030
1036
  <Box padding={2} background="warning50" style={{ borderRadius: '4px', marginTop: '8px' }}>
1031
1037
  <Typography variant="pi" textColor="warning700" style={{ fontSize: '11px' }}>
1032
- User will be blocked after {settings.maxFailedLogins} failed attempts
1038
+ {t('settings.security.maxFailed.hint', 'User will be blocked after {count} failed attempts', { count: settings.maxFailedLogins })}
1033
1039
  </Typography>
1034
1040
  </Box>
1035
1041
  </Box>
@@ -1046,9 +1052,9 @@ const SettingsPage = () => {
1046
1052
  <Accordion.Header>
1047
1053
  <Accordion.Trigger
1048
1054
  icon={Mail}
1049
- description="Email alerts for security events"
1055
+ description={t('settings.email.description', 'Email alerts for security events')}
1050
1056
  >
1051
- Email Notifications (Advanced)
1057
+ {t('settings.email.title', 'Email Notifications (Advanced)')}
1052
1058
  </Accordion.Trigger>
1053
1059
  </Accordion.Header>
1054
1060
  <Accordion.Content>
@@ -1057,10 +1063,10 @@ const SettingsPage = () => {
1057
1063
  {/* Email Alerts Toggle */}
1058
1064
  <Box background="neutral100" padding={5} style={{ borderRadius: theme.borderRadius.md, marginBottom: '32px' }}>
1059
1065
  <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '8px', display: 'block', textAlign: 'center', color: theme.colors.neutral[700] }}>
1060
- 📧 EMAIL ALERTS
1066
+ 📧 {t('settings.email.alerts.title', 'EMAIL ALERTS')}
1061
1067
  </Typography>
1062
1068
  <Typography variant="pi" textColor="neutral600" style={{ marginBottom: '20px', display: 'block', textAlign: 'center', fontSize: '12px' }}>
1063
- Send security alerts to users via email
1069
+ {t('settings.email.alerts.subtitle', 'Send security alerts to users via email')}
1064
1070
  </Typography>
1065
1071
  <Grid.Root gap={4}>
1066
1072
  <Grid.Item col={12}>
@@ -1082,10 +1088,10 @@ const SettingsPage = () => {
1082
1088
  textColor={settings.enableEmailAlerts ? 'success700' : 'neutral800'}
1083
1089
  style={{ fontSize: '16px' }}
1084
1090
  >
1085
- Enable Email Alerts
1091
+ {t('settings.email.enable.title', 'Enable Email Alerts')}
1086
1092
  </Typography>
1087
1093
  <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
1088
- Send security alerts for suspicious logins, new locations, and VPN/Proxy usage
1094
+ {t('settings.email.enable.description', 'Send security alerts for suspicious logins, new locations, and VPN/Proxy usage')}
1089
1095
  </Typography>
1090
1096
  </Flex>
1091
1097
  </Flex>
@@ -1098,7 +1104,7 @@ const SettingsPage = () => {
1098
1104
  {settings.enableEmailAlerts && (
1099
1105
  <>
1100
1106
  <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
1101
- ⚙️ ALERT TYPES
1107
+ ⚙️ {t('settings.email.types.title', 'ALERT TYPES')}
1102
1108
  </Typography>
1103
1109
  <Grid.Root gap={4} style={{ marginBottom: '32px' }}>
1104
1110
  <Grid.Item col={4} s={12}>
@@ -1118,7 +1124,7 @@ const SettingsPage = () => {
1118
1124
  onChange={() => handleChange('alertOnSuspiciousLogin', !settings.alertOnSuspiciousLogin)}
1119
1125
  >
1120
1126
  <Typography variant="omega" fontWeight="semiBold" style={{ fontSize: '14px' }}>
1121
- Suspicious Login
1127
+ {t('settings.email.types.suspicious', 'Suspicious Login')}
1122
1128
  </Typography>
1123
1129
  </Checkbox>
1124
1130
  </Box>
@@ -1140,7 +1146,7 @@ const SettingsPage = () => {
1140
1146
  onChange={() => handleChange('alertOnNewLocation', !settings.alertOnNewLocation)}
1141
1147
  >
1142
1148
  <Typography variant="omega" fontWeight="semiBold" style={{ fontSize: '14px' }}>
1143
- New Location
1149
+ {t('settings.email.types.newLocation', 'New Location')}
1144
1150
  </Typography>
1145
1151
  </Checkbox>
1146
1152
  </Box>
@@ -1162,7 +1168,7 @@ const SettingsPage = () => {
1162
1168
  onChange={() => handleChange('alertOnVpnProxy', !settings.alertOnVpnProxy)}
1163
1169
  >
1164
1170
  <Typography variant="omega" fontWeight="semiBold" style={{ fontSize: '14px' }}>
1165
- VPN/Proxy
1171
+ {t('settings.email.types.vpnProxy', 'VPN/Proxy')}
1166
1172
  </Typography>
1167
1173
  </Checkbox>
1168
1174
  </Box>
@@ -1172,18 +1178,18 @@ const SettingsPage = () => {
1172
1178
  {/* Email Templates */}
1173
1179
  <Divider style={{ marginBottom: '24px' }} />
1174
1180
  <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '8px', display: 'block', color: theme.colors.neutral[700] }}>
1175
- EMAIL TEMPLATES
1181
+ {t('settings.email.templates.title', 'EMAIL TEMPLATES')}
1176
1182
  </Typography>
1177
1183
  <Typography variant="pi" textColor="neutral600" style={{ marginBottom: '20px', display: 'block', fontSize: '12px' }}>
1178
- Customize email notification templates with dynamic variables
1184
+ {t('settings.email.templates.subtitle', 'Customize email notification templates with dynamic variables')}
1179
1185
  </Typography>
1180
1186
 
1181
1187
  {/* Template Tabs */}
1182
1188
  <Tabs.Root value={activeTemplateTab} onValueChange={setActiveTemplateTab}>
1183
1189
  <Tabs.List aria-label="Email Templates">
1184
- <Tabs.Trigger value="suspiciousLogin">Suspicious Login</Tabs.Trigger>
1185
- <Tabs.Trigger value="newLocation">New Location</Tabs.Trigger>
1186
- <Tabs.Trigger value="vpnProxy">VPN/Proxy</Tabs.Trigger>
1190
+ <Tabs.Trigger value="suspiciousLogin">{t('settings.email.templates.tab.suspicious', 'Suspicious Login')}</Tabs.Trigger>
1191
+ <Tabs.Trigger value="newLocation">{t('settings.email.templates.tab.newLocation', 'New Location')}</Tabs.Trigger>
1192
+ <Tabs.Trigger value="vpnProxy">{t('settings.email.templates.tab.vpnProxy', 'VPN/Proxy')}</Tabs.Trigger>
1187
1193
  </Tabs.List>
1188
1194
 
1189
1195
  {Object.keys(settings.emailTemplates).map((templateKey) => (
@@ -1192,7 +1198,7 @@ const SettingsPage = () => {
1192
1198
  {/* Subject */}
1193
1199
  <Box style={{ marginBottom: '24px' }}>
1194
1200
  <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
1195
- Email Subject
1201
+ {t('settings.email.templates.subject', 'Email Subject')}
1196
1202
  </Typography>
1197
1203
  <TextInput
1198
1204
  value={settings.emailTemplates[templateKey].subject}
@@ -1201,7 +1207,7 @@ const SettingsPage = () => {
1201
1207
  newTemplates[templateKey].subject = e.target.value;
1202
1208
  handleChange('emailTemplates', newTemplates);
1203
1209
  }}
1204
- placeholder="Enter email subject..."
1210
+ placeholder={t('settings.email.templates.subjectPlaceholder', 'Enter email subject...')}
1205
1211
  />
1206
1212
  </Box>
1207
1213
 
@@ -1215,7 +1221,7 @@ const SettingsPage = () => {
1215
1221
  <Flex alignItems="center" gap={2}>
1216
1222
  <Code style={{ width: '16px', height: '16px', color: theme.colors.primary[600] }} />
1217
1223
  <Typography variant="omega" fontWeight="bold" textColor="primary600">
1218
- Available Variables (click to copy)
1224
+ {t('settings.email.templates.variables', 'Available Variables (click to copy)')}
1219
1225
  </Typography>
1220
1226
  </Flex>
1221
1227
  <Flex gap={2} wrap="wrap">
@@ -1226,7 +1232,7 @@ const SettingsPage = () => {
1226
1232
  variant="tertiary"
1227
1233
  onClick={() => {
1228
1234
  navigator.clipboard.writeText(variable);
1229
- toggleNotification({ type: 'success', message: `${variable} copied!` });
1235
+ toggleNotification({ type: 'success', message: t('notifications.success.variableCopied', '{variable} copied!', { variable }) });
1230
1236
  }}
1231
1237
  style={{
1232
1238
  fontFamily: 'monospace',
@@ -1251,9 +1257,9 @@ const SettingsPage = () => {
1251
1257
  <Flex justifyContent="space-between" alignItems="center" style={{ marginBottom: '16px' }}>
1252
1258
  <Flex alignItems="center" gap={2}>
1253
1259
  <Typography variant="delta" fontWeight="bold" style={{ fontSize: '18px' }}>
1254
- 🎨 HTML Template
1260
+ 🎨 {t('settings.email.templates.html.title', 'HTML Template')}
1255
1261
  </Typography>
1256
- <Badge variant="success">Main Template</Badge>
1262
+ <Badge variant="success">{t('settings.email.templates.html.badge', 'Main Template')}</Badge>
1257
1263
  </Flex>
1258
1264
  <Button
1259
1265
  variant="tertiary"
@@ -1263,14 +1269,14 @@ const SettingsPage = () => {
1263
1269
  const newTemplates = { ...settings.emailTemplates };
1264
1270
  newTemplates[templateKey].html = defaultTemplates[templateKey].html;
1265
1271
  handleChange('emailTemplates', newTemplates);
1266
- toggleNotification({ type: 'success', message: 'Default HTML template loaded!' });
1272
+ toggleNotification({ type: 'success', message: t('notifications.success.defaultLoaded', 'Default template loaded!') });
1267
1273
  }}
1268
1274
  >
1269
- 📋 Load Default
1275
+ 📋 {t('settings.email.templates.html.loadDefault', 'Load Default')}
1270
1276
  </Button>
1271
1277
  </Flex>
1272
1278
  <Typography variant="pi" textColor="neutral600" style={{ marginBottom: '16px', display: 'block', fontSize: '14px' }}>
1273
- HTML template for email notifications. Use variables like <code>{'{{user.email}}'}</code> for dynamic content.
1279
+ {t('settings.email.templates.html.description', 'HTML template for email notifications. Use variables like {{user.email}} for dynamic content.')}
1274
1280
  </Typography>
1275
1281
  <Box
1276
1282
  style={{
@@ -1325,10 +1331,10 @@ const SettingsPage = () => {
1325
1331
  size="S"
1326
1332
  onClick={() => {
1327
1333
  navigator.clipboard.writeText(settings.emailTemplates[templateKey].html);
1328
- toggleNotification({ type: 'success', message: 'HTML template copied!' });
1334
+ toggleNotification({ type: 'success', message: t('notifications.success.htmlCopied', 'HTML template copied!') });
1329
1335
  }}
1330
1336
  >
1331
- 📋 Copy Template
1337
+ 📋 {t('settings.email.templates.html.copy', 'Copy Template')}
1332
1338
  </Button>
1333
1339
  <Button
1334
1340
  variant="tertiary"
@@ -1338,12 +1344,12 @@ const SettingsPage = () => {
1338
1344
  toggleNotification({
1339
1345
  type: validation.isValid ? 'success' : 'warning',
1340
1346
  message: validation.isValid
1341
- ? `✓ Template valid! Found ${validation.foundVars.length}/${validation.totalAvailable} variables.`
1342
- : '[WARNING] No variables found. Add at least one variable.',
1347
+ ? t('notifications.success.validated', 'Template valid! Found {found}/{total} variables.', { found: validation.foundVars.length, total: validation.totalAvailable })
1348
+ : t('notifications.warning.noVariables', '[WARNING] No variables found. Add at least one variable.'),
1343
1349
  });
1344
1350
  }}
1345
1351
  >
1346
- ✓ Validate
1352
+ {t('settings.email.templates.html.validate', 'Validate')}
1347
1353
  </Button>
1348
1354
  <Button
1349
1355
  variant="tertiary"
@@ -1353,11 +1359,11 @@ const SettingsPage = () => {
1353
1359
  const chars = settings.emailTemplates[templateKey].html.length;
1354
1360
  toggleNotification({
1355
1361
  type: 'info',
1356
- message: `Template has ${lines} lines and ${chars} characters`
1362
+ message: t('notifications.info.templateStats', 'Template has {lines} lines and {chars} characters', { lines, chars })
1357
1363
  });
1358
1364
  }}
1359
1365
  >
1360
- ℹ️ Template Info
1366
+ ℹ️ {t('settings.email.templates.html.info', 'Template Info')}
1361
1367
  </Button>
1362
1368
  </Flex>
1363
1369
  </Box>
@@ -1371,9 +1377,9 @@ const SettingsPage = () => {
1371
1377
  <Flex justifyContent="space-between" alignItems="center" style={{ marginBottom: '16px' }}>
1372
1378
  <Flex alignItems="center" gap={2}>
1373
1379
  <Typography variant="delta" fontWeight="bold" style={{ fontSize: '18px' }}>
1374
- 📄 Text Template
1380
+ 📄 {t('settings.email.templates.text.title', 'Text Template')}
1375
1381
  </Typography>
1376
- <Badge variant="secondary">Fallback</Badge>
1382
+ <Badge variant="secondary">{t('settings.email.templates.text.badge', 'Fallback')}</Badge>
1377
1383
  </Flex>
1378
1384
  <Button
1379
1385
  variant="tertiary"
@@ -1383,14 +1389,14 @@ const SettingsPage = () => {
1383
1389
  const newTemplates = { ...settings.emailTemplates };
1384
1390
  newTemplates[templateKey].text = defaultTemplates[templateKey].text;
1385
1391
  handleChange('emailTemplates', newTemplates);
1386
- toggleNotification({ type: 'success', message: 'Default text template loaded!' });
1392
+ toggleNotification({ type: 'success', message: t('notifications.success.defaultLoaded', 'Default template loaded!') });
1387
1393
  }}
1388
1394
  >
1389
- 📋 Load Default
1395
+ 📋 {t('settings.email.templates.text.loadDefault', 'Load Default')}
1390
1396
  </Button>
1391
1397
  </Flex>
1392
1398
  <Typography variant="pi" textColor="neutral600" style={{ marginBottom: '16px', display: 'block', fontSize: '14px' }}>
1393
- Plain text version (no HTML) as fallback for older email clients
1399
+ {t('settings.email.templates.text.description', 'Plain text version (no HTML) as fallback for older email clients')}
1394
1400
  </Typography>
1395
1401
  <Box
1396
1402
  style={{
@@ -1445,10 +1451,10 @@ const SettingsPage = () => {
1445
1451
  size="S"
1446
1452
  onClick={() => {
1447
1453
  navigator.clipboard.writeText(settings.emailTemplates[templateKey].text);
1448
- toggleNotification({ type: 'success', message: 'Text template copied!' });
1454
+ toggleNotification({ type: 'success', message: t('notifications.success.textCopied', 'Text template copied!') });
1449
1455
  }}
1450
1456
  >
1451
- 📋 Copy Template
1457
+ 📋 {t('settings.email.templates.text.copy', 'Copy Template')}
1452
1458
  </Button>
1453
1459
  </Flex>
1454
1460
  </Box>
@@ -1470,9 +1476,9 @@ const SettingsPage = () => {
1470
1476
  <Accordion.Header>
1471
1477
  <Accordion.Trigger
1472
1478
  icon={Code}
1473
- description="Discord & Slack integration"
1479
+ description={t('settings.webhooks.description', 'Discord & Slack integration')}
1474
1480
  >
1475
- Webhook Integration (Advanced)
1481
+ {t('settings.webhooks.title', 'Webhook Integration (Advanced)')}
1476
1482
  </Accordion.Trigger>
1477
1483
  </Accordion.Header>
1478
1484
  <Accordion.Content>
@@ -1500,10 +1506,10 @@ const SettingsPage = () => {
1500
1506
  textColor={settings.enableWebhooks ? 'success700' : 'neutral800'}
1501
1507
  style={{ fontSize: '16px' }}
1502
1508
  >
1503
- Enable Webhooks
1509
+ {t('settings.webhooks.enable.title', 'Enable Webhooks')}
1504
1510
  </Typography>
1505
1511
  <Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
1506
- Send session events to Discord, Slack, or custom endpoints
1512
+ {t('settings.webhooks.enable.description', 'Send session events to Discord, Slack, or custom endpoints')}
1507
1513
  </Typography>
1508
1514
  </Flex>
1509
1515
  </Flex>
@@ -1518,7 +1524,7 @@ const SettingsPage = () => {
1518
1524
  <Grid.Item col={12}>
1519
1525
  <Box>
1520
1526
  <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '12px', display: 'block' }}>
1521
- 🔗 Discord Webhook URL
1527
+ 🔗 {t('settings.webhooks.discord.title', 'Discord Webhook URL')}
1522
1528
  </Typography>
1523
1529
  <Box
1524
1530
  style={{
@@ -1529,7 +1535,7 @@ const SettingsPage = () => {
1529
1535
  }}
1530
1536
  >
1531
1537
  <textarea
1532
- placeholder="https://discord.com/api/webhooks/123456789/abcdefghijklmnopqrstuvwxyz..."
1538
+ placeholder={t('settings.webhooks.discord.placeholder', 'https://discord.com/api/webhooks/123456789/abcdefghijklmnopqrstuvwxyz...')}
1533
1539
  value={settings.discordWebhookUrl}
1534
1540
  onChange={(e) => handleChange('discordWebhookUrl', e.target.value)}
1535
1541
  rows={3}
@@ -1550,11 +1556,11 @@ const SettingsPage = () => {
1550
1556
  </Box>
1551
1557
  <Flex justifyContent="space-between" alignItems="center" style={{ marginTop: '10px' }}>
1552
1558
  <Typography variant="pi" textColor="neutral600" style={{ fontSize: '12px' }}>
1553
- Optional: Post session alerts to your Discord channel
1559
+ {t('settings.webhooks.discord.hint', 'Optional: Post session alerts to your Discord channel')}
1554
1560
  </Typography>
1555
1561
  {settings.discordWebhookUrl && (
1556
1562
  <Typography variant="pi" textColor="primary600" style={{ fontSize: '11px', fontFamily: 'monospace' }}>
1557
- {settings.discordWebhookUrl.length} characters
1563
+ {t('settings.webhooks.characters', '{count} characters', { count: settings.discordWebhookUrl.length })}
1558
1564
  </Typography>
1559
1565
  )}
1560
1566
  </Flex>
@@ -1564,7 +1570,7 @@ const SettingsPage = () => {
1564
1570
  <Grid.Item col={12}>
1565
1571
  <Box>
1566
1572
  <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '12px', display: 'block' }}>
1567
- 💬 Slack Webhook URL
1573
+ 💬 {t('settings.webhooks.slack.title', 'Slack Webhook URL')}
1568
1574
  </Typography>
1569
1575
  <Box
1570
1576
  style={{
@@ -1575,7 +1581,7 @@ const SettingsPage = () => {
1575
1581
  }}
1576
1582
  >
1577
1583
  <textarea
1578
- placeholder="https://hooks.slack.com/services/XXXX/XXXX/XXXX"
1584
+ placeholder={t('settings.webhooks.slack.placeholder', 'https://hooks.slack.com/services/XXXX/XXXX/XXXX')}
1579
1585
  value={settings.slackWebhookUrl}
1580
1586
  onChange={(e) => handleChange('slackWebhookUrl', e.target.value)}
1581
1587
  rows={3}
@@ -1596,11 +1602,11 @@ const SettingsPage = () => {
1596
1602
  </Box>
1597
1603
  <Flex justifyContent="space-between" alignItems="center" style={{ marginTop: '10px' }}>
1598
1604
  <Typography variant="pi" textColor="neutral600" style={{ fontSize: '12px' }}>
1599
- Optional: Post session alerts to your Slack workspace
1605
+ {t('settings.webhooks.slack.hint', 'Optional: Post session alerts to your Slack workspace')}
1600
1606
  </Typography>
1601
1607
  {settings.slackWebhookUrl && (
1602
1608
  <Typography variant="pi" textColor="primary600" style={{ fontSize: '11px', fontFamily: 'monospace' }}>
1603
- {settings.slackWebhookUrl.length} characters
1609
+ {t('settings.webhooks.characters', '{count} characters', { count: settings.slackWebhookUrl.length })}
1604
1610
  </Typography>
1605
1611
  )}
1606
1612
  </Flex>
@@ -1622,12 +1628,10 @@ const SettingsPage = () => {
1622
1628
  <Check style={{ width: '20px', height: '20px', color: theme.colors.success[600], flexShrink: 0, marginTop: '2px' }} />
1623
1629
  <Box style={{ flex: 1 }}>
1624
1630
  <Typography variant="omega" fontWeight="bold" style={{ marginBottom: '8px', display: 'block', color: theme.colors.primary[700] }}>
1625
- Database-Backed Settings
1631
+ {t('settings.footer.title', 'Database-Backed Settings')}
1626
1632
  </Typography>
1627
1633
  <Typography variant="pi" textColor="primary700" style={{ fontSize: '13px', lineHeight: '1.8' }}>
1628
- All settings are stored in your Strapi database and shared across all admin users.
1629
- Changes take effect immediately - no server restart required!
1630
- Email templates, webhooks, and security options are all managed from this interface.
1634
+ {t('settings.footer.description', 'All settings are stored in your Strapi database and shared across all admin users. Changes take effect immediately - no server restart required! Email templates, webhooks, and security options are all managed from this interface.')}
1631
1635
  </Typography>
1632
1636
  </Box>
1633
1637
  </Flex>