@carlonicora/nextjs-jsonapi 4.1.2 → 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 (229) 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/src/components/containers/RoundPageContainer.tsx +3 -1
  88. package/src/components/containers/RoundPageContainerTitle.tsx +5 -2
  89. package/src/components/editors/BlockNoteEditor.tsx +31 -15
  90. package/src/components/editors/BlockNoteEditorAiActions.tsx +11 -7
  91. package/src/components/editors/__tests__/BlockNoteEditorAiActions.test.ts +10 -3
  92. package/src/components/errors/ErrorDetails.tsx +5 -2
  93. package/src/components/forms/CommonAssociationForm.tsx +1 -1
  94. package/src/components/forms/EntityMultiSelector.tsx +6 -2
  95. package/src/components/forms/EntitySelector.tsx +4 -1
  96. package/src/components/forms/FileUploader.tsx +3 -2
  97. package/src/components/forms/MultiFileUploader.tsx +3 -1
  98. package/src/components/navigations/Breadcrumb.tsx +3 -2
  99. package/src/components/navigations/Header.tsx +3 -1
  100. package/src/components/navigations/MobileNavigationBar.tsx +3 -1
  101. package/src/components/navigations/__tests__/Header.spec.tsx +2 -2
  102. package/src/components/tables/cells/cell.id.tsx +13 -2
  103. package/src/core/abstracts/AbstractService.ts +3 -3
  104. package/src/core/abstracts/ClientAbstractService.ts +3 -2
  105. package/src/features/assistant-action/components/ApprovalActionCard.tsx +74 -5
  106. package/src/features/assistant-action/components/__tests__/ApprovalActionCard.spec.tsx +81 -0
  107. package/src/features/assistant-message/components/parts/RelevanceMeter.tsx +4 -1
  108. package/src/features/assistant-message/components/parts/tabs/UsersTab.tsx +2 -1
  109. package/src/features/auth/components/buttons/GoogleSignInButton.tsx +4 -1
  110. package/src/features/auth/components/details/LandingComponent.tsx +1 -1
  111. package/src/features/auth/components/forms/ForgotPassword.tsx +1 -1
  112. package/src/features/auth/components/forms/Login.tsx +1 -1
  113. package/src/features/auth/components/forms/Register.tsx +1 -1
  114. package/src/features/auth/components/forms/ResetPassword.tsx +2 -2
  115. package/src/features/auth/components/two-factor/BackupCodesDialog.tsx +1 -1
  116. package/src/features/auth/components/two-factor/PasskeyButton.tsx +1 -1
  117. package/src/features/billing/components/cards/BillingUsageSummaryCard.tsx +21 -11
  118. package/src/features/billing/components/cards/CustomerInfoCard.tsx +16 -13
  119. package/src/features/billing/components/cards/InvoicesSummaryCard.tsx +33 -21
  120. package/src/features/billing/components/cards/PaymentMethodSummaryCard.tsx +20 -10
  121. package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +35 -22
  122. package/src/features/billing/components/containers/BillingDashboardContainer.tsx +24 -24
  123. package/src/features/billing/components/utils/__tests__/billing-formatters.spec.ts +63 -0
  124. package/src/features/billing/components/utils/currency.ts +67 -10
  125. package/src/features/billing/components/utils/date.ts +9 -4
  126. package/src/features/billing/components/utils/format-options.ts +20 -0
  127. package/src/features/billing/components/utils/index.ts +3 -1
  128. package/src/features/billing/components/utils/useBillingFormatOptions.ts +11 -0
  129. package/src/features/billing/components/widgets/BillingAlertBanner.tsx +8 -9
  130. package/src/features/billing/contexts/PriceContext.tsx +18 -6
  131. package/src/features/billing/i18n-keys.ts +212 -0
  132. package/src/features/billing/stripe-customer/components/containers/PaymentMethodsContainer.tsx +8 -8
  133. package/src/features/billing/stripe-customer/components/details/PaymentMethodCard.tsx +11 -9
  134. package/src/features/billing/stripe-customer/components/forms/PaymentMethodEditor.tsx +4 -4
  135. package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +9 -7
  136. package/src/features/billing/stripe-invoice/components/containers/InvoicesContainer.tsx +10 -8
  137. package/src/features/billing/stripe-invoice/components/details/InvoiceDetails.tsx +34 -29
  138. package/src/features/billing/stripe-invoice/components/lists/InvoicesList.tsx +12 -9
  139. package/src/features/billing/stripe-invoice/components/widgets/InvoiceStatusBadge.tsx +9 -6
  140. package/src/features/billing/stripe-price/components/details/PriceDetails.tsx +4 -3
  141. package/src/features/billing/stripe-price/components/forms/PriceArchiver.tsx +3 -1
  142. package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +3 -1
  143. package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +6 -5
  144. package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +12 -7
  145. package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.service.ts +3 -1
  146. package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +10 -6
  147. package/src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx +44 -29
  148. package/src/features/billing/stripe-subscription/components/forms/CancelSubscriptionDialog.tsx +14 -11
  149. package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +20 -17
  150. package/src/features/billing/stripe-subscription/components/widgets/IntervalToggle.tsx +4 -2
  151. package/src/features/billing/stripe-subscription/components/widgets/PricingCard.tsx +16 -7
  152. package/src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx +3 -1
  153. package/src/features/billing/stripe-subscription/components/widgets/ProductPricingRow.tsx +3 -1
  154. package/src/features/billing/stripe-subscription/components/widgets/ProrationPreview.tsx +12 -7
  155. package/src/features/billing/stripe-subscription/components/widgets/SubscriptionConfirmation.tsx +11 -8
  156. package/src/features/billing/stripe-subscription/components/widgets/SubscriptionStatusBadge.tsx +13 -10
  157. package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +8 -6
  158. package/src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx +7 -4
  159. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +4 -2
  160. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx +3 -1
  161. package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +37 -28
  162. package/src/features/billing/stripe-subscription/hooks/useConfirmSubscriptionPayment.ts +9 -7
  163. package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +20 -9
  164. package/src/features/billing/stripe-usage/components/containers/UsageContainer.tsx +6 -6
  165. package/src/features/billing/stripe-usage/components/details/UsageSummaryCard.tsx +24 -9
  166. package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +16 -12
  167. package/src/features/company/components/details/TokenStatusIndicator.tsx +7 -5
  168. package/src/features/company/components/forms/CompanyEditor.tsx +2 -2
  169. package/src/features/help/components/HelpArticleBody.tsx +1 -1
  170. package/src/features/help/components/HelpHeader.tsx +1 -1
  171. package/src/features/help/components/HelpSideNav.tsx +1 -1
  172. package/src/features/help/i18n-keys.ts +2 -0
  173. package/src/features/notification/components/common/NotificationErrorBoundary.tsx +17 -12
  174. package/src/features/notification/components/containers/NotificationsListContainer.tsx +4 -2
  175. package/src/features/notification/components/notifications/Notification.tsx +6 -3
  176. package/src/features/notification/contexts/NotificationContext.tsx +9 -4
  177. package/src/features/notification/contexts/__tests__/NotificationContext.spec.tsx +13 -0
  178. package/src/features/notification/hooks/useNotificationTableStructure.tsx +4 -2
  179. package/src/features/oauth/components/OAuthClientCard.tsx +13 -7
  180. package/src/features/oauth/components/OAuthClientDetail.tsx +34 -30
  181. package/src/features/oauth/components/OAuthClientForm.tsx +24 -24
  182. package/src/features/oauth/components/OAuthClientList.tsx +10 -6
  183. package/src/features/oauth/components/OAuthClientSecretDisplay.tsx +10 -8
  184. package/src/features/oauth/components/OAuthRedirectUriInput.tsx +9 -13
  185. package/src/features/oauth/components/OAuthScopeSelector.tsx +15 -6
  186. package/src/features/oauth/components/__tests__/OAuthClientForm.spec.tsx +11 -9
  187. package/src/features/oauth/components/consent/OAuthScopeList.tsx +4 -2
  188. package/src/features/oauth/hooks/__tests__/useOAuthScopeText.spec.ts +30 -0
  189. package/src/features/oauth/hooks/index.ts +1 -0
  190. package/src/features/oauth/hooks/useOAuthClient.ts +11 -5
  191. package/src/features/oauth/hooks/useOAuthClients.ts +9 -3
  192. package/src/features/oauth/hooks/useOAuthConsent.ts +11 -5
  193. package/src/features/oauth/hooks/useOAuthScopeText.ts +33 -0
  194. package/src/features/rbac/components/RbacByRoleContainer.tsx +7 -6
  195. package/src/features/rbac/components/RbacContainer.tsx +7 -6
  196. package/src/features/rbac/contexts/RbacContext.tsx +3 -3
  197. package/src/features/user/components/containers/UserContainer.tsx +3 -1
  198. package/src/features/user/data/__tests__/user.test.ts +66 -0
  199. package/src/features/user/data/user.interface.ts +2 -0
  200. package/src/features/user/data/user.service.ts +9 -0
  201. package/src/features/user/data/user.ts +7 -0
  202. package/src/features/waitlist/components/forms/WaitlistForm.tsx +1 -1
  203. package/src/features/waitlist/components/lists/WaitlistList.tsx +1 -1
  204. package/src/i18n/__tests__/config.test.ts +21 -1
  205. package/src/i18n/config.ts +13 -0
  206. package/src/i18n/index.ts +1 -0
  207. package/src/shadcnui/custom/multi-select.tsx +47 -14
  208. package/src/shadcnui/custom/multiple-selector.tsx +14 -2
  209. package/src/shadcnui/ui/__tests__/use-ui-text.test.tsx +76 -0
  210. package/src/shadcnui/ui/breadcrumb.tsx +9 -2
  211. package/src/shadcnui/ui/carousel.tsx +15 -4
  212. package/src/shadcnui/ui/dialog.tsx +15 -2
  213. package/src/shadcnui/ui/sheet.tsx +6 -1
  214. package/src/shadcnui/ui/sidebar.tsx +33 -7
  215. package/src/shadcnui/ui/spinner.tsx +6 -1
  216. package/src/shadcnui/ui/use-ui-text.ts +49 -0
  217. package/src/utils/__tests__/date-formatter.test.ts +24 -1
  218. package/src/utils/date-formatter.ts +16 -0
  219. package/src/utils/index.ts +1 -1
  220. package/dist/BlockNoteEditor-BENYTTCZ.js.map +0 -1
  221. package/dist/BlockNoteEditor-VOYKZOP5.mjs.map +0 -1
  222. package/dist/chunk-4BZJ4LAB.js.map +0 -1
  223. package/dist/chunk-6ISF5KNQ.mjs.map +0 -1
  224. package/dist/chunk-ORU7KLCY.js.map +0 -1
  225. package/dist/chunk-ZZ6E33FO.mjs.map +0 -1
  226. /package/dist/{BlockNoteViewer-Y2ZM4ESA.mjs.map → BlockNoteViewer-MDPO6BXN.mjs.map} +0 -0
  227. /package/dist/{chunk-46S3KPLU.mjs.map → chunk-ILFFC6BG.mjs.map} +0 -0
  228. /package/dist/{chunk-HZVVRIJQ.mjs.map → chunk-IUS2RHCX.mjs.map} +0 -0
  229. /package/dist/{chunk-ZO3QQ5CQ.mjs.map → chunk-MQDR6MEE.mjs.map} +0 -0
@@ -2,6 +2,25 @@ import { render, screen } from "@testing-library/react";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { ApprovalActionCard } from "../ApprovalActionCard";
4
4
 
5
+ /**
6
+ * A message catalogue the test fills in. Keys it does not hold behave like a
7
+ * host app without them: `t()` hands back the key path and `t.has()` is false,
8
+ * so the card must fall back to the humanised attribute name.
9
+ */
10
+ const messages = vi.hoisted(() => ({}) as Record<string, string>);
11
+
12
+ vi.mock("next-intl", () => {
13
+ const t = (key: string) => messages[key] ?? key;
14
+ (t as unknown as { has: (key: string) => boolean }).has = (key: string) => key in messages;
15
+ return {
16
+ useTranslations: () => t,
17
+ useLocale: () => "en",
18
+ useNow: () => new Date(),
19
+ useTimeZone: () => "UTC",
20
+ useFormatter: () => ({ dateTime: vi.fn(), number: vi.fn(), relativeTime: vi.fn() }),
21
+ };
22
+ });
23
+
5
24
  const findOne = vi.fn();
6
25
  const approve = vi.fn();
7
26
  const deny = vi.fn();
@@ -45,6 +64,7 @@ function makeAction(overrides: Record<string, unknown> = {}) {
45
64
 
46
65
  describe("ApprovalActionCard", () => {
47
66
  beforeEach(() => {
67
+ for (const key of Object.keys(messages)) delete messages[key];
48
68
  findOne.mockReset();
49
69
  approve.mockReset();
50
70
  deny.mockReset();
@@ -254,6 +274,67 @@ describe("ApprovalActionCard", () => {
254
274
  expect(screen.queryByText(UUID_LIKE)).toBeNull();
255
275
  });
256
276
 
277
+ it("labels fields with the entity editor's own message keys when the app defines them", async () => {
278
+ Object.assign(messages, {
279
+ "features.npc.fields.name.label": "Nome",
280
+ "features.npc.fields.tldr.label": "Riassunto",
281
+ "entities.npcs": "PNG",
282
+ });
283
+ findOne.mockResolvedValue(makeAction());
284
+
285
+ render(<ApprovalActionCard actionId="act-1" />);
286
+
287
+ expect(await screen.findByText("Nome")).toBeInTheDocument();
288
+ expect(screen.getByText("Riassunto")).toBeInTheDocument();
289
+ // `related` has no editor key and no generic key: humanised, as before
290
+ expect(screen.getByText("Related")).toBeInTheDocument();
291
+ expect(screen.queryByText("Name")).toBeNull();
292
+ expect(screen.queryByText("Tldr")).toBeNull();
293
+ });
294
+
295
+ it("falls back to the generic ui.fields key, and finds editor keys for irregular plurals", async () => {
296
+ Object.assign(messages, {
297
+ "ui.fields.related": "Collegati",
298
+ "features.knife.fields.name.label": "Nome del gancio",
299
+ });
300
+ findOne.mockResolvedValue(
301
+ makeAction({
302
+ proposal: JSON.stringify({
303
+ type: "knives",
304
+ attributes: { name: "The sister left behind", publicDescription: "Visible" },
305
+ relationships: { related: [{ id: "r1", type: "npcs", label: "Zoe" }] },
306
+ }),
307
+ }),
308
+ );
309
+
310
+ render(<ApprovalActionCard actionId="act-1" />);
311
+
312
+ expect(await screen.findByText("Nome del gancio")).toBeInTheDocument();
313
+ expect(screen.getByText("Collegati")).toBeInTheDocument();
314
+ // no key anywhere: the humanised attribute name, unchanged
315
+ expect(screen.getByText("Public Description")).toBeInTheDocument();
316
+ });
317
+
318
+ it("translates the relationship name on a link proposal", async () => {
319
+ Object.assign(messages, { "features.clue.fields.containedInScene.label": "Contenuto nella scena" });
320
+ findOne.mockResolvedValue(
321
+ makeAction({
322
+ toolName: "link_entities",
323
+ proposal: JSON.stringify({
324
+ type: "clues",
325
+ target: { id: "c1", type: "clues", label: "Bloody knife" },
326
+ relationship: "containedInScene",
327
+ targets: [{ id: "s1", type: "scenes", label: "The docks" }],
328
+ }),
329
+ }),
330
+ );
331
+
332
+ render(<ApprovalActionCard actionId="act-1" />);
333
+
334
+ expect(await screen.findByText("Contenuto nella scena")).toBeInTheDocument();
335
+ expect(screen.getByText("The docks")).toBeInTheDocument();
336
+ });
337
+
257
338
  it("still renders the summary and the buttons when neither payload parses", async () => {
258
339
  findOne.mockResolvedValue(makeAction({ proposal: "{not-json", toolArgs: "{not-json" }));
259
340
 
@@ -1,5 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
4
+
3
5
  interface Props {
4
6
  /** 0-1 (decimal) or 0-100 (percent). Auto-detected. */
5
7
  value: number;
@@ -7,6 +9,7 @@ interface Props {
7
9
  }
8
10
 
9
11
  export function RelevanceMeter({ value, className = "" }: Props) {
12
+ const t = useTranslations();
10
13
  const pct = Math.max(0, Math.min(100, value <= 1 ? value * 100 : value));
11
14
  const label = `${Math.round(pct)}%`;
12
15
  return (
@@ -15,7 +18,7 @@ export function RelevanceMeter({ value, className = "" }: Props) {
15
18
  aria-valuenow={Math.round(pct)}
16
19
  aria-valuemin={0}
17
20
  aria-valuemax={100}
18
- aria-label={`Relevance ${label}`}
21
+ aria-label={`${t("common.relevance")} ${label}`}
19
22
  className={`bg-muted relative mx-auto flex h-5 w-20 items-center justify-center overflow-hidden rounded border ${className}`}
20
23
  >
21
24
  <div className="bg-accent absolute top-0 start-0 h-full" style={{ width: `${pct}%` }} />
@@ -101,7 +101,8 @@ export function UsersTab({ users, citations, sources }: Props) {
101
101
  return null;
102
102
  }
103
103
  const href = generate({ page: module, id: user.id });
104
- const name = (user as any).name ?? (user as any).fullName ?? user.identifier ?? "User";
104
+ const name =
105
+ (user as any).name ?? (user as any).fullName ?? user.identifier ?? t("entities.users", { count: 1 });
105
106
  const avatarUrl = (user as any).avatar as string | undefined;
106
107
  const showCounts = citationCount > 0 || contentCount > 0;
107
108
  return (
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { getPublicApiUrl } from "../../../../client/config";
4
5
  import { isGoogleAuthEnabled } from "../../../../login";
5
6
  import { Button, Link } from "../../../../shadcnui";
@@ -9,6 +10,8 @@ interface GoogleSignInButtonProps {
9
10
  }
10
11
 
11
12
  export function GoogleSignInButton({ referralCode }: GoogleSignInButtonProps) {
13
+ const t = useTranslations();
14
+
12
15
  if (!isGoogleAuthEnabled()) {
13
16
  return null;
14
17
  }
@@ -45,7 +48,7 @@ export function GoogleSignInButton({ referralCode }: GoogleSignInButtonProps) {
45
48
  d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
46
49
  />
47
50
  </svg>
48
- Sign in with Google
51
+ {t("auth.buttons.sign_in_with_google")}
49
52
  </Button>
50
53
  </Link>
51
54
  );
@@ -55,7 +55,7 @@ export function LandingComponent() {
55
55
  {isDiscordAuthEnabled() && (
56
56
  <Link href={`${getPublicApiUrl()}auth/discord`} className="flex w-full justify-end">
57
57
  <Button className="w-full" variant={`outline`} data-testid="page-login-button-initial-login">
58
- Login with Discord
58
+ {t(`auth.buttons.login_with_discord`)}
59
59
  </Button>
60
60
  </Link>
61
61
  )}
@@ -49,7 +49,7 @@ export function ForgotPassword() {
49
49
  await AuthService.initialiseForgotPassword(payload);
50
50
  setShowConfirmation(true);
51
51
  } catch (e) {
52
- errorToast({ error: e });
52
+ errorToast({ title: t("common.errors.error"), error: e });
53
53
  }
54
54
  };
55
55
 
@@ -152,7 +152,7 @@ export function Login() {
152
152
  {isDiscordAuthEnabled() && (
153
153
  <Link href={buildDiscordOAuthUrl()} className="flex w-full justify-end">
154
154
  <Button className="w-full" variant={`outline`} data-testid="page-login-button-initial-login">
155
- Login with Discord
155
+ {t(`auth.buttons.login_with_discord`)}
156
156
  </Button>
157
157
  </Link>
158
158
  )}
@@ -159,7 +159,7 @@ export default function Register() {
159
159
  clearReferralCode();
160
160
  setShowConfirmation(true);
161
161
  } catch (e) {
162
- errorToast({ error: e });
162
+ errorToast({ title: t("common.errors.error"), error: e });
163
163
  }
164
164
  };
165
165
 
@@ -106,8 +106,8 @@ export function ResetPassword() {
106
106
  ) : (
107
107
  <Form {...form}>
108
108
  <form onSubmit={form.handleSubmit(onSubmit)}>
109
- <FormPassword form={form} id="password" name={"Password"} />
110
- <FormPassword form={form} id="passwordRetype" name={"Retype Password"} />
109
+ <FormPassword form={form} id="password" name={t(`user.fields.password.label`)} />
110
+ <FormPassword form={form} id="passwordRetype" name={t(`auth.fields.retype_password.retype_label`)} />
111
111
  <Button className="mt-4 w-full" type={"submit"}>
112
112
  {t(`auth.buttons.reset_password`)}
113
113
  </Button>
@@ -59,7 +59,7 @@ export function BackupCodesDialog({ remainingCodes, onRegenerate, trigger }: Bac
59
59
  };
60
60
 
61
61
  const handleDownload = () => {
62
- const text = `Backup Codes\n\n${codes.join("\n")}\n\nKeep these codes safe. Each code can only be used once.`;
62
+ const text = `${t("auth.two_factor.backup_codes")}\n\n${codes.join("\n")}\n\n${t("auth.two_factor.backup_codes_file_note")}`;
63
63
  const blob = new Blob([text], { type: "text/plain" });
64
64
  const url = URL.createObjectURL(blob);
65
65
  const a = document.createElement("a");
@@ -38,7 +38,7 @@ export function PasskeyButton({ pendingToken, onSuccess, onError, disabled = fal
38
38
 
39
39
  onSuccess(auth);
40
40
  } catch (error) {
41
- onError(error instanceof Error ? error : new Error("Passkey authentication failed"));
41
+ onError(error instanceof Error ? error : new Error(t("auth.two_factor.passkey_failed")));
42
42
  } finally {
43
43
  setIsLoading(false);
44
44
  }
@@ -1,7 +1,9 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { Activity, ChevronRight } from "lucide-react";
4
5
  import { Card, CardContent, CardHeader, CardTitle, Skeleton } from "../../../../shadcnui";
6
+ import { useI18nLocale } from "../../../../i18n";
5
7
  import { MeterInterface, MeterSummaryInterface } from "../../stripe-usage";
6
8
 
7
9
  type BillingUsageSummaryCardProps = {
@@ -12,8 +14,8 @@ type BillingUsageSummaryCardProps = {
12
14
  onViewDetailsClick: () => void;
13
15
  };
14
16
 
15
- function formatNumber(value: number): string {
16
- return new Intl.NumberFormat(undefined, {
17
+ function formatNumber(value: number, locale: string): string {
18
+ return new Intl.NumberFormat(locale, {
17
19
  notation: "compact",
18
20
  compactDisplay: "short",
19
21
  }).format(value);
@@ -26,11 +28,13 @@ export function BillingUsageSummaryCard({
26
28
  error,
27
29
  onViewDetailsClick,
28
30
  }: BillingUsageSummaryCardProps) {
31
+ const t = useTranslations();
32
+ const locale = useI18nLocale();
29
33
  if (loading) {
30
34
  return (
31
35
  <Card>
32
36
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
33
- <CardTitle>Usage This Month</CardTitle>
37
+ <CardTitle>{t("billing.ui.cards.usage.title")}</CardTitle>
34
38
  <Activity className="h-4 w-4 text-muted-foreground" />
35
39
  </CardHeader>
36
40
  <CardContent>
@@ -45,7 +49,7 @@ export function BillingUsageSummaryCard({
45
49
  return (
46
50
  <Card>
47
51
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
48
- <CardTitle>Usage This Month</CardTitle>
52
+ <CardTitle>{t("billing.ui.cards.usage.title")}</CardTitle>
49
53
  <Activity className="h-4 w-4 text-muted-foreground" />
50
54
  </CardHeader>
51
55
  <CardContent>
@@ -67,26 +71,32 @@ export function BillingUsageSummaryCard({
67
71
  return (
68
72
  <Card className="cursor-pointer hover:bg-accent/50 transition-colors" onClick={onViewDetailsClick}>
69
73
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
70
- <CardTitle>Usage This Month</CardTitle>
74
+ <CardTitle>{t("billing.ui.cards.usage.title")}</CardTitle>
71
75
  <Activity className="h-4 w-4 text-muted-foreground" />
72
76
  </CardHeader>
73
77
  <CardContent>
74
78
  {meters.length === 0 ? (
75
79
  <div className="space-y-2">
76
- <p className="text-xl font-bold text-muted-foreground">No meters</p>
77
- <p className="text-xs text-muted-foreground">No usage meters are configured</p>
80
+ <p className="text-xl font-bold text-muted-foreground">{t("billing.ui.cards.usage.no_meters")}</p>
81
+ <p className="text-xs text-muted-foreground">{t("billing.ui.cards.usage.no_meters_description")}</p>
78
82
  </div>
79
83
  ) : (
80
84
  <div className="space-y-2">
81
- <p className="text-xl font-bold">{formatNumber(totalUsage)} units</p>
85
+ <p className="text-xl font-bold">
86
+ {t("billing.ui.cards.usage.units", { value: formatNumber(totalUsage, locale) })}
87
+ </p>
82
88
  {primaryMeter && primarySummary && (
83
89
  <p className="text-sm text-muted-foreground">
84
- {primaryMeter.displayName}: {formatNumber(primarySummary.aggregatedValue)}
90
+ {primaryMeter.displayName}: {formatNumber(primarySummary.aggregatedValue, locale)}
91
+ </p>
92
+ )}
93
+ {meters.length > 1 && (
94
+ <p className="text-xs text-muted-foreground">
95
+ {t("billing.ui.cards.usage.across_meters", { count: meters.length })}
85
96
  </p>
86
97
  )}
87
- {meters.length > 1 && <p className="text-xs text-muted-foreground">Across {meters.length} meters</p>}
88
98
  <span className="flex items-center text-xs text-muted-foreground">
89
- View details
99
+ {t("billing.ui.cards.view_details")}
90
100
  <ChevronRight className="h-3 w-3 ms-1 rtl:rotate-180" />
91
101
  </span>
92
102
  </div>
@@ -1,8 +1,10 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { ExternalLink, User } from "lucide-react";
4
5
  import { useState } from "react";
5
6
  import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "../../../../shadcnui";
7
+ import { useI18nLocale } from "../../../../i18n";
6
8
  import { StripeCustomerInterface, StripeCustomerService } from "../../stripe-customer";
7
9
 
8
10
  type CustomerInfoCardProps = {
@@ -11,18 +13,19 @@ type CustomerInfoCardProps = {
11
13
  error?: string;
12
14
  };
13
15
 
14
- function formatBalance(balance: number | undefined, currency: string | undefined): string {
15
- if (balance === undefined || balance === 0) return "$0.00";
16
+ function formatBalance(balance: number | undefined, currency: string | undefined, locale: string): string {
16
17
  const currencyCode = currency?.toUpperCase() || "USD";
17
18
  // Balance in Stripe is negative when customer has credit
18
- const displayBalance = -balance;
19
- return new Intl.NumberFormat(undefined, {
19
+ const displayBalance = balance ? -balance : 0;
20
+ return new Intl.NumberFormat(locale, {
20
21
  style: "currency",
21
22
  currency: currencyCode,
22
23
  }).format(displayBalance / 100);
23
24
  }
24
25
 
25
26
  export function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardProps) {
27
+ const t = useTranslations();
28
+ const locale = useI18nLocale();
26
29
  const [portalLoading, setPortalLoading] = useState(false);
27
30
 
28
31
  const handlePortalClick = async (e: React.MouseEvent) => {
@@ -42,7 +45,7 @@ export function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardP
42
45
  return (
43
46
  <Card>
44
47
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
45
- <CardTitle>Billing Account</CardTitle>
48
+ <CardTitle>{t("billing.ui.cards.customer.title")}</CardTitle>
46
49
  <User className="h-4 w-4 text-muted-foreground" />
47
50
  </CardHeader>
48
51
  <CardContent>
@@ -58,7 +61,7 @@ export function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardP
58
61
  return (
59
62
  <Card>
60
63
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
61
- <CardTitle>Billing Account</CardTitle>
64
+ <CardTitle>{t("billing.ui.cards.customer.title")}</CardTitle>
62
65
  <User className="h-4 w-4 text-muted-foreground" />
63
66
  </CardHeader>
64
67
  <CardContent>
@@ -72,12 +75,12 @@ export function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardP
72
75
  return (
73
76
  <Card>
74
77
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
75
- <CardTitle>Billing Account</CardTitle>
78
+ <CardTitle>{t("billing.ui.cards.customer.title")}</CardTitle>
76
79
  <User className="h-4 w-4 text-muted-foreground" />
77
80
  </CardHeader>
78
81
  <CardContent>
79
- <p className="text-xl font-bold text-muted-foreground">Not set up</p>
80
- <p className="text-xs text-muted-foreground">Billing account will be created when you subscribe</p>
82
+ <p className="text-xl font-bold text-muted-foreground">{t("billing.ui.cards.customer.not_set_up")}</p>
83
+ <p className="text-xs text-muted-foreground">{t("billing.ui.cards.customer.not_set_up_description")}</p>
81
84
  </CardContent>
82
85
  </Card>
83
86
  );
@@ -86,7 +89,7 @@ export function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardP
86
89
  return (
87
90
  <Card>
88
91
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
89
- <CardTitle>Billing Account</CardTitle>
92
+ <CardTitle>{t("billing.ui.cards.customer.title")}</CardTitle>
90
93
  <User className="h-4 w-4 text-muted-foreground" />
91
94
  </CardHeader>
92
95
  <CardContent>
@@ -95,14 +98,14 @@ export function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardP
95
98
  {customer.email && <p className="text-sm text-muted-foreground">{customer.email}</p>}
96
99
  {customer.balance !== undefined && customer.balance !== 0 && (
97
100
  <p className="text-sm">
98
- <span className="text-muted-foreground">Credit Balance: </span>
101
+ <span className="text-muted-foreground">{t("billing.ui.cards.customer.credit_balance")} </span>
99
102
  <span className={customer.balance < 0 ? "text-success" : "text-destructive"}>
100
- {formatBalance(customer.balance, customer.currency)}
103
+ {formatBalance(customer.balance, customer.currency, locale)}
101
104
  </span>
102
105
  </p>
103
106
  )}
104
107
  <Button variant="outline" size="sm" className="mt-2" onClick={handlePortalClick} disabled={portalLoading}>
105
- {portalLoading ? "Loading..." : "Manage in Stripe Portal"}
108
+ {portalLoading ? t("common.loading") : t("billing.ui.cards.customer.manage_portal")}
106
109
  <ExternalLink className="h-4 w-4 ms-1" />
107
110
  </Button>
108
111
  </div>
@@ -1,8 +1,10 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { ChevronRight, ReceiptIcon } from "lucide-react";
4
5
  import { Badge, Card, CardContent, CardHeader, CardTitle, Skeleton } from "../../../../shadcnui";
5
6
  import { InvoiceStatus, StripeInvoiceInterface } from "../../stripe-invoice";
7
+ import { formatDate, useBillingFormatOptions } from "../utils";
6
8
 
7
9
  type InvoicesSummaryCardProps = {
8
10
  invoices: StripeInvoiceInterface[];
@@ -11,6 +13,9 @@ type InvoicesSummaryCardProps = {
11
13
  onViewAllClick: () => void;
12
14
  };
13
15
 
16
+ /** Statuses with a `billing.ui.invoice_status.*` label; anything else Stripe adds is shown raw. */
17
+ const INVOICE_STATUSES: string[] = Object.values(InvoiceStatus);
18
+
14
19
  function getStatusBadgeVariant(status: InvoiceStatus): "default" | "secondary" | "destructive" | "outline" {
15
20
  switch (status) {
16
21
  case InvoiceStatus.PAID:
@@ -25,28 +30,22 @@ function getStatusBadgeVariant(status: InvoiceStatus): "default" | "secondary" |
25
30
  }
26
31
  }
27
32
 
28
- function formatDate(date: Date): string {
29
- return new Date(date).toLocaleDateString(undefined, {
30
- year: "numeric",
31
- month: "short",
32
- day: "numeric",
33
- });
34
- }
35
-
36
- function formatAmount(amount: number, currency: string): string {
33
+ function formatAmount(amount: number, currency: string, locale: string): string {
37
34
  const currencyCode = currency?.toUpperCase() || "USD";
38
- return new Intl.NumberFormat(undefined, {
35
+ return new Intl.NumberFormat(locale, {
39
36
  style: "currency",
40
37
  currency: currencyCode,
41
38
  }).format(amount / 100);
42
39
  }
43
40
 
44
41
  export function InvoicesSummaryCard({ invoices, loading, error, onViewAllClick }: InvoicesSummaryCardProps) {
42
+ const t = useTranslations();
43
+ const fmt = useBillingFormatOptions();
45
44
  if (loading) {
46
45
  return (
47
46
  <Card>
48
47
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
49
- <CardTitle>Recent Invoices</CardTitle>
48
+ <CardTitle>{t("billing.ui.cards.invoices.title")}</CardTitle>
50
49
  <ReceiptIcon className="h-4 w-4 text-muted-foreground" />
51
50
  </CardHeader>
52
51
  <CardContent>
@@ -62,7 +61,7 @@ export function InvoicesSummaryCard({ invoices, loading, error, onViewAllClick }
62
61
  return (
63
62
  <Card>
64
63
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
65
- <CardTitle>Recent Invoices</CardTitle>
64
+ <CardTitle>{t("billing.ui.cards.invoices.title")}</CardTitle>
66
65
  <ReceiptIcon className="h-4 w-4 text-muted-foreground" />
67
66
  </CardHeader>
68
67
  <CardContent>
@@ -80,29 +79,42 @@ export function InvoicesSummaryCard({ invoices, loading, error, onViewAllClick }
80
79
  return (
81
80
  <Card className="cursor-pointer hover:bg-accent/50 transition-colors" onClick={onViewAllClick}>
82
81
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
83
- <CardTitle>Recent Invoices</CardTitle>
82
+ <CardTitle>{t("billing.ui.cards.invoices.title")}</CardTitle>
84
83
  <ReceiptIcon className="h-4 w-4 text-muted-foreground" />
85
84
  </CardHeader>
86
85
  <CardContent>
87
86
  {invoices.length === 0 ? (
88
87
  <div className="space-y-2">
89
- <p className="text-xl font-bold text-muted-foreground">No invoices yet</p>
90
- <p className="text-xs text-muted-foreground">Invoices will appear after your first billing cycle</p>
88
+ <p className="text-xl font-bold text-muted-foreground">{t("billing.ui.invoices.empty_title")}</p>
89
+ <p className="text-xs text-muted-foreground">{t("billing.ui.cards.invoices.empty_description")}</p>
91
90
  </div>
92
91
  ) : latestInvoice ? (
93
92
  <div className="space-y-2">
94
93
  <div className="flex items-center gap-2">
95
- <p className="text-xl font-bold">{formatAmount(latestInvoice.total, latestInvoice.currency)}</p>
96
- <Badge variant={getStatusBadgeVariant(latestInvoice.status)}>{latestInvoice.status}</Badge>
94
+ <p className="text-xl font-bold">
95
+ {formatAmount(latestInvoice.total, latestInvoice.currency, fmt.locale)}
96
+ </p>
97
+ <Badge variant={getStatusBadgeVariant(latestInvoice.status)}>
98
+ {INVOICE_STATUSES.includes(latestInvoice.status)
99
+ ? t(`billing.ui.invoice_status.${latestInvoice.status}`)
100
+ : latestInvoice.status}
101
+ </Badge>
97
102
  </div>
98
103
  <p className="text-sm text-muted-foreground">
99
- {latestInvoice.stripeInvoiceNumber || `Invoice from ${formatDate(latestInvoice.periodStart)}`}
104
+ {latestInvoice.stripeInvoiceNumber ||
105
+ t("billing.ui.cards.invoices.invoice_from", { date: formatDate(latestInvoice.periodStart, fmt) })}
100
106
  </p>
101
107
  <div className="flex items-center gap-4 text-xs text-muted-foreground">
102
- {paidInvoices.length > 0 && <span>{paidInvoices.length} paid</span>}
103
- {openInvoices.length > 0 && <span className="text-warning">{openInvoices.length} open</span>}
108
+ {paidInvoices.length > 0 && (
109
+ <span>{t("billing.ui.cards.invoices.paid_count", { count: paidInvoices.length })}</span>
110
+ )}
111
+ {openInvoices.length > 0 && (
112
+ <span className="text-warning">
113
+ {t("billing.ui.cards.invoices.open_count", { count: openInvoices.length })}
114
+ </span>
115
+ )}
104
116
  <span className="flex items-center">
105
- View all
117
+ {t("billing.ui.cards.view_all")}
106
118
  <ChevronRight className="h-3 w-3 ms-1 rtl:rotate-180" />
107
119
  </span>
108
120
  </div>
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useTranslations } from "next-intl";
3
4
  import { Wallet, ChevronRight } from "lucide-react";
4
5
  import { Card, CardContent, CardHeader, CardTitle, Button, Skeleton } from "../../../../shadcnui";
5
6
  import { PaymentMethodInterface } from "../../stripe-customer";
@@ -32,11 +33,12 @@ export function PaymentMethodSummaryCard({
32
33
  error,
33
34
  onManageClick,
34
35
  }: PaymentMethodSummaryCardProps) {
36
+ const t = useTranslations();
35
37
  if (loading) {
36
38
  return (
37
39
  <Card>
38
40
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
39
- <CardTitle>Payment Method</CardTitle>
41
+ <CardTitle>{t("billing.ui.payment_method.title")}</CardTitle>
40
42
  <Wallet className="h-4 w-4 text-muted-foreground" />
41
43
  </CardHeader>
42
44
  <CardContent>
@@ -51,7 +53,7 @@ export function PaymentMethodSummaryCard({
51
53
  return (
52
54
  <Card>
53
55
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
54
- <CardTitle>Payment Method</CardTitle>
56
+ <CardTitle>{t("billing.ui.payment_method.title")}</CardTitle>
55
57
  <Wallet className="h-4 w-4 text-muted-foreground" />
56
58
  </CardHeader>
57
59
  <CardContent>
@@ -67,14 +69,16 @@ export function PaymentMethodSummaryCard({
67
69
  return (
68
70
  <Card className="cursor-pointer hover:bg-accent/50 transition-colors" onClick={onManageClick}>
69
71
  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
70
- <CardTitle>Payment Method</CardTitle>
72
+ <CardTitle>{t("billing.ui.payment_method.title")}</CardTitle>
71
73
  <Wallet className="h-4 w-4 text-muted-foreground" />
72
74
  </CardHeader>
73
75
  <CardContent>
74
76
  {paymentMethods.length === 0 ? (
75
77
  <div className="space-y-2">
76
- <p className="text-xl font-bold text-muted-foreground">No payment method</p>
77
- <p className="text-xs text-muted-foreground">Add a card to enable subscriptions</p>
78
+ <p className="text-xl font-bold text-muted-foreground">
79
+ {t("billing.ui.cards.payment_method.empty_title")}
80
+ </p>
81
+ <p className="text-xs text-muted-foreground">{t("billing.ui.cards.payment_method.empty_description")}</p>
78
82
  <Button
79
83
  variant="outline"
80
84
  size="sm"
@@ -84,7 +88,7 @@ export function PaymentMethodSummaryCard({
84
88
  onManageClick();
85
89
  }}
86
90
  >
87
- Add Card
91
+ {t("billing.ui.cards.payment_method.add_card")}
88
92
  <ChevronRight className="h-4 w-4 ms-1 rtl:rotate-180" />
89
93
  </Button>
90
94
  </div>
@@ -94,17 +98,23 @@ export function PaymentMethodSummaryCard({
94
98
  {getCardBrandIcon(defaultMethod.card.brand)} ****{defaultMethod.card.last4}
95
99
  </p>
96
100
  <p className="text-sm text-muted-foreground">
97
- Expires {String(defaultMethod.card.expMonth).padStart(2, "0")}/{defaultMethod.card.expYear}
101
+ {t("billing.ui.payment_method.expires", {
102
+ date: `${String(defaultMethod.card.expMonth).padStart(2, "0")}/${defaultMethod.card.expYear}`,
103
+ })}
98
104
  </p>
99
105
  {paymentMethods.length > 1 && (
100
- <p className="text-xs text-muted-foreground">+{paymentMethods.length - 1} more card(s)</p>
106
+ <p className="text-xs text-muted-foreground">
107
+ {t("billing.ui.cards.payment_method.more_cards", { count: paymentMethods.length - 1 })}
108
+ </p>
101
109
  )}
102
110
  </div>
103
111
  ) : (
104
112
  <div className="space-y-2">
105
- <p className="text-xl font-bold">{defaultMethod?.type || "Payment Method"}</p>
113
+ <p className="text-xl font-bold">{defaultMethod?.type || t("billing.ui.payment_method.title")}</p>
106
114
  {paymentMethods.length > 1 && (
107
- <p className="text-xs text-muted-foreground">+{paymentMethods.length - 1} more method(s)</p>
115
+ <p className="text-xs text-muted-foreground">
116
+ {t("billing.ui.cards.payment_method.more_methods", { count: paymentMethods.length - 1 })}
117
+ </p>
108
118
  )}
109
119
  </div>
110
120
  )}