@carlonicora/nextjs-jsonapi 4.1.1 → 4.2.0

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 (230) hide show
  1. package/dist/{AssistantMessageInterface-CubxuBIJ.d.ts → AssistantMessageInterface-DHJJO_Z_.d.ts} +1 -1
  2. package/dist/{AssistantMessageInterface-sHjI2zPN.d.mts → AssistantMessageInterface-S7oyERTT.d.mts} +1 -1
  3. package/dist/{BlockNoteEditor-VOYKZOP5.mjs → BlockNoteEditor-HXAHDJA7.mjs} +34 -27
  4. package/dist/BlockNoteEditor-HXAHDJA7.mjs.map +1 -0
  5. package/dist/{BlockNoteEditor-BENYTTCZ.js → BlockNoteEditor-LLCZDFLZ.js} +50 -43
  6. package/dist/BlockNoteEditor-LLCZDFLZ.js.map +1 -0
  7. package/dist/{BlockNoteViewer-USLR4Y7C.js → BlockNoteViewer-77CCTC2U.js} +3 -3
  8. package/dist/{BlockNoteViewer-USLR4Y7C.js.map → BlockNoteViewer-77CCTC2U.js.map} +1 -1
  9. package/dist/{BlockNoteViewer-Y2ZM4ESA.mjs → BlockNoteViewer-MDPO6BXN.mjs} +2 -2
  10. package/dist/{auth.interface-Df4vXbNz.d.mts → auth.interface-CeFCz9NU.d.mts} +1 -1
  11. package/dist/{auth.interface-BzI4onI_.d.ts → auth.interface-D1R7VVky.d.ts} +1 -1
  12. package/dist/billing/index.d.mts +40 -6
  13. package/dist/billing/index.d.ts +40 -6
  14. package/dist/billing/index.js +806 -774
  15. package/dist/billing/index.js.map +1 -1
  16. package/dist/billing/index.mjs +494 -462
  17. package/dist/billing/index.mjs.map +1 -1
  18. package/dist/{chunk-6ISF5KNQ.mjs → chunk-2VXDZRYY.mjs} +1103 -816
  19. package/dist/chunk-2VXDZRYY.mjs.map +1 -0
  20. package/dist/{chunk-4BZJ4LAB.js → chunk-4ORLPEK7.js} +136 -104
  21. package/dist/chunk-4ORLPEK7.js.map +1 -0
  22. package/dist/{chunk-ZZ6E33FO.mjs → chunk-6DRSSW5O.mjs} +36 -4
  23. package/dist/chunk-6DRSSW5O.mjs.map +1 -0
  24. package/dist/{chunk-KS6WZP2E.js → chunk-6SMJRAE3.js} +4 -4
  25. package/dist/{chunk-KS6WZP2E.js.map → chunk-6SMJRAE3.js.map} +1 -1
  26. package/dist/{chunk-WQ77CUUZ.js → chunk-GPXYQMOZ.js} +7 -7
  27. package/dist/{chunk-WQ77CUUZ.js.map → chunk-GPXYQMOZ.js.map} +1 -1
  28. package/dist/{chunk-46S3KPLU.mjs → chunk-ILFFC6BG.mjs} +2 -2
  29. package/dist/{chunk-HZVVRIJQ.mjs → chunk-IUS2RHCX.mjs} +2 -2
  30. package/dist/{chunk-F7RNBGHS.js → chunk-KJMMBUOA.js} +3 -3
  31. package/dist/{chunk-F7RNBGHS.js.map → chunk-KJMMBUOA.js.map} +1 -1
  32. package/dist/{chunk-ZO3QQ5CQ.mjs → chunk-MQDR6MEE.mjs} +2 -2
  33. package/dist/{chunk-ORU7KLCY.js → chunk-VDVIUDY5.js} +1445 -1158
  34. package/dist/chunk-VDVIUDY5.js.map +1 -0
  35. package/dist/client/index.d.mts +20 -6
  36. package/dist/client/index.d.ts +20 -6
  37. package/dist/client/index.js +6 -4
  38. package/dist/client/index.js.map +1 -1
  39. package/dist/client/index.mjs +5 -3
  40. package/dist/components/index.d.mts +59 -21
  41. package/dist/components/index.d.ts +59 -21
  42. package/dist/components/index.js +4 -4
  43. package/dist/components/index.mjs +3 -3
  44. package/dist/{config-CEAq4Ozy.d.mts → config-BP2vyddw.d.mts} +5 -0
  45. package/dist/{config-DydDHCfb.d.ts → config-BhW2QrRD.d.ts} +5 -0
  46. package/dist/contexts/index.d.mts +4 -4
  47. package/dist/contexts/index.d.ts +4 -4
  48. package/dist/contexts/index.js +4 -4
  49. package/dist/contexts/index.mjs +3 -3
  50. package/dist/core/index.d.mts +17 -9
  51. package/dist/core/index.d.ts +17 -9
  52. package/dist/core/index.js +4 -2
  53. package/dist/core/index.js.map +1 -1
  54. package/dist/core/index.mjs +3 -1
  55. package/dist/features/analytics/index.d.mts +1 -1
  56. package/dist/features/analytics/index.d.ts +1 -1
  57. package/dist/features/analytics/index.js +122 -122
  58. package/dist/features/analytics/index.mjs +4 -4
  59. package/dist/features/analytics/server.js +4 -4
  60. package/dist/features/analytics/server.mjs +2 -2
  61. package/dist/features/help/index.d.mts +1 -1
  62. package/dist/features/help/index.d.ts +1 -1
  63. package/dist/features/help/index.js +42 -40
  64. package/dist/features/help/index.js.map +1 -1
  65. package/dist/features/help/index.mjs +8 -6
  66. package/dist/features/help/index.mjs.map +1 -1
  67. package/dist/features/tokenusage/index.js +125 -125
  68. package/dist/features/tokenusage/index.mjs +4 -4
  69. package/dist/index.d.mts +7 -7
  70. package/dist/index.d.ts +7 -7
  71. package/dist/index.js +5 -3
  72. package/dist/index.js.map +1 -1
  73. package/dist/index.mjs +4 -2
  74. package/dist/{notification.interface-P_pNfzZQ.d.mts → notification.interface-BB6GPpZY.d.mts} +1 -1
  75. package/dist/{notification.interface-BNCvnVCW.d.ts → notification.interface-CmAGrijJ.d.ts} +1 -1
  76. package/dist/{s3.service-CbUa6a-8.d.ts → s3.service-C-4YpEMu.d.ts} +7 -3
  77. package/dist/{s3.service-DAp8FbuG.d.mts → s3.service-dVj-nxMl.d.mts} +7 -3
  78. package/dist/server/index.d.mts +4 -4
  79. package/dist/server/index.d.ts +4 -4
  80. package/dist/server/index.js +3 -3
  81. package/dist/server/index.mjs +2 -2
  82. package/dist/{useSocket-BHVIyigI.d.ts → useSocket-B0H9pmmH.d.ts} +1 -1
  83. package/dist/{useSocket-B9gTbuCj.d.mts → useSocket-B5RQb6v2.d.mts} +1 -1
  84. package/dist/{user.interface-CrmwJLYW.d.ts → user.interface-C0ZdsBZj.d.ts} +2 -0
  85. package/dist/{user.interface-CSaGWFbg.d.mts → user.interface-CIWGlbqM.d.mts} +2 -0
  86. package/package.json +1 -1
  87. package/scripts/vitest-slow-tests.reporter.spec.ts +54 -0
  88. package/src/components/containers/RoundPageContainer.tsx +3 -1
  89. package/src/components/containers/RoundPageContainerTitle.tsx +5 -2
  90. package/src/components/editors/BlockNoteEditor.tsx +31 -15
  91. package/src/components/editors/BlockNoteEditorAiActions.tsx +11 -7
  92. package/src/components/editors/__tests__/BlockNoteEditorAiActions.test.ts +10 -3
  93. package/src/components/errors/ErrorDetails.tsx +5 -2
  94. package/src/components/forms/CommonAssociationForm.tsx +1 -1
  95. package/src/components/forms/EntityMultiSelector.tsx +6 -2
  96. package/src/components/forms/EntitySelector.tsx +4 -1
  97. package/src/components/forms/FileUploader.tsx +3 -2
  98. package/src/components/forms/MultiFileUploader.tsx +3 -1
  99. package/src/components/navigations/Breadcrumb.tsx +3 -2
  100. package/src/components/navigations/Header.tsx +3 -1
  101. package/src/components/navigations/MobileNavigationBar.tsx +3 -1
  102. package/src/components/navigations/__tests__/Header.spec.tsx +2 -2
  103. package/src/components/tables/cells/cell.id.tsx +13 -2
  104. package/src/core/abstracts/AbstractService.ts +3 -3
  105. package/src/core/abstracts/ClientAbstractService.ts +3 -2
  106. package/src/features/assistant-action/components/ApprovalActionCard.tsx +74 -5
  107. package/src/features/assistant-action/components/__tests__/ApprovalActionCard.spec.tsx +81 -0
  108. package/src/features/assistant-message/components/parts/RelevanceMeter.tsx +4 -1
  109. package/src/features/assistant-message/components/parts/tabs/UsersTab.tsx +2 -1
  110. package/src/features/auth/components/buttons/GoogleSignInButton.tsx +4 -1
  111. package/src/features/auth/components/details/LandingComponent.tsx +1 -1
  112. package/src/features/auth/components/forms/ForgotPassword.tsx +1 -1
  113. package/src/features/auth/components/forms/Login.tsx +1 -1
  114. package/src/features/auth/components/forms/Register.tsx +1 -1
  115. package/src/features/auth/components/forms/ResetPassword.tsx +2 -2
  116. package/src/features/auth/components/two-factor/BackupCodesDialog.tsx +1 -1
  117. package/src/features/auth/components/two-factor/PasskeyButton.tsx +1 -1
  118. package/src/features/billing/components/cards/BillingUsageSummaryCard.tsx +21 -11
  119. package/src/features/billing/components/cards/CustomerInfoCard.tsx +16 -13
  120. package/src/features/billing/components/cards/InvoicesSummaryCard.tsx +33 -21
  121. package/src/features/billing/components/cards/PaymentMethodSummaryCard.tsx +20 -10
  122. package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +35 -22
  123. package/src/features/billing/components/containers/BillingDashboardContainer.tsx +24 -24
  124. package/src/features/billing/components/utils/__tests__/billing-formatters.spec.ts +63 -0
  125. package/src/features/billing/components/utils/currency.ts +67 -10
  126. package/src/features/billing/components/utils/date.ts +9 -4
  127. package/src/features/billing/components/utils/format-options.ts +20 -0
  128. package/src/features/billing/components/utils/index.ts +3 -1
  129. package/src/features/billing/components/utils/useBillingFormatOptions.ts +11 -0
  130. package/src/features/billing/components/widgets/BillingAlertBanner.tsx +8 -9
  131. package/src/features/billing/contexts/PriceContext.tsx +18 -6
  132. package/src/features/billing/i18n-keys.ts +212 -0
  133. package/src/features/billing/stripe-customer/components/containers/PaymentMethodsContainer.tsx +8 -8
  134. package/src/features/billing/stripe-customer/components/details/PaymentMethodCard.tsx +11 -9
  135. package/src/features/billing/stripe-customer/components/forms/PaymentMethodEditor.tsx +4 -4
  136. package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +9 -7
  137. package/src/features/billing/stripe-invoice/components/containers/InvoicesContainer.tsx +10 -8
  138. package/src/features/billing/stripe-invoice/components/details/InvoiceDetails.tsx +34 -29
  139. package/src/features/billing/stripe-invoice/components/lists/InvoicesList.tsx +12 -9
  140. package/src/features/billing/stripe-invoice/components/widgets/InvoiceStatusBadge.tsx +9 -6
  141. package/src/features/billing/stripe-price/components/details/PriceDetails.tsx +4 -3
  142. package/src/features/billing/stripe-price/components/forms/PriceArchiver.tsx +3 -1
  143. package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +3 -1
  144. package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +6 -5
  145. package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +12 -7
  146. package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.service.ts +3 -1
  147. package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +10 -6
  148. package/src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx +44 -29
  149. package/src/features/billing/stripe-subscription/components/forms/CancelSubscriptionDialog.tsx +14 -11
  150. package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +20 -17
  151. package/src/features/billing/stripe-subscription/components/widgets/IntervalToggle.tsx +4 -2
  152. package/src/features/billing/stripe-subscription/components/widgets/PricingCard.tsx +16 -7
  153. package/src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx +3 -1
  154. package/src/features/billing/stripe-subscription/components/widgets/ProductPricingRow.tsx +3 -1
  155. package/src/features/billing/stripe-subscription/components/widgets/ProrationPreview.tsx +12 -7
  156. package/src/features/billing/stripe-subscription/components/widgets/SubscriptionConfirmation.tsx +11 -8
  157. package/src/features/billing/stripe-subscription/components/widgets/SubscriptionStatusBadge.tsx +13 -10
  158. package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +8 -6
  159. package/src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx +7 -4
  160. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +4 -2
  161. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx +3 -1
  162. package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +37 -28
  163. package/src/features/billing/stripe-subscription/hooks/useConfirmSubscriptionPayment.ts +9 -7
  164. package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +20 -9
  165. package/src/features/billing/stripe-usage/components/containers/UsageContainer.tsx +6 -6
  166. package/src/features/billing/stripe-usage/components/details/UsageSummaryCard.tsx +24 -9
  167. package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +16 -12
  168. package/src/features/company/components/details/TokenStatusIndicator.tsx +7 -5
  169. package/src/features/company/components/forms/CompanyEditor.tsx +2 -2
  170. package/src/features/help/components/HelpArticleBody.tsx +1 -1
  171. package/src/features/help/components/HelpHeader.tsx +1 -1
  172. package/src/features/help/components/HelpSideNav.tsx +1 -1
  173. package/src/features/help/i18n-keys.ts +2 -0
  174. package/src/features/notification/components/common/NotificationErrorBoundary.tsx +17 -12
  175. package/src/features/notification/components/containers/NotificationsListContainer.tsx +4 -2
  176. package/src/features/notification/components/notifications/Notification.tsx +6 -3
  177. package/src/features/notification/contexts/NotificationContext.tsx +9 -4
  178. package/src/features/notification/contexts/__tests__/NotificationContext.spec.tsx +13 -0
  179. package/src/features/notification/hooks/useNotificationTableStructure.tsx +4 -2
  180. package/src/features/oauth/components/OAuthClientCard.tsx +13 -7
  181. package/src/features/oauth/components/OAuthClientDetail.tsx +34 -30
  182. package/src/features/oauth/components/OAuthClientForm.tsx +24 -24
  183. package/src/features/oauth/components/OAuthClientList.tsx +10 -6
  184. package/src/features/oauth/components/OAuthClientSecretDisplay.tsx +10 -8
  185. package/src/features/oauth/components/OAuthRedirectUriInput.tsx +9 -13
  186. package/src/features/oauth/components/OAuthScopeSelector.tsx +15 -6
  187. package/src/features/oauth/components/__tests__/OAuthClientForm.spec.tsx +11 -9
  188. package/src/features/oauth/components/consent/OAuthScopeList.tsx +4 -2
  189. package/src/features/oauth/hooks/__tests__/useOAuthScopeText.spec.ts +30 -0
  190. package/src/features/oauth/hooks/index.ts +1 -0
  191. package/src/features/oauth/hooks/useOAuthClient.ts +11 -5
  192. package/src/features/oauth/hooks/useOAuthClients.ts +9 -3
  193. package/src/features/oauth/hooks/useOAuthConsent.ts +11 -5
  194. package/src/features/oauth/hooks/useOAuthScopeText.ts +33 -0
  195. package/src/features/rbac/components/RbacByRoleContainer.tsx +7 -6
  196. package/src/features/rbac/components/RbacContainer.tsx +7 -6
  197. package/src/features/rbac/contexts/RbacContext.tsx +3 -3
  198. package/src/features/user/components/containers/UserContainer.tsx +3 -1
  199. package/src/features/user/data/__tests__/user.test.ts +66 -0
  200. package/src/features/user/data/user.interface.ts +2 -0
  201. package/src/features/user/data/user.service.ts +9 -0
  202. package/src/features/user/data/user.ts +7 -0
  203. package/src/features/waitlist/components/forms/WaitlistForm.tsx +1 -1
  204. package/src/features/waitlist/components/lists/WaitlistList.tsx +1 -1
  205. package/src/i18n/__tests__/config.test.ts +21 -1
  206. package/src/i18n/config.ts +13 -0
  207. package/src/i18n/index.ts +1 -0
  208. package/src/shadcnui/custom/multi-select.tsx +47 -14
  209. package/src/shadcnui/custom/multiple-selector.tsx +14 -2
  210. package/src/shadcnui/ui/__tests__/use-ui-text.test.tsx +76 -0
  211. package/src/shadcnui/ui/breadcrumb.tsx +9 -2
  212. package/src/shadcnui/ui/carousel.tsx +15 -4
  213. package/src/shadcnui/ui/dialog.tsx +15 -2
  214. package/src/shadcnui/ui/sheet.tsx +6 -1
  215. package/src/shadcnui/ui/sidebar.tsx +33 -7
  216. package/src/shadcnui/ui/spinner.tsx +6 -1
  217. package/src/shadcnui/ui/use-ui-text.ts +49 -0
  218. package/src/utils/__tests__/date-formatter.test.ts +24 -1
  219. package/src/utils/date-formatter.ts +16 -0
  220. package/src/utils/index.ts +1 -1
  221. package/dist/BlockNoteEditor-BENYTTCZ.js.map +0 -1
  222. package/dist/BlockNoteEditor-VOYKZOP5.mjs.map +0 -1
  223. package/dist/chunk-4BZJ4LAB.js.map +0 -1
  224. package/dist/chunk-6ISF5KNQ.mjs.map +0 -1
  225. package/dist/chunk-ORU7KLCY.js.map +0 -1
  226. package/dist/chunk-ZZ6E33FO.mjs.map +0 -1
  227. /package/dist/{BlockNoteViewer-Y2ZM4ESA.mjs.map → BlockNoteViewer-MDPO6BXN.mjs.map} +0 -0
  228. /package/dist/{chunk-46S3KPLU.mjs.map → chunk-ILFFC6BG.mjs.map} +0 -0
  229. /package/dist/{chunk-HZVVRIJQ.mjs.map → chunk-IUS2RHCX.mjs.map} +0 -0
  230. /package/dist/{chunk-ZO3QQ5CQ.mjs.map → chunk-MQDR6MEE.mjs.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { useState, useCallback } from "react";
4
- import { format } from "date-fns";
5
5
  import { Copy, Check, RefreshCw, Pencil, Trash2, ExternalLink } from "lucide-react";
6
6
  import {
7
7
  Badge,
@@ -23,7 +23,9 @@ import {
23
23
  AlertDialogHeader,
24
24
  AlertDialogTitle,
25
25
  } from "../../../shadcnui";
26
- import { OAuthClientInterface, OAUTH_SCOPE_DISPLAY } from "../interfaces/oauth.interface";
26
+ import { useI18nLocale } from "../../../i18n";
27
+ import { useOAuthScopeText } from "../hooks/useOAuthScopeText";
28
+ import { OAuthClientInterface } from "../interfaces/oauth.interface";
27
29
 
28
30
  export interface OAuthClientDetailProps {
29
31
  /** The OAuth client to display */
@@ -48,6 +50,9 @@ export function OAuthClientDetail({
48
50
  onDelete,
49
51
  onRegenerateSecret,
50
52
  }: OAuthClientDetailProps) {
53
+ const t = useTranslations();
54
+ const locale = useI18nLocale();
55
+ const scopeText = useOAuthScopeText();
51
56
  const [copiedField, setCopiedField] = useState<string | null>(null);
52
57
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
53
58
  const [showRegenerateConfirm, setShowRegenerateConfirm] = useState(false);
@@ -86,7 +91,9 @@ export function OAuthClientDetail({
86
91
  }
87
92
  }, [onRegenerateSecret]);
88
93
 
89
- const createdDate = client.createdAt ? format(new Date(client.createdAt), "MMMM d, yyyy") : "Unknown";
94
+ const createdDate = client.createdAt
95
+ ? new Date(client.createdAt).toLocaleDateString(locale, { dateStyle: "long" })
96
+ : t("oauth.ui.card.unknown");
90
97
 
91
98
  return (
92
99
  <>
@@ -99,23 +106,25 @@ export function OAuthClientDetail({
99
106
  </div>
100
107
  <div className="flex items-center gap-2">
101
108
  <Badge variant={client.isActive ? "default" : "secondary"}>
102
- {client.isActive ? "Active" : "Inactive"}
109
+ {client.isActive ? t("oauth.ui.card.active") : t("oauth.ui.card.inactive")}
110
+ </Badge>
111
+ <Badge variant="outline">
112
+ {client.isConfidential ? t("oauth.ui.card.confidential") : t("oauth.ui.card.public")}
103
113
  </Badge>
104
- <Badge variant="outline">{client.isConfidential ? "Confidential" : "Public"}</Badge>
105
114
  </div>
106
115
  </div>
107
116
  </CardHeader>
108
117
  <CardContent className="space-y-6">
109
118
  {/* Client ID */}
110
119
  <div className="space-y-2">
111
- <Label>Client ID</Label>
120
+ <Label>{t("oauth.ui.detail.client_id")}</Label>
112
121
  <div className="flex gap-2">
113
122
  <Input value={client.clientId} readOnly className="tabular-nums" />
114
123
  <Button
115
124
  variant="outline"
116
125
  size="icon"
117
126
  onClick={() => copyToClipboard(client.clientId, "clientId")}
118
- title="Copy Client ID"
127
+ title={t("oauth.ui.detail.copy_client_id")}
119
128
  >
120
129
  {copiedField === "clientId" ? <Check className="text-success h-4 w-4" /> : <Copy className="h-4 w-4" />}
121
130
  </Button>
@@ -124,7 +133,7 @@ export function OAuthClientDetail({
124
133
 
125
134
  {/* Client Secret */}
126
135
  <div className="space-y-2">
127
- <Label>Client Secret</Label>
136
+ <Label>{t("oauth.ui.detail.client_secret")}</Label>
128
137
  <div className="flex gap-2">
129
138
  <Input value="••••••••••••••••••••••••••••••••" readOnly className="tabular-nums" />
130
139
  {onRegenerateSecret && (
@@ -132,23 +141,21 @@ export function OAuthClientDetail({
132
141
  variant="outline"
133
142
  size="icon"
134
143
  onClick={() => setShowRegenerateConfirm(true)}
135
- title="Regenerate Secret"
144
+ title={t("oauth.ui.detail.regenerate_secret")}
136
145
  disabled={isLoading}
137
146
  >
138
147
  <RefreshCw className="h-4 w-4" />
139
148
  </Button>
140
149
  )}
141
150
  </div>
142
- <p className="text-xs text-muted-foreground">
143
- Regenerating will invalidate the current secret and all existing tokens.
144
- </p>
151
+ <p className="text-xs text-muted-foreground">{t("oauth.ui.detail.regenerate_help")}</p>
145
152
  </div>
146
153
 
147
154
  <Separator />
148
155
 
149
156
  {/* Redirect URIs */}
150
157
  <div className="space-y-2">
151
- <Label>Redirect URIs</Label>
158
+ <Label>{t("oauth.ui.redirect_uris.label")}</Label>
152
159
  <ul className="space-y-1">
153
160
  {client.redirectUris.map((uri, index) => (
154
161
  <li key={index} className="flex items-center gap-2 text-sm tabular-nums">
@@ -161,11 +168,11 @@ export function OAuthClientDetail({
161
168
 
162
169
  {/* Scopes */}
163
170
  <div className="space-y-2">
164
- <Label>Allowed Scopes</Label>
171
+ <Label>{t("oauth.ui.scope_selector.label")}</Label>
165
172
  <div className="flex flex-wrap gap-2">
166
173
  {client.allowedScopes.map((scope) => (
167
174
  <Badge key={scope} variant="secondary">
168
- {OAUTH_SCOPE_DISPLAY[scope]?.name || scope}
175
+ {scopeText(scope).name}
169
176
  </Badge>
170
177
  ))}
171
178
  </div>
@@ -173,7 +180,7 @@ export function OAuthClientDetail({
173
180
 
174
181
  {/* Grant Types */}
175
182
  <div className="space-y-2">
176
- <Label>Grant Types</Label>
183
+ <Label>{t("oauth.ui.detail.grant_types")}</Label>
177
184
  <div className="flex flex-wrap gap-2">
178
185
  {client.allowedGrantTypes.map((grant) => (
179
186
  <Badge key={grant} variant="outline">
@@ -187,7 +194,7 @@ export function OAuthClientDetail({
187
194
 
188
195
  {/* Metadata */}
189
196
  <div className="flex flex-wrap gap-x-6 gap-y-2 text-sm text-muted-foreground">
190
- <span>Created: {createdDate}</span>
197
+ <span>{t("oauth.ui.detail.created", { date: createdDate })}</span>
191
198
  </div>
192
199
 
193
200
  {/* Actions */}
@@ -195,13 +202,13 @@ export function OAuthClientDetail({
195
202
  {onEdit && (
196
203
  <Button variant="outline" onClick={onEdit} disabled={isLoading}>
197
204
  <Pencil className="h-4 w-4 me-2" />
198
- Edit
205
+ {t("ui.buttons.edit")}
199
206
  </Button>
200
207
  )}
201
208
  {onDelete && (
202
209
  <Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={isLoading}>
203
210
  <Trash2 className="h-4 w-4 me-2" />
204
- Delete
211
+ {t("ui.buttons.delete")}
205
212
  </Button>
206
213
  )}
207
214
  </div>
@@ -212,19 +219,19 @@ export function OAuthClientDetail({
212
219
  <AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
213
220
  <AlertDialogContent>
214
221
  <AlertDialogHeader>
215
- <AlertDialogTitle>Delete OAuth Application?</AlertDialogTitle>
222
+ <AlertDialogTitle>{t("oauth.settings.delete_title")}</AlertDialogTitle>
216
223
  <AlertDialogDescription>
217
- This will permanently delete "{client.name}" and revoke all access tokens. This action cannot be undone.
224
+ {t("oauth.settings.delete_description", { name: client.name })}
218
225
  </AlertDialogDescription>
219
226
  </AlertDialogHeader>
220
227
  <AlertDialogFooter>
221
- <AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
228
+ <AlertDialogCancel disabled={isDeleting}>{t("ui.buttons.cancel")}</AlertDialogCancel>
222
229
  <AlertDialogAction
223
230
  onClick={handleDelete}
224
231
  disabled={isDeleting}
225
232
  className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
226
233
  >
227
- {isDeleting ? "Deleting..." : "Delete"}
234
+ {isDeleting ? t("oauth.ui.detail.deleting") : t("ui.buttons.delete")}
228
235
  </AlertDialogAction>
229
236
  </AlertDialogFooter>
230
237
  </AlertDialogContent>
@@ -234,16 +241,13 @@ export function OAuthClientDetail({
234
241
  <AlertDialog open={showRegenerateConfirm} onOpenChange={setShowRegenerateConfirm}>
235
242
  <AlertDialogContent>
236
243
  <AlertDialogHeader>
237
- <AlertDialogTitle>Regenerate Client Secret?</AlertDialogTitle>
238
- <AlertDialogDescription>
239
- This will generate a new client secret and invalidate the old one. All existing tokens will be revoked.
240
- You will need to update your application with the new secret.
241
- </AlertDialogDescription>
244
+ <AlertDialogTitle>{t("oauth.ui.detail.regenerate_title")}</AlertDialogTitle>
245
+ <AlertDialogDescription>{t("oauth.ui.detail.regenerate_description")}</AlertDialogDescription>
242
246
  </AlertDialogHeader>
243
247
  <AlertDialogFooter>
244
- <AlertDialogCancel disabled={isRegenerating}>Cancel</AlertDialogCancel>
248
+ <AlertDialogCancel disabled={isRegenerating}>{t("ui.buttons.cancel")}</AlertDialogCancel>
245
249
  <AlertDialogAction onClick={handleRegenerateSecret} disabled={isRegenerating}>
246
- {isRegenerating ? "Regenerating..." : "Regenerate"}
250
+ {isRegenerating ? t("oauth.ui.detail.regenerating") : t("oauth.ui.detail.regenerate")}
247
251
  </AlertDialogAction>
248
252
  </AlertDialogFooter>
249
253
  </AlertDialogContent>
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { useState, useCallback } from "react";
4
5
  import {
5
6
  Button,
@@ -64,6 +65,7 @@ export function OAuthClientForm({
64
65
  availableScopes,
65
66
  defaultScopes = [],
66
67
  }: OAuthClientFormProps) {
68
+ const t = useTranslations();
67
69
  const isEditMode = !!client;
68
70
 
69
71
  const [formState, setFormState] = useState<FormState>({
@@ -80,21 +82,21 @@ export function OAuthClientForm({
80
82
  const newErrors: FormErrors = {};
81
83
 
82
84
  if (!formState.name.trim()) {
83
- newErrors.name = "Application name is required";
85
+ newErrors.name = t("oauth.ui.form.errors.name_required");
84
86
  }
85
87
 
86
88
  const validUris = formState.redirectUris.filter((uri) => uri.trim());
87
89
  if (validUris.length === 0) {
88
- newErrors.redirectUris = "At least one redirect URI is required";
90
+ newErrors.redirectUris = t("oauth.ui.form.errors.redirect_uri_required");
89
91
  }
90
92
 
91
93
  if (formState.allowedScopes.length === 0) {
92
- newErrors.allowedScopes = "At least one scope must be selected";
94
+ newErrors.allowedScopes = t("oauth.ui.form.errors.scope_required");
93
95
  }
94
96
 
95
97
  setErrors(newErrors);
96
98
  return Object.keys(newErrors).length === 0;
97
- }, [formState]);
99
+ }, [formState, t]);
98
100
 
99
101
  const handleSubmit = useCallback(
100
102
  async (e: React.FormEvent) => {
@@ -120,20 +122,20 @@ export function OAuthClientForm({
120
122
  <Card>
121
123
  <form onSubmit={handleSubmit}>
122
124
  <CardHeader>
123
- <CardTitle>{isEditMode ? "Edit Application" : "Create OAuth Application"}</CardTitle>
125
+ <CardTitle>{isEditMode ? t("oauth.ui.form.edit_title") : t("oauth.settings.create_title")}</CardTitle>
124
126
  <CardDescription>
125
- {isEditMode ? "Update your OAuth application settings." : "Register a new application to access the API."}
127
+ {isEditMode ? t("oauth.ui.form.edit_description") : t("oauth.ui.form.create_description")}
126
128
  </CardDescription>
127
129
  </CardHeader>
128
130
  <CardContent className="space-y-6">
129
131
  {/* Name */}
130
132
  <div className="space-y-2">
131
- <Label htmlFor="name">Application Name *</Label>
133
+ <Label htmlFor="name">{t("oauth.ui.form.name")} *</Label>
132
134
  <Input
133
135
  id="name"
134
136
  value={formState.name}
135
137
  onChange={(e) => setFormState((s) => ({ ...s, name: e.target.value }))}
136
- placeholder="My Application"
138
+ placeholder={t("oauth.ui.form.name_placeholder")}
137
139
  disabled={isLoading}
138
140
  className={errors.name ? "border-destructive" : ""}
139
141
  />
@@ -142,12 +144,12 @@ export function OAuthClientForm({
142
144
 
143
145
  {/* Description */}
144
146
  <div className="space-y-2">
145
- <Label htmlFor="description">Description</Label>
147
+ <Label htmlFor="description">{t("oauth.ui.form.description")}</Label>
146
148
  <Textarea
147
149
  id="description"
148
150
  value={formState.description}
149
151
  onChange={(e) => setFormState((s) => ({ ...s, description: e.target.value }))}
150
- placeholder="A brief description of your application"
152
+ placeholder={t("oauth.ui.form.description_placeholder")}
151
153
  disabled={isLoading}
152
154
  rows={3}
153
155
  />
@@ -172,7 +174,7 @@ export function OAuthClientForm({
172
174
 
173
175
  {/* Client Type */}
174
176
  <div className="space-y-3">
175
- <Label>Client Type</Label>
177
+ <Label>{t("oauth.ui.form.client_type")}</Label>
176
178
  <RadioGroup
177
179
  value={formState.isConfidential ? "confidential" : "public"}
178
180
  onValueChange={(v) => setFormState((s) => ({ ...s, isConfidential: v === "confidential" }))}
@@ -182,36 +184,34 @@ export function OAuthClientForm({
182
184
  <RadioGroupItem value="confidential" id="confidential" className="mt-1" />
183
185
  <div>
184
186
  <Label htmlFor="confidential" className="cursor-pointer">
185
- Confidential
187
+ {t("oauth.ui.card.confidential")}
186
188
  </Label>
187
- <p className="text-sm text-muted-foreground">
188
- Server-side application that can securely store the client secret.
189
- </p>
189
+ <p className="text-sm text-muted-foreground">{t("oauth.ui.form.confidential_help")}</p>
190
190
  </div>
191
191
  </div>
192
192
  <div className="flex items-start space-x-3 p-3 rounded-md border">
193
193
  <RadioGroupItem value="public" id="public" className="mt-1" />
194
194
  <div>
195
195
  <Label htmlFor="public" className="cursor-pointer">
196
- Public
196
+ {t("oauth.ui.card.public")}
197
197
  </Label>
198
- <p className="text-sm text-muted-foreground">
199
- Mobile or desktop application. Requires PKCE for authorization.
200
- </p>
198
+ <p className="text-sm text-muted-foreground">{t("oauth.ui.form.public_help")}</p>
201
199
  </div>
202
200
  </div>
203
201
  </RadioGroup>
204
- {isEditMode && (
205
- <p className="text-sm text-muted-foreground">Client type cannot be changed after creation.</p>
206
- )}
202
+ {isEditMode && <p className="text-sm text-muted-foreground">{t("oauth.ui.form.client_type_locked")}</p>}
207
203
  </div>
208
204
  </CardContent>
209
205
  <CardFooter className="flex justify-end gap-3">
210
206
  <Button type="button" variant="outline" onClick={onCancel} disabled={isLoading}>
211
- Cancel
207
+ {t("ui.buttons.cancel")}
212
208
  </Button>
213
209
  <Button type="submit" disabled={isLoading}>
214
- {isLoading ? "Saving..." : isEditMode ? "Save Changes" : "Create Application"}
210
+ {isLoading
211
+ ? t("oauth.ui.form.saving")
212
+ : isEditMode
213
+ ? t("oauth.ui.form.save_changes")
214
+ : t("oauth.ui.list.create")}
215
215
  </Button>
216
216
  </CardFooter>
217
217
  </form>
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { Plus, Key } from "lucide-react";
4
5
  import { Button, Skeleton } from "../../../shadcnui";
5
6
  import { SectionHeader } from "../../../components/typography";
@@ -38,9 +39,12 @@ export function OAuthClientList({
38
39
  onCreateClick,
39
40
  onEditClick,
40
41
  onDeleteClick,
41
- emptyStateMessage = "No OAuth applications yet. Create one to get started.",
42
- title = "OAuth Applications",
42
+ emptyStateMessage: emptyStateMessageProp,
43
+ title: titleProp,
43
44
  }: OAuthClientListProps) {
45
+ const t = useTranslations();
46
+ const emptyStateMessage = emptyStateMessageProp ?? t("oauth.ui.list.empty_message");
47
+ const title = titleProp ?? t("oauth.ui.list.title");
44
48
  // Loading state
45
49
  if (isLoading && clients.length === 0) {
46
50
  return (
@@ -81,18 +85,18 @@ export function OAuthClientList({
81
85
  {onCreateClick && (
82
86
  <Button onClick={onCreateClick}>
83
87
  <Plus className="h-4 w-4 me-2" />
84
- New App
88
+ {t("oauth.ui.list.new_app")}
85
89
  </Button>
86
90
  )}
87
91
  </div>
88
92
  <div className="rounded-lg border border-dashed p-12 text-center">
89
93
  <Key className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
90
- <h3 className="text-lg font-medium mb-2">No OAuth Applications</h3>
94
+ <h3 className="text-lg font-medium mb-2">{t("oauth.ui.list.empty_title")}</h3>
91
95
  <p className="text-muted-foreground mb-4">{emptyStateMessage}</p>
92
96
  {onCreateClick && (
93
97
  <Button onClick={onCreateClick}>
94
98
  <Plus className="h-4 w-4 me-2" />
95
- Create Application
99
+ {t("oauth.ui.list.create")}
96
100
  </Button>
97
101
  )}
98
102
  </div>
@@ -108,7 +112,7 @@ export function OAuthClientList({
108
112
  {onCreateClick && (
109
113
  <Button onClick={onCreateClick}>
110
114
  <Plus className="h-4 w-4 me-2" />
111
- New App
115
+ {t("oauth.ui.list.new_app")}
112
116
  </Button>
113
117
  )}
114
118
  </div>
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { useState, useCallback } from "react";
4
5
  import { Copy, Check, AlertTriangle } from "lucide-react";
5
6
  import {
@@ -42,6 +43,7 @@ export interface OAuthClientSecretDisplayProps {
42
43
  * ```
43
44
  */
44
45
  export function OAuthClientSecretDisplay({ secret, onDismiss, open, clientName }: OAuthClientSecretDisplayProps) {
46
+ const t = useTranslations();
45
47
  const [copied, setCopied] = useState(false);
46
48
 
47
49
  const handleCopy = useCallback(async () => {
@@ -65,21 +67,21 @@ export function OAuthClientSecretDisplay({ secret, onDismiss, open, clientName }
65
67
  <DialogHeader>
66
68
  <DialogTitle className="flex items-center gap-2">
67
69
  <AlertTriangle className="h-5 w-5 text-warning" />
68
- Save Your Client Secret
70
+ {t("oauth.ui.secret_display.title")}
69
71
  </DialogTitle>
70
72
  <DialogDescription>
71
73
  {clientName
72
- ? `Your client secret for "${clientName}" is shown below.`
73
- : "Your client secret is shown below."}
74
+ ? t("oauth.ui.secret_display.description_named", { name: clientName })
75
+ : t("oauth.ui.secret_display.description")}
74
76
  </DialogDescription>
75
77
  </DialogHeader>
76
78
 
77
79
  <Alert variant="destructive" className="my-4">
78
80
  <AlertTriangle className="h-4 w-4" />
79
81
  <AlertDescription>
80
- <strong>This is the only time your client secret will be displayed.</strong>
82
+ <strong>{t("oauth.ui.secret_display.warning_title")}</strong>
81
83
  <br />
82
- Copy it now and store it securely. You will not be able to retrieve it later.
84
+ {t("oauth.ui.secret_display.warning_body")}
83
85
  </AlertDescription>
84
86
  </Alert>
85
87
 
@@ -92,17 +94,17 @@ export function OAuthClientSecretDisplay({ secret, onDismiss, open, clientName }
92
94
  variant="outline"
93
95
  size="icon"
94
96
  onClick={handleCopy}
95
- title={copied ? "Copied!" : "Copy to clipboard"}
97
+ title={copied ? t("oauth.ui.secret_display.copied_short") : t("oauth.ui.secret_display.copy_to_clipboard")}
96
98
  >
97
99
  {copied ? <Check className="text-success h-4 w-4" /> : <Copy className="h-4 w-4" />}
98
100
  </Button>
99
101
  </div>
100
102
 
101
- {copied && <p className="text-success text-center text-sm">Copied to clipboard!</p>}
103
+ {copied && <p className="text-success text-center text-sm">{t("oauth.ui.secret_display.copied")}</p>}
102
104
 
103
105
  <DialogFooter className="mt-4">
104
106
  <Button onClick={handleDismiss} className="w-full">
105
- I've Saved My Secret
107
+ {t("oauth.ui.secret_display.confirm")}
106
108
  </Button>
107
109
  </DialogFooter>
108
110
  </DialogContent>
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { useCallback } from "react";
4
5
  import { Plus, Trash2 } from "lucide-react";
5
6
  import { Button, Input, Label } from "../../../shadcnui";
@@ -67,8 +68,10 @@ export function OAuthRedirectUriInput({
67
68
  onChange,
68
69
  error,
69
70
  disabled = false,
70
- label = "Redirect URIs",
71
+ label: labelProp,
71
72
  }: OAuthRedirectUriInputProps) {
73
+ const t = useTranslations();
74
+ const label = labelProp ?? t("oauth.ui.redirect_uris.label");
72
75
  const handleAdd = useCallback(() => {
73
76
  onChange([...value, ""]);
74
77
  }, [value, onChange]);
@@ -94,10 +97,7 @@ export function OAuthRedirectUriInput({
94
97
  return (
95
98
  <div className="space-y-2">
96
99
  <Label>{label} *</Label>
97
- <p className="text-sm text-muted-foreground">
98
- Enter the URIs where users will be redirected after authorization. Use https:// for production, or custom
99
- schemes for mobile apps.
100
- </p>
100
+ <p className="text-sm text-muted-foreground">{t("oauth.ui.redirect_uris.help")}</p>
101
101
 
102
102
  <div className="space-y-2">
103
103
  {value.map((uri, index) => {
@@ -109,15 +109,11 @@ export function OAuthRedirectUriInput({
109
109
  <Input
110
110
  value={uri}
111
111
  onChange={(e) => handleChange(index, e.target.value)}
112
- placeholder="https://example.com/callback or myapp://oauth"
112
+ placeholder={t("oauth.ui.redirect_uris.placeholder")}
113
113
  disabled={disabled}
114
114
  className={!isValid ? "border-destructive" : ""}
115
115
  />
116
- {!isValid && (
117
- <p className="text-xs text-destructive mt-1">
118
- Must be https://, http://localhost, or a custom scheme (app://)
119
- </p>
120
- )}
116
+ {!isValid && <p className="text-xs text-destructive mt-1">{t("oauth.ui.redirect_uris.invalid")}</p>}
121
117
  </div>
122
118
  <Button
123
119
  type="button"
@@ -125,7 +121,7 @@ export function OAuthRedirectUriInput({
125
121
  size="icon"
126
122
  onClick={() => handleRemove(index)}
127
123
  disabled={disabled || value.length === 1}
128
- title="Remove URI"
124
+ title={t("oauth.ui.redirect_uris.remove")}
129
125
  >
130
126
  <Trash2 className="h-4 w-4" />
131
127
  </Button>
@@ -136,7 +132,7 @@ export function OAuthRedirectUriInput({
136
132
 
137
133
  <Button type="button" variant="outline" size="sm" onClick={handleAdd} disabled={disabled} className="mt-2">
138
134
  <Plus className="h-4 w-4 me-2" />
139
- Add Redirect URI
135
+ {t("oauth.ui.redirect_uris.add")}
140
136
  </Button>
141
137
 
142
138
  {error && <p className="text-destructive text-xs/relaxed">{error}</p>}
@@ -1,7 +1,9 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { useCallback } from "react";
4
5
  import { Checkbox, Label } from "../../../shadcnui";
6
+ import { useOAuthScopeText } from "../hooks/useOAuthScopeText";
5
7
  import { AVAILABLE_OAUTH_SCOPES, OAuthScopeInfo } from "../interfaces/oauth.interface";
6
8
 
7
9
  export interface OAuthScopeSelectorProps {
@@ -39,8 +41,11 @@ export function OAuthScopeSelector({
39
41
  availableScopes = AVAILABLE_OAUTH_SCOPES,
40
42
  disabled = false,
41
43
  error,
42
- label = "Allowed Scopes",
44
+ label: labelProp,
43
45
  }: OAuthScopeSelectorProps) {
46
+ const t = useTranslations();
47
+ const scopeText = useOAuthScopeText();
48
+ const label = labelProp ?? t("oauth.ui.scope_selector.label");
44
49
  const handleToggle = useCallback(
45
50
  (scope: string, checked: boolean) => {
46
51
  if (checked) {
@@ -56,7 +61,7 @@ export function OAuthScopeSelector({
56
61
  const groupedScopes = availableScopes.reduce(
57
62
  (acc, scope) => {
58
63
  const [category] = scope.scope.split(":");
59
- const groupName = category === scope.scope ? "General" : category;
64
+ const groupName = category === scope.scope ? t("oauth.ui.scope_selector.general") : category;
60
65
 
61
66
  if (!acc[groupName]) {
62
67
  acc[groupName] = [];
@@ -71,7 +76,7 @@ export function OAuthScopeSelector({
71
76
  <div className="space-y-4">
72
77
  <div>
73
78
  <Label>{label} *</Label>
74
- <p className="text-sm text-muted-foreground">Select the permissions your application needs.</p>
79
+ <p className="text-sm text-muted-foreground">{t("oauth.ui.scope_selector.help")}</p>
75
80
  </div>
76
81
 
77
82
  <div className="space-y-4">
@@ -98,10 +103,14 @@ export function OAuthScopeSelector({
98
103
  />
99
104
  <div className="flex-1">
100
105
  <Label htmlFor={`scope-${scopeInfo.scope}`} className="cursor-pointer">
101
- {scopeInfo.name}
102
- {isAdmin && <span className="ms-2 text-xs text-destructive">(Dangerous)</span>}
106
+ {scopeText(scopeInfo).name}
107
+ {isAdmin && (
108
+ <span className="ms-2 text-xs text-destructive">
109
+ {t("oauth.ui.scope_selector.dangerous")}
110
+ </span>
111
+ )}
103
112
  </Label>
104
- <p className="text-xs text-muted-foreground">{scopeInfo.description}</p>
113
+ <p className="text-xs text-muted-foreground">{scopeText(scopeInfo).description}</p>
105
114
  </div>
106
115
  </div>
107
116
  );
@@ -3,6 +3,8 @@ import { describe, expect, it, vi } from "vitest";
3
3
  import { OAuthClientForm } from "../OAuthClientForm";
4
4
  import { OAUTH_SCOPE_DISPLAY, OAuthClientInterface } from "../../interfaces/oauth.interface";
5
5
 
6
+ vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key }));
7
+
6
8
  const scopeCheckbox = (name: string) => screen.getByRole("checkbox", { name: new RegExp(name) });
7
9
 
8
10
  const makeClient = (allowedScopes: string[]): OAuthClientInterface =>
@@ -18,9 +20,9 @@ describe("OAuthClientForm", () => {
18
20
  it("renders only the scopes passed in availableScopes", () => {
19
21
  render(<OAuthClientForm onSubmit={vi.fn()} onCancel={vi.fn()} availableScopes={[OAUTH_SCOPE_DISPLAY.mcp]} />);
20
22
 
21
- expect(screen.getByText("MCP Server Access")).toBeTruthy();
22
- expect(screen.queryByText("Read Access")).toBeNull();
23
- expect(screen.queryByText("Write Access")).toBeNull();
23
+ expect(screen.getByText("oauth.ui.scopes.mcp.name")).toBeTruthy();
24
+ expect(screen.queryByText("oauth.ui.scopes.read.name")).toBeNull();
25
+ expect(screen.queryByText("oauth.ui.scopes.write.name")).toBeNull();
24
26
  });
25
27
 
26
28
  it("preselects defaultScopes in create mode", () => {
@@ -33,8 +35,8 @@ describe("OAuthClientForm", () => {
33
35
  />,
34
36
  );
35
37
 
36
- expect(scopeCheckbox("MCP Server Access").getAttribute("aria-checked")).toBe("true");
37
- expect(scopeCheckbox("Read Access").getAttribute("aria-checked")).toBe("false");
38
+ expect(scopeCheckbox("oauth.ui.scopes.mcp.name").getAttribute("aria-checked")).toBe("true");
39
+ expect(scopeCheckbox("oauth.ui.scopes.read.name").getAttribute("aria-checked")).toBe("false");
38
40
  });
39
41
 
40
42
  it("uses client.allowedScopes instead of defaultScopes in edit mode", () => {
@@ -48,15 +50,15 @@ describe("OAuthClientForm", () => {
48
50
  />,
49
51
  );
50
52
 
51
- expect(scopeCheckbox("Read Access").getAttribute("aria-checked")).toBe("true");
52
- expect(scopeCheckbox("MCP Server Access").getAttribute("aria-checked")).toBe("false");
53
+ expect(scopeCheckbox("oauth.ui.scopes.read.name").getAttribute("aria-checked")).toBe("true");
54
+ expect(scopeCheckbox("oauth.ui.scopes.mcp.name").getAttribute("aria-checked")).toBe("false");
53
55
  });
54
56
 
55
57
  it("offers the generic scope list by default, without Only35 scopes", () => {
56
58
  render(<OAuthClientForm onSubmit={vi.fn()} onCancel={vi.fn()} />);
57
59
 
58
- expect(screen.getByText("MCP Server Access")).toBeTruthy();
59
- expect(screen.getByText("Read Access")).toBeTruthy();
60
+ expect(screen.getByText("oauth.ui.scopes.mcp.name")).toBeTruthy();
61
+ expect(screen.getByText("oauth.ui.scopes.read.name")).toBeTruthy();
60
62
  expect(screen.queryByText("View Photographs")).toBeNull();
61
63
  expect(screen.queryByText("View Rolls")).toBeNull();
62
64
  });
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useTranslations } from "next-intl";
4
4
  import { Eye, Pencil, Image, Upload, Film, FolderPlus, User, Shield, LucideIcon } from "lucide-react";
5
+ import { useOAuthScopeText } from "../../hooks/useOAuthScopeText";
5
6
  import { OAuthScopeInfo } from "../../interfaces/oauth.interface";
6
7
  import { MicroLabel } from "../../../../components/typography";
7
8
 
@@ -27,6 +28,7 @@ const SCOPE_ICONS: Record<string, LucideIcon> = {
27
28
  */
28
29
  export function OAuthScopeList({ scopes }: OAuthScopeListProps) {
29
30
  const t = useTranslations();
31
+ const scopeText = useOAuthScopeText();
30
32
 
31
33
  if (scopes.length === 0) {
32
34
  return null;
@@ -47,8 +49,8 @@ export function OAuthScopeList({ scopes }: OAuthScopeListProps) {
47
49
  </div>
48
50
  </div>
49
51
  <div className="flex-1">
50
- <p className="font-medium">{scope.name}</p>
51
- <p className="text-sm text-muted-foreground">{scope.description}</p>
52
+ <p className="font-medium">{scopeText(scope).name}</p>
53
+ <p className="text-sm text-muted-foreground">{scopeText(scope).description}</p>
52
54
  </div>
53
55
  </li>
54
56
  );