@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
@@ -0,0 +1,30 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { OAUTH_SCOPE_DISPLAY } from "../../interfaces/oauth.interface";
4
+ import { useOAuthScopeText } from "../useOAuthScopeText";
5
+
6
+ // The suite's next-intl mock (vitest.setup.ts) returns the key itself.
7
+ describe("useOAuthScopeText", () => {
8
+ it("reads the built-in scopes from oauth.ui.scopes.<id>.*", () => {
9
+ const { result } = renderHook(() => useOAuthScopeText());
10
+
11
+ expect(result.current("mcp")).toEqual({
12
+ name: "oauth.ui.scopes.mcp.name",
13
+ description: "oauth.ui.scopes.mcp.description",
14
+ });
15
+ expect(result.current(OAUTH_SCOPE_DISPLAY.read)).toEqual({
16
+ name: "oauth.ui.scopes.read.name",
17
+ description: "oauth.ui.scopes.read.description",
18
+ });
19
+ });
20
+
21
+ it("keeps the text an app-defined scope carries, falling back to its id", () => {
22
+ const { result } = renderHook(() => useOAuthScopeText());
23
+
24
+ expect(result.current({ scope: "photos:read", name: "View Photographs", description: "See photos" })).toEqual({
25
+ name: "View Photographs",
26
+ description: "See photos",
27
+ });
28
+ expect(result.current("photos:write")).toEqual({ name: "photos:write", description: "" });
29
+ });
30
+ });
@@ -1,3 +1,4 @@
1
1
  export * from "./useOAuthClients";
2
2
  export * from "./useOAuthClient";
3
3
  export * from "./useOAuthConsent";
4
+ export * from "./useOAuthScopeText";
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { useAtomValue, useSetAtom } from "jotai";
4
- import { useCallback, useEffect, useState } from "react";
5
+ import { useCallback, useEffect, useRef, useState } from "react";
5
6
  import {
6
7
  oauthClientByIdAtom,
7
8
  removeOAuthClientAtom,
@@ -49,6 +50,11 @@ export function useOAuthClient(clientId: string): UseOAuthClientReturn {
49
50
  const updateClientInStore = useSetAtom(updateOAuthClientAtom);
50
51
  const removeClientFromStore = useSetAtom(removeOAuthClientAtom);
51
52
  const setNewClientSecret = useSetAtom(setNewClientSecretAtom);
53
+ const t = useTranslations();
54
+ // A ref, not a dependency: the translator must not re-create the callbacks below
55
+ // (they drive effects that refetch), it only words the fallback errors.
56
+ const tRef = useRef(t);
57
+ tRef.current = t;
52
58
 
53
59
  // Local state for fetched client (if not in store)
54
60
  const [fetchedClient, setFetchedClient] = useState<OAuthClientInterface | null>(null);
@@ -68,7 +74,7 @@ export function useOAuthClient(clientId: string): UseOAuthClientReturn {
68
74
  setFetchedClient(fetched);
69
75
  } catch (err) {
70
76
  console.error("[useOAuthClient] Failed to fetch client:", err);
71
- setError(err instanceof Error ? err : new Error("Failed to fetch OAuth client"));
77
+ setError(err instanceof Error ? err : new Error(tRef.current("oauth.ui.errors.fetch_client")));
72
78
  } finally {
73
79
  setIsLoading(false);
74
80
  }
@@ -94,7 +100,7 @@ export function useOAuthClient(clientId: string): UseOAuthClientReturn {
94
100
  setFetchedClient(updated);
95
101
  } catch (err) {
96
102
  console.error("[useOAuthClient] Failed to update client:", err);
97
- const error = err instanceof Error ? err : new Error("Failed to update OAuth client");
103
+ const error = err instanceof Error ? err : new Error(tRef.current("oauth.ui.errors.update_client"));
98
104
  setError(error);
99
105
  throw error;
100
106
  } finally {
@@ -115,7 +121,7 @@ export function useOAuthClient(clientId: string): UseOAuthClientReturn {
115
121
  removeClientFromStore(clientId);
116
122
  } catch (err) {
117
123
  console.error("[useOAuthClient] Failed to delete client:", err);
118
- const error = err instanceof Error ? err : new Error("Failed to delete OAuth client");
124
+ const error = err instanceof Error ? err : new Error(tRef.current("oauth.ui.errors.delete_client"));
119
125
  setError(error);
120
126
  throw error;
121
127
  } finally {
@@ -141,7 +147,7 @@ export function useOAuthClient(clientId: string): UseOAuthClientReturn {
141
147
  return result.clientSecret;
142
148
  } catch (err) {
143
149
  console.error("[useOAuthClient] Failed to regenerate secret:", err);
144
- const error = err instanceof Error ? err : new Error("Failed to regenerate client secret");
150
+ const error = err instanceof Error ? err : new Error(tRef.current("oauth.ui.errors.regenerate_secret"));
145
151
  setError(error);
146
152
  throw error;
147
153
  } finally {
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { useAtom, useSetAtom } from "jotai";
4
- import { useCallback, useEffect } from "react";
5
+ import { useCallback, useEffect, useRef } from "react";
5
6
  import {
6
7
  addOAuthClientAtom,
7
8
  oauthClientsAtom,
@@ -48,6 +49,11 @@ export function useOAuthClients(): UseOAuthClientsReturn {
48
49
  const [error, setError] = useAtom(oauthClientsErrorAtom);
49
50
  const addClient = useSetAtom(addOAuthClientAtom);
50
51
  const setNewClientSecret = useSetAtom(setNewClientSecretAtom);
52
+ const t = useTranslations();
53
+ // A ref, not a dependency: the translator must not re-create the callbacks below
54
+ // (they drive effects that refetch), it only words the fallback errors.
55
+ const tRef = useRef(t);
56
+ tRef.current = t;
51
57
 
52
58
  const fetchClients = useCallback(async () => {
53
59
  setIsLoading(true);
@@ -58,7 +64,7 @@ export function useOAuthClients(): UseOAuthClientsReturn {
58
64
  setClients(fetchedClients);
59
65
  } catch (err) {
60
66
  console.error("[useOAuthClients] Failed to fetch clients:", err);
61
- setError(err instanceof Error ? err : new Error("Failed to fetch OAuth clients"));
67
+ setError(err instanceof Error ? err : new Error(tRef.current("oauth.ui.errors.fetch_clients")));
62
68
  } finally {
63
69
  setIsLoading(false);
64
70
  }
@@ -91,7 +97,7 @@ export function useOAuthClients(): UseOAuthClientsReturn {
91
97
  return result;
92
98
  } catch (err) {
93
99
  console.error("[useOAuthClients] Failed to create client:", err);
94
- const error = err instanceof Error ? err : new Error("Failed to create OAuth client");
100
+ const error = err instanceof Error ? err : new Error(tRef.current("oauth.ui.errors.create_client"));
95
101
  setError(error);
96
102
  throw error;
97
103
  } finally {
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useEffect, useState } from "react";
3
+ import { useTranslations } from "next-intl";
4
+ import { useCallback, useEffect, useRef, useState } from "react";
4
5
  import { OAuthConsentInfo, OAuthConsentRequest } from "../interfaces/oauth.interface";
5
6
  import { OAuthService } from "../data/oauth.service";
6
7
  import { useCurrentUserContextOptional } from "../../user/contexts/CurrentUserContext";
@@ -60,6 +61,11 @@ export function useOAuthConsent(
60
61
  const [companyId, setCompanyId] = useState<string | undefined>(undefined);
61
62
  const contextCompanyId = useCurrentUserContextOptional()?.company?.id;
62
63
  const currentCompanyId = options?.defaultCompanyId ?? contextCompanyId;
64
+ const t = useTranslations();
65
+ // A ref, not a dependency: the translator must not re-create the callbacks below
66
+ // (they drive effects that refetch), it only words the fallback errors.
67
+ const tRef = useRef(t);
68
+ tRef.current = t;
63
69
 
64
70
  // Seed the studio choice once the user's companies arrive: the studio open in
65
71
  // the web session when it is in the list, otherwise the first one.
@@ -78,7 +84,7 @@ export function useOAuthConsent(
78
84
  // scope is deliberately NOT required: RFC 6749 §3.3 makes it optional
79
85
  // and the server defaults to the client's registered scopes.
80
86
  if (!params.clientId || !params.redirectUri) {
81
- setError(new Error("Missing required authorization parameters"));
87
+ setError(new Error(tRef.current("oauth.ui.errors.missing_parameters")));
82
88
  setIsLoading(false);
83
89
  return;
84
90
  }
@@ -91,7 +97,7 @@ export function useOAuthConsent(
91
97
  setClientInfo(info);
92
98
  } catch (err) {
93
99
  console.error("[useOAuthConsent] Failed to fetch authorization info:", err);
94
- setError(err instanceof Error ? err : new Error("Failed to load authorization info"));
100
+ setError(err instanceof Error ? err : new Error(tRef.current("oauth.ui.errors.load_authorization")));
95
101
  } finally {
96
102
  setIsLoading(false);
97
103
  }
@@ -120,7 +126,7 @@ export function useOAuthConsent(
120
126
  }
121
127
  } catch (err) {
122
128
  console.error("[useOAuthConsent] Failed to approve authorization:", err);
123
- setError(err instanceof Error ? err : new Error("Failed to approve authorization"));
129
+ setError(err instanceof Error ? err : new Error(tRef.current("oauth.ui.errors.approve_authorization")));
124
130
  setIsSubmitting(false);
125
131
  }
126
132
  // Note: Don't set isSubmitting to false on success - we're redirecting
@@ -139,7 +145,7 @@ export function useOAuthConsent(
139
145
  }
140
146
  } catch (err) {
141
147
  console.error("[useOAuthConsent] Failed to deny authorization:", err);
142
- setError(err instanceof Error ? err : new Error("Failed to deny authorization"));
148
+ setError(err instanceof Error ? err : new Error(tRef.current("oauth.ui.errors.deny_authorization")));
143
149
  setIsSubmitting(false);
144
150
  }
145
151
  // Note: Don't set isSubmitting to false on success - we're redirecting
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { useCallback } from "react";
5
+ import { OAUTH_SCOPE_DISPLAY, OAuthScopeInfo } from "../interfaces/oauth.interface";
6
+
7
+ export type OAuthScopeText = { name: string; description: string };
8
+
9
+ /**
10
+ * Returns a function that words a scope in the active language.
11
+ *
12
+ * The scopes defined in `OAUTH_SCOPE_DISPLAY` read their name and description
13
+ * from `oauth.ui.scopes.<id>.*`; the English text in that constant stays as
14
+ * data only. Any other scope (an app's own, or one the server describes) keeps
15
+ * the name and description it carries, falling back to its id.
16
+ */
17
+ export function useOAuthScopeText(): (scope: string | OAuthScopeInfo) => OAuthScopeText {
18
+ const t = useTranslations();
19
+ return useCallback(
20
+ (scope: string | OAuthScopeInfo): OAuthScopeText => {
21
+ const id = typeof scope === "string" ? scope : scope.scope;
22
+ if (Object.prototype.hasOwnProperty.call(OAUTH_SCOPE_DISPLAY, id)) {
23
+ return {
24
+ name: t(`oauth.ui.scopes.${id}.name`),
25
+ description: t(`oauth.ui.scopes.${id}.description`),
26
+ };
27
+ }
28
+ const info = typeof scope === "string" ? undefined : scope;
29
+ return { name: info?.name || id, description: info?.description ?? "" };
30
+ },
31
+ [t],
32
+ );
33
+ }
@@ -61,11 +61,12 @@ const CellButton = memo(function CellButton({
61
61
  );
62
62
  });
63
63
 
64
- const ACTION_LABELS: Record<ActionType, string> = {
65
- read: "Read",
66
- create: "Create",
67
- update: "Update",
68
- delete: "Delete",
64
+ /** Message keys for the action column headers. */
65
+ const ACTION_LABEL_KEYS: Record<ActionType, string> = {
66
+ read: "rbac.actions.read",
67
+ create: "rbac.actions.create",
68
+ update: "rbac.actions.update",
69
+ delete: "rbac.actions.delete",
69
70
  };
70
71
 
71
72
  export default function RbacByRoleContainer() {
@@ -192,7 +193,7 @@ export default function RbacByRoleContainer() {
192
193
  key={action}
193
194
  className="min-w-28 px-2 py-2 text-center text-xs font-medium text-muted-foreground"
194
195
  >
195
- {ACTION_LABELS[action]}
196
+ {t(ACTION_LABEL_KEYS[action])}
196
197
  </th>
197
198
  ))}
198
199
  </tr>
@@ -81,11 +81,12 @@ const CellButton = memo(function CellButton({
81
81
  // changed.
82
82
  // ---------------------------------------------------------------------------
83
83
 
84
- const ACTION_LABELS: Record<ActionType, string> = {
85
- read: "Read",
86
- create: "Create",
87
- update: "Update",
88
- delete: "Delete",
84
+ /** Message keys for the action column headers. */
85
+ const ACTION_LABEL_KEYS: Record<ActionType, string> = {
86
+ read: "rbac.actions.read",
87
+ create: "rbac.actions.create",
88
+ update: "rbac.actions.update",
89
+ delete: "rbac.actions.delete",
89
90
  };
90
91
 
91
92
  interface ModuleEditorProps {
@@ -121,7 +122,7 @@ const ModuleEditor = memo(function ModuleEditor({
121
122
  <th className="w-40 px-4 py-2 text-start text-xs font-medium text-muted-foreground">{t("rbac.role")}</th>
122
123
  {ACTION_TYPES.map((action) => (
123
124
  <th key={action} className="min-w-28 px-2 py-2 text-center text-xs font-medium text-muted-foreground">
124
- {ACTION_LABELS[action]}
125
+ {t(ACTION_LABEL_KEYS[action])}
125
126
  </th>
126
127
  ))}
127
128
  </tr>
@@ -138,14 +138,14 @@ export const RbacProvider = ({
138
138
  setSaving(true);
139
139
  try {
140
140
  await RbacService.saveMatrix({ matrix, roleNames, moduleNames, outputPath });
141
- showToast(`Saved to ${outputPath}`);
141
+ showToast(t("rbac.saved_to", { path: outputPath }));
142
142
  } catch (err) {
143
143
  console.error("Failed to save RBAC matrix:", err);
144
144
  showError(err instanceof Error ? err.message : String(err));
145
145
  } finally {
146
146
  setSaving(false);
147
147
  }
148
- }, [matrix, roleNames, moduleNames, outputPath]);
148
+ }, [matrix, roleNames, moduleNames, outputPath, t]);
149
149
 
150
150
  const breadcrumb = (): BreadcrumbItemData[] => {
151
151
  const response: BreadcrumbItemData[] = [];
@@ -168,7 +168,7 @@ export const RbacProvider = ({
168
168
  functions.push(
169
169
  <Button key="rbacSave" size="sm" onClick={handleSave} disabled={!canSave} className="gap-1">
170
170
  {saving ? <Loader2Icon className="h-3.5 w-3.5 animate-spin" /> : <DownloadIcon className="h-3.5 w-3.5" />}
171
- Save to permissions.ts
171
+ {t("rbac.save_to_file")}
172
172
  </Button>,
173
173
  );
174
174
  if (functions.length > 0) response.functions = functions;
@@ -2,16 +2,18 @@
2
2
 
3
3
  import { RoundPageContainer, Tab } from "@/components";
4
4
  import { Modules } from "@/core";
5
+ import { useTranslations } from "next-intl";
5
6
  import { useUserContext } from "../../contexts";
6
7
  import { UserContent } from "../details";
7
8
 
8
9
  export function UserContainer() {
10
+ const t = useTranslations();
9
11
  const { user } = useUserContext();
10
12
  if (!user) return null;
11
13
 
12
14
  const tabs: Tab[] = [
13
15
  {
14
- label: "Details",
16
+ label: t("ui.labels.details"),
15
17
  content: <UserContent user={user} />,
16
18
  },
17
19
  ];
@@ -0,0 +1,66 @@
1
+ import { beforeAll, describe, expect, it } from "vitest";
2
+ import { ApiRequestDataTypeInterface } from "../../../../core/interfaces/ApiRequestDataTypeInterface";
3
+ import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
4
+ import { User } from "../user";
5
+
6
+ // The model resolves its JSON:API types through the real ModuleRegistry, so every
7
+ // module it names in rehydrate()/createJsonApi() must be registered.
8
+ const modules: Record<string, ApiRequestDataTypeInterface> = {
9
+ User: { name: "users", model: User } as any,
10
+ Role: { name: "roles", model: class {} } as any,
11
+ Company: { name: "companies", model: class {} } as any,
12
+ Module: { name: "modules", model: class {} } as any,
13
+ };
14
+
15
+ const registerIfAbsent = (key: string, module: ApiRequestDataTypeInterface) => {
16
+ try {
17
+ ModuleRegistry.get(key as any);
18
+ } catch {
19
+ ModuleRegistry.register(key, module);
20
+ }
21
+ };
22
+
23
+ beforeAll(() => {
24
+ for (const [key, module] of Object.entries(modules)) registerIfAbsent(key, module);
25
+ });
26
+
27
+ function makeHydratedData(attributeOverrides: Record<string, any> = {}) {
28
+ return {
29
+ jsonApi: {
30
+ id: "user-1",
31
+ type: "users",
32
+ attributes: {
33
+ name: "Carlo",
34
+ email: "carlo@example.test",
35
+ ...attributeOverrides,
36
+ },
37
+ meta: {},
38
+ relationships: {},
39
+ },
40
+ included: [] as any[],
41
+ };
42
+ }
43
+
44
+ describe("User.rehydrate", () => {
45
+ it("exposes the saved language", () => {
46
+ const user = new User().rehydrate(makeHydratedData({ language: "it" }));
47
+ expect(user.language).toBe("it");
48
+ });
49
+
50
+ it("leaves language undefined when the user has not chosen one", () => {
51
+ const user = new User().rehydrate(makeHydratedData());
52
+ expect(user.language).toBeUndefined();
53
+ });
54
+ });
55
+
56
+ describe("User.createJsonApi", () => {
57
+ it("writes language when provided", () => {
58
+ const payload = new User().createJsonApi({ id: "user-1", language: "it" });
59
+ expect(payload.data.attributes.language).toBe("it");
60
+ });
61
+
62
+ it("omits language when undefined", () => {
63
+ const payload = new User().createJsonApi({ id: "user-1", name: "Carlo" });
64
+ expect(payload.data.attributes).not.toHaveProperty("language");
65
+ });
66
+ });
@@ -17,6 +17,7 @@ export type UserInput = {
17
17
  avatar?: string;
18
18
  phone?: string;
19
19
  rate?: number;
20
+ language?: string;
20
21
  };
21
22
 
22
23
  export interface UserInterface extends ApiDataInterface {
@@ -28,6 +29,7 @@ export interface UserInterface extends ApiDataInterface {
28
29
  get avatarUrl(): string | undefined;
29
30
  get phone(): string | undefined;
30
31
  get rate(): number | undefined;
32
+ get language(): string | undefined;
31
33
 
32
34
  get isActivated(): boolean;
33
35
  get isDeleted(): boolean;
@@ -243,6 +243,15 @@ export class UserService extends AbstractService {
243
243
  });
244
244
  }
245
245
 
246
+ static async patchLanguage(params: { id: string; language: string }): Promise<UserInterface> {
247
+ return this.callApi({
248
+ type: Modules.User,
249
+ method: HttpMethod.PATCH,
250
+ endpoint: new EndpointCreator({ endpoint: Modules.User, id: params.id, childEndpoint: "language" }).generate(),
251
+ input: params,
252
+ });
253
+ }
254
+
246
255
  static async patchRate(params: UserInput): Promise<UserInterface> {
247
256
  return this.callApi({
248
257
  type: Modules.User,
@@ -14,6 +14,7 @@ export class User extends AbstractApiData implements UserInterface, SearchResult
14
14
  private _avatarUrl?: string;
15
15
  private _phone?: string;
16
16
  private _rate?: number;
17
+ private _language?: string;
17
18
 
18
19
  private _isActivated?: boolean;
19
20
  private _isDeleted?: boolean;
@@ -62,6 +63,10 @@ export class User extends AbstractApiData implements UserInterface, SearchResult
62
63
  return this._rate;
63
64
  }
64
65
 
66
+ get language(): string | undefined {
67
+ return this._language;
68
+ }
69
+
65
70
  get relevance(): number | undefined {
66
71
  return this._relevance;
67
72
  }
@@ -103,6 +108,7 @@ export class User extends AbstractApiData implements UserInterface, SearchResult
103
108
  this._bio = data.jsonApi.attributes.bio;
104
109
  this._phone = data.jsonApi.attributes.phone;
105
110
  this._rate = data.jsonApi.attributes.rate;
111
+ this._language = data.jsonApi.attributes.language;
106
112
 
107
113
  this._avatar = data.jsonApi.attributes.avatar;
108
114
  this._avatarUrl = data.jsonApi.attributes.avatarUrl;
@@ -143,6 +149,7 @@ export class User extends AbstractApiData implements UserInterface, SearchResult
143
149
  if (data.adminCreated) response.data.attributes.adminCreated = true;
144
150
  if (data.avatar !== undefined) response.data.attributes.avatar = data.avatar;
145
151
  if (data.rate !== undefined) response.data.attributes.rate = data.rate;
152
+ if (data.language !== undefined) response.data.attributes.language = data.language;
146
153
 
147
154
  if (data.roleIds) {
148
155
  response.data.relationships.roles = {
@@ -115,7 +115,7 @@ export function WaitlistForm({ onSuccess }: WaitlistFormProps) {
115
115
  setIsSuccess(true);
116
116
  onSuccess?.();
117
117
  } catch (e) {
118
- errorToast({ error: e });
118
+ errorToast({ title: t("common.errors.error"), error: e });
119
119
  } finally {
120
120
  setIsSubmitting(false);
121
121
  }
@@ -38,7 +38,7 @@ export function WaitlistList({ fullWidth }: WaitlistListProps = {}) {
38
38
  showToast(t("waitlist.admin.invite_sent", { email: entry.email }));
39
39
  void data.refresh();
40
40
  } catch (error) {
41
- errorToast({ error });
41
+ errorToast({ title: t("common.errors.error"), error });
42
42
  }
43
43
  };
44
44
 
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { configureI18n, getI18nServerLocale, type I18nConfig } from "../config";
2
+ import { configureI18n, getI18nServerLocale, getI18nSupportedLocales, type I18nConfig } from "../config";
3
3
 
4
4
  function config(getLocale: I18nConfig["getLocale"]): I18nConfig {
5
5
  return {
@@ -25,3 +25,23 @@ describe("getI18nServerLocale", () => {
25
25
  await expect(getI18nServerLocale()).rejects.toThrow(/configureI18n/);
26
26
  });
27
27
  });
28
+
29
+ describe("getI18nSupportedLocales", () => {
30
+ afterEach(() => {
31
+ configureI18n(null as unknown as I18nConfig);
32
+ });
33
+
34
+ it('returns ["en"] when i18n is not configured', () => {
35
+ expect(getI18nSupportedLocales()).toEqual(["en"]);
36
+ });
37
+
38
+ it('returns ["en"] when supportedLocales is not set', () => {
39
+ configureI18n(config(async () => "en"));
40
+ expect(getI18nSupportedLocales()).toEqual(["en"]);
41
+ });
42
+
43
+ it("returns the configured supported locales", () => {
44
+ configureI18n({ ...config(async () => "en"), supportedLocales: ["en", "it"] });
45
+ expect(getI18nSupportedLocales()).toEqual(["en", "it"]);
46
+ });
47
+ });
@@ -33,6 +33,11 @@ export interface I18nConfig {
33
33
  * `next/root-params` — a module only allowed inside the app directory.
34
34
  */
35
35
  getLocale: GetLocaleFunction;
36
+ /**
37
+ * Locales the app supports. The first entry is the fallback.
38
+ * Defaults to `["en"]` when omitted.
39
+ */
40
+ supportedLocales?: string[];
36
41
  }
37
42
 
38
43
  // Private storage
@@ -89,3 +94,11 @@ export function useI18nDateFnsLocale(): any {
89
94
  // Fallback to undefined (Calendar will use default)
90
95
  return undefined;
91
96
  }
97
+
98
+ /** The app's supported locales. Defaults to `["en"]` when not configured. */
99
+ export function getI18nSupportedLocales(): string[] {
100
+ if (_config?.supportedLocales && _config.supportedLocales.length > 0) {
101
+ return _config.supportedLocales;
102
+ }
103
+ return ["en"];
104
+ }
package/src/i18n/index.ts CHANGED
@@ -6,6 +6,7 @@ export {
6
6
  useI18nDateFnsLocale,
7
7
  getI18nLink,
8
8
  getI18nServerLocale,
9
+ getI18nSupportedLocales,
9
10
  } from "./config";
10
11
 
11
12
  export type {