@carlonicora/nextjs-jsonapi 6.24.0 → 6.25.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 (246) hide show
  1. package/dist/{BlockNoteEditor-MONT74RQ.js → BlockNoteEditor-TNX77GCD.js} +28 -25
  2. package/dist/BlockNoteEditor-TNX77GCD.js.map +1 -0
  3. package/dist/{BlockNoteEditor-ENDOVTAW.mjs → BlockNoteEditor-Z5LDF47Z.mjs} +13 -10
  4. package/dist/{BlockNoteEditor-ENDOVTAW.mjs.map → BlockNoteEditor-Z5LDF47Z.mjs.map} +1 -1
  5. package/dist/{BlockNoteViewer-K2HBF3IP.js → BlockNoteViewer-6KQJODNF.js} +9 -7
  6. package/dist/BlockNoteViewer-6KQJODNF.js.map +1 -0
  7. package/dist/{BlockNoteViewer-2TJZ6JET.mjs → BlockNoteViewer-TQ452ZYW.mjs} +8 -6
  8. package/dist/{BlockNoteViewer-2TJZ6JET.mjs.map → BlockNoteViewer-TQ452ZYW.mjs.map} +1 -1
  9. package/dist/JsonApiRequest-OKNQZLS6.js +26 -0
  10. package/dist/{JsonApiRequest-KC6DOHKT.js.map → JsonApiRequest-OKNQZLS6.js.map} +1 -1
  11. package/dist/{JsonApiRequest-ADLBJ5RX.mjs → JsonApiRequest-WNPDS356.mjs} +2 -2
  12. package/dist/billing/index.js +504 -517
  13. package/dist/billing/index.js.map +1 -1
  14. package/dist/billing/index.mjs +20 -33
  15. package/dist/billing/index.mjs.map +1 -1
  16. package/dist/{chunk-VNUT2D27.mjs → chunk-2PWYG2WJ.mjs} +2 -2
  17. package/dist/{chunk-W4ML6D2X.mjs → chunk-3U7YJ73T.mjs} +7 -7
  18. package/dist/{chunk-BQ2WAG4K.js → chunk-3VNDFKT7.js} +19 -19
  19. package/dist/{chunk-BQ2WAG4K.js.map → chunk-3VNDFKT7.js.map} +1 -1
  20. package/dist/{chunk-BE6UMM5X.mjs → chunk-47HNJQ7U.mjs} +3 -3
  21. package/dist/chunk-5T2AQMQ2.mjs +90 -0
  22. package/dist/chunk-5T2AQMQ2.mjs.map +1 -0
  23. package/dist/{chunk-A5M3MORY.js → chunk-5WM72MLE.js} +3 -3
  24. package/dist/{chunk-A5M3MORY.js.map → chunk-5WM72MLE.js.map} +1 -1
  25. package/dist/{chunk-S7SPAC6X.mjs → chunk-73NKUBMP.mjs} +3 -3
  26. package/dist/chunk-743M4TIB.mjs +76 -0
  27. package/dist/chunk-743M4TIB.mjs.map +1 -0
  28. package/dist/{chunk-HN7L6RRW.js → chunk-CSBQAPHD.js} +903 -955
  29. package/dist/chunk-CSBQAPHD.js.map +1 -0
  30. package/dist/{chunk-YTDGYI62.mjs → chunk-DWPHHUMV.mjs} +3 -3
  31. package/dist/{chunk-53SD4YRF.js → chunk-FBHUUPRV.js} +5 -5
  32. package/dist/{chunk-53SD4YRF.js.map → chunk-FBHUUPRV.js.map} +1 -1
  33. package/dist/{chunk-T6G2F2BK.mjs → chunk-HJT7TXGG.mjs} +16 -68
  34. package/dist/chunk-HJT7TXGG.mjs.map +1 -0
  35. package/dist/chunk-HROKKUBQ.js +156 -0
  36. package/dist/chunk-HROKKUBQ.js.map +1 -0
  37. package/dist/{chunk-TMV7TL7O.mjs → chunk-JELEQV5T.mjs} +8 -6
  38. package/dist/{chunk-TMV7TL7O.mjs.map → chunk-JELEQV5T.mjs.map} +1 -1
  39. package/dist/{chunk-TNTCLVUR.js → chunk-JGL4P3DS.js} +17 -15
  40. package/dist/chunk-JGL4P3DS.js.map +1 -0
  41. package/dist/{chunk-7OIWOPM5.mjs → chunk-K7KLUYS7.mjs} +4 -3
  42. package/dist/chunk-K7KLUYS7.mjs.map +1 -0
  43. package/dist/{chunk-OZADFYE7.mjs → chunk-KVYYPKRO.mjs} +308 -241
  44. package/dist/chunk-KVYYPKRO.mjs.map +1 -0
  45. package/dist/{chunk-RCOKQ3R4.js → chunk-LNO2JLU3.js} +2 -2
  46. package/dist/{chunk-RCOKQ3R4.js.map → chunk-LNO2JLU3.js.map} +1 -1
  47. package/dist/{chunk-BDWV4KYR.js → chunk-M6SNTMK6.js} +1386 -1319
  48. package/dist/chunk-M6SNTMK6.js.map +1 -0
  49. package/dist/{chunk-GMMDXINC.js → chunk-N3NQS7X6.js} +12 -12
  50. package/dist/{chunk-GMMDXINC.js.map → chunk-N3NQS7X6.js.map} +1 -1
  51. package/dist/chunk-NP3AFULU.js +76 -0
  52. package/dist/chunk-NP3AFULU.js.map +1 -0
  53. package/dist/{chunk-SO3YUCL6.js → chunk-OOM54Z5Z.js} +6 -5
  54. package/dist/chunk-OOM54Z5Z.js.map +1 -0
  55. package/dist/{chunk-YSE2Z4RB.js → chunk-PQQFOONG.js} +12 -12
  56. package/dist/{chunk-YSE2Z4RB.js.map → chunk-PQQFOONG.js.map} +1 -1
  57. package/dist/chunk-RW6XZGZG.mjs +156 -0
  58. package/dist/chunk-RW6XZGZG.mjs.map +1 -0
  59. package/dist/chunk-SYMI4IDW.js +90 -0
  60. package/dist/chunk-SYMI4IDW.js.map +1 -0
  61. package/dist/{chunk-N2BFT7KI.mjs → chunk-W7U6O3OE.mjs} +2 -71
  62. package/dist/chunk-W7U6O3OE.mjs.map +1 -0
  63. package/dist/{chunk-YMCL3JXZ.mjs → chunk-YPBIPE2B.mjs} +2 -2
  64. package/dist/{chunk-P7L6OFX5.js → chunk-Z6T2575A.js} +12 -81
  65. package/dist/chunk-Z6T2575A.js.map +1 -0
  66. package/dist/client/index.js +13 -10
  67. package/dist/client/index.js.map +1 -1
  68. package/dist/client/index.mjs +17 -14
  69. package/dist/components/forms/DatePickerPopover.d.ts.map +1 -1
  70. package/dist/components/forms/DateRangeSelector.d.ts.map +1 -1
  71. package/dist/components/forms/FormDate.d.ts.map +1 -1
  72. package/dist/components/forms/FormDateTime.d.ts.map +1 -1
  73. package/dist/components/forms/time-cycle.d.ts +14 -0
  74. package/dist/components/forms/time-cycle.d.ts.map +1 -0
  75. package/dist/components/index.js +11 -8
  76. package/dist/components/index.js.map +1 -1
  77. package/dist/components/index.mjs +10 -7
  78. package/dist/components/tables/cells/cell.date.d.ts.map +1 -1
  79. package/dist/components/tables/cells/cell.dateTime.d.ts.map +1 -1
  80. package/dist/contexts/DateLocaleContext.d.ts +56 -0
  81. package/dist/contexts/DateLocaleContext.d.ts.map +1 -0
  82. package/dist/contexts/index.d.ts +1 -0
  83. package/dist/contexts/index.d.ts.map +1 -1
  84. package/dist/contexts/index.js +24 -8
  85. package/dist/contexts/index.js.map +1 -1
  86. package/dist/contexts/index.mjs +23 -7
  87. package/dist/core/index.js +17 -6
  88. package/dist/core/index.js.map +1 -1
  89. package/dist/core/index.mjs +19 -8
  90. package/dist/date-locale/index.d.ts +2 -0
  91. package/dist/date-locale/index.d.ts.map +1 -0
  92. package/dist/date-locale/index.js +21 -0
  93. package/dist/date-locale/index.js.map +1 -0
  94. package/dist/date-locale/index.mjs +21 -0
  95. package/dist/email-automations/index.js +131 -123
  96. package/dist/email-automations/index.js.map +1 -1
  97. package/dist/email-automations/index.mjs +23 -15
  98. package/dist/email-automations/index.mjs.map +1 -1
  99. package/dist/features/account-deletion/components/AccountDeletionAdminTrigger.d.ts.map +1 -1
  100. package/dist/features/analytics/components/AnalyticsJourneyContainer.d.ts.map +1 -1
  101. package/dist/features/analytics/components/AnalyticsSessionsTable.d.ts.map +1 -1
  102. package/dist/features/analytics/index.js +154 -147
  103. package/dist/features/analytics/index.js.map +1 -1
  104. package/dist/features/analytics/index.mjs +32 -25
  105. package/dist/features/analytics/index.mjs.map +1 -1
  106. package/dist/features/analytics/server.js +11 -9
  107. package/dist/features/analytics/server.js.map +1 -1
  108. package/dist/features/analytics/server.mjs +9 -7
  109. package/dist/features/analytics/server.mjs.map +1 -1
  110. package/dist/features/billing/components/utils/date.d.ts +3 -3
  111. package/dist/features/billing/components/utils/format-options.d.ts +6 -4
  112. package/dist/features/billing/components/utils/format-options.d.ts.map +1 -1
  113. package/dist/features/billing/components/utils/useBillingFormatOptions.d.ts +5 -1
  114. package/dist/features/billing/components/utils/useBillingFormatOptions.d.ts.map +1 -1
  115. package/dist/features/billing/stripe-usage/components/details/UsageSummaryCard.d.ts.map +1 -1
  116. package/dist/features/billing/stripe-usage/components/lists/UsageHistoryTable.d.ts.map +1 -1
  117. package/dist/features/blog/components/admin/BlogTranslationStatus.d.ts.map +1 -1
  118. package/dist/features/blog/components/public/BlogPostMeta.d.ts.map +1 -1
  119. package/dist/features/blog/hooks/useBlogPostTableStructure.d.ts.map +1 -1
  120. package/dist/features/blog/index.js +168 -162
  121. package/dist/features/blog/index.js.map +1 -1
  122. package/dist/features/blog/index.mjs +25 -19
  123. package/dist/features/blog/index.mjs.map +1 -1
  124. package/dist/features/blog/server.js +29 -27
  125. package/dist/features/blog/server.js.map +1 -1
  126. package/dist/features/blog/server.mjs +8 -6
  127. package/dist/features/blog/server.mjs.map +1 -1
  128. package/dist/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.d.ts.map +1 -1
  129. package/dist/features/handbook/components/parts/HandbookIndexStatus.d.ts.map +1 -1
  130. package/dist/features/help/components/HelpArticleBody.d.ts.map +1 -1
  131. package/dist/features/help/index.js +48 -43
  132. package/dist/features/help/index.js.map +1 -1
  133. package/dist/features/help/index.mjs +15 -10
  134. package/dist/features/help/index.mjs.map +1 -1
  135. package/dist/features/notification/components/notifications/Notification.d.ts +2 -1
  136. package/dist/features/notification/components/notifications/Notification.d.ts.map +1 -1
  137. package/dist/features/notification/contexts/NotificationContext.d.ts.map +1 -1
  138. package/dist/features/notification/hooks/useNotificationTableStructure.d.ts.map +1 -1
  139. package/dist/features/oauth/components/OAuthClientCard.d.ts.map +1 -1
  140. package/dist/features/oauth/components/OAuthClientDetail.d.ts.map +1 -1
  141. package/dist/features/tokenusage/index.js +133 -130
  142. package/dist/features/tokenusage/index.js.map +1 -1
  143. package/dist/features/tokenusage/index.mjs +10 -7
  144. package/dist/features/tokenusage/index.mjs.map +1 -1
  145. package/dist/features/tokenusage/lib/formatters.d.ts +6 -3
  146. package/dist/features/tokenusage/lib/formatters.d.ts.map +1 -1
  147. package/dist/index.js +19 -7
  148. package/dist/index.js.map +1 -1
  149. package/dist/index.mjs +23 -11
  150. package/dist/local-transport-AT4TE264.js +15 -0
  151. package/dist/{local-transport-MJPOKPTU.js.map → local-transport-AT4TE264.js.map} +1 -1
  152. package/dist/{local-transport-RVD4FZGA.mjs → local-transport-KWEUZ4GX.mjs} +5 -5
  153. package/dist/offline/index.js +56 -54
  154. package/dist/offline/index.js.map +1 -1
  155. package/dist/offline/index.mjs +23 -21
  156. package/dist/offline/index.mjs.map +1 -1
  157. package/dist/{request-4AWGPIGH.mjs → request-6GAIWWTG.mjs} +2 -2
  158. package/dist/{request-H5FZBZA2.js → request-BWO4Y434.js} +3 -3
  159. package/dist/{request-H5FZBZA2.js.map → request-BWO4Y434.js.map} +1 -1
  160. package/dist/server/ServerSession.d.ts +2 -0
  161. package/dist/server/ServerSession.d.ts.map +1 -1
  162. package/dist/server/index.js +10 -8
  163. package/dist/server/index.js.map +1 -1
  164. package/dist/server/index.mjs +9 -7
  165. package/dist/utils/date-formatter.d.ts +45 -4
  166. package/dist/utils/date-formatter.d.ts.map +1 -1
  167. package/dist/utils/index.d.ts +1 -1
  168. package/dist/utils/index.d.ts.map +1 -1
  169. package/dist/write-through-KNRI5L4V.mjs +15 -0
  170. package/dist/write-through-KNRI5L4V.mjs.map +1 -0
  171. package/dist/write-through-MYHQ6GJU.js +15 -0
  172. package/dist/{write-through-HE2QSOVI.js.map → write-through-MYHQ6GJU.js.map} +1 -1
  173. package/messages/en.json +1 -0
  174. package/messages/it.json +1 -0
  175. package/package.json +6 -1
  176. package/src/components/forms/DatePickerPopover.tsx +5 -3
  177. package/src/components/forms/DateRangeSelector.tsx +9 -7
  178. package/src/components/forms/FormDate.tsx +5 -3
  179. package/src/components/forms/FormDateTime.tsx +75 -39
  180. package/src/components/forms/__tests__/time-cycle.spec.ts +37 -0
  181. package/src/components/forms/time-cycle.ts +27 -0
  182. package/src/components/tables/cells/cell.date.tsx +3 -5
  183. package/src/components/tables/cells/cell.dateTime.tsx +3 -7
  184. package/src/contexts/DateLocaleContext.tsx +106 -0
  185. package/src/contexts/__tests__/DateLocaleContext.spec.tsx +175 -0
  186. package/src/contexts/index.ts +1 -0
  187. package/src/date-locale/index.ts +12 -0
  188. package/src/features/account-deletion/components/AccountDeletionAdminTrigger.tsx +3 -4
  189. package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +8 -8
  190. package/src/features/analytics/components/AnalyticsSessionsTable.tsx +4 -6
  191. package/src/features/auth/components/two-factor/PasskeyList.tsx +3 -3
  192. package/src/features/auth/components/two-factor/TotpAuthenticatorList.tsx +3 -3
  193. package/src/features/billing/components/utils/__tests__/billing-formatters.spec.ts +14 -6
  194. package/src/features/billing/components/utils/date.ts +5 -5
  195. package/src/features/billing/components/utils/format-options.ts +6 -4
  196. package/src/features/billing/components/utils/useBillingFormatOptions.ts +8 -2
  197. package/src/features/billing/stripe-usage/components/details/UsageSummaryCard.tsx +4 -12
  198. package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +4 -17
  199. package/src/features/blog/components/admin/BlogTranslationStatus.tsx +3 -2
  200. package/src/features/blog/components/public/BlogPostMeta.tsx +5 -4
  201. package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +1 -2
  202. package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +2 -3
  203. package/src/features/blog/hooks/useBlogPostTableStructure.tsx +4 -8
  204. package/src/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.tsx +11 -5
  205. package/src/features/email-blast/components/containers/EmailBlastContainer.tsx +3 -3
  206. package/src/features/email-blast/hooks/useEmailBlastTableStructure.tsx +4 -4
  207. package/src/features/handbook/components/parts/HandbookIndexStatus.tsx +4 -3
  208. package/src/features/help/components/HelpArticleBody.tsx +4 -3
  209. package/src/features/notification/components/notifications/Notification.tsx +38 -7
  210. package/src/features/notification/contexts/NotificationContext.tsx +6 -3
  211. package/src/features/notification/hooks/useNotificationTableStructure.tsx +6 -5
  212. package/src/features/oauth/components/OAuthClientCard.tsx +8 -5
  213. package/src/features/oauth/components/OAuthClientDetail.tsx +3 -6
  214. package/src/features/tokenusage/lib/__tests__/formatters.spec.ts +19 -0
  215. package/src/features/tokenusage/lib/formatters.ts +11 -6
  216. package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +5 -5
  217. package/src/server/ServerSession.ts +3 -0
  218. package/src/server/__tests__/ServerSession.read.spec.ts +18 -1
  219. package/src/utils/__tests__/date-formatter.spec.ts +134 -10
  220. package/src/utils/date-formatter.ts +200 -44
  221. package/src/utils/index.ts +11 -1
  222. package/dist/BlockNoteEditor-MONT74RQ.js.map +0 -1
  223. package/dist/BlockNoteViewer-K2HBF3IP.js.map +0 -1
  224. package/dist/JsonApiRequest-KC6DOHKT.js +0 -26
  225. package/dist/chunk-7OIWOPM5.mjs.map +0 -1
  226. package/dist/chunk-BDWV4KYR.js.map +0 -1
  227. package/dist/chunk-HN7L6RRW.js.map +0 -1
  228. package/dist/chunk-N2BFT7KI.mjs.map +0 -1
  229. package/dist/chunk-OZADFYE7.mjs.map +0 -1
  230. package/dist/chunk-P7L6OFX5.js.map +0 -1
  231. package/dist/chunk-SO3YUCL6.js.map +0 -1
  232. package/dist/chunk-T6G2F2BK.mjs.map +0 -1
  233. package/dist/chunk-TNTCLVUR.js.map +0 -1
  234. package/dist/local-transport-MJPOKPTU.js +0 -15
  235. package/dist/write-through-7I3LA2WN.mjs +0 -15
  236. package/dist/write-through-HE2QSOVI.js +0 -15
  237. /package/dist/{JsonApiRequest-ADLBJ5RX.mjs.map → JsonApiRequest-WNPDS356.mjs.map} +0 -0
  238. /package/dist/{chunk-VNUT2D27.mjs.map → chunk-2PWYG2WJ.mjs.map} +0 -0
  239. /package/dist/{chunk-W4ML6D2X.mjs.map → chunk-3U7YJ73T.mjs.map} +0 -0
  240. /package/dist/{chunk-BE6UMM5X.mjs.map → chunk-47HNJQ7U.mjs.map} +0 -0
  241. /package/dist/{chunk-S7SPAC6X.mjs.map → chunk-73NKUBMP.mjs.map} +0 -0
  242. /package/dist/{chunk-YTDGYI62.mjs.map → chunk-DWPHHUMV.mjs.map} +0 -0
  243. /package/dist/{chunk-YMCL3JXZ.mjs.map → chunk-YPBIPE2B.mjs.map} +0 -0
  244. /package/dist/{local-transport-RVD4FZGA.mjs.map → date-locale/index.mjs.map} +0 -0
  245. /package/dist/{request-4AWGPIGH.mjs.map → local-transport-KWEUZ4GX.mjs.map} +0 -0
  246. /package/dist/{write-through-7I3LA2WN.mjs.map → request-6GAIWWTG.mjs.map} +0 -0
@@ -5,7 +5,7 @@ import { Send } from "lucide-react";
5
5
  import { useTranslations } from "next-intl";
6
6
  import { useMemo } from "react";
7
7
  import { TableContent, UseTableStructureHook } from "../../../hooks";
8
- import { useI18nLocale } from "../../../i18n";
8
+ import { useDateFormatter } from "../../../contexts/DateLocaleContext";
9
9
  import { Badge, Button } from "../../../shadcnui";
10
10
  import { WaitlistFields } from "../data/waitlist.fields";
11
11
  import { WaitlistInterface, WaitlistStatus } from "../data/WaitlistInterface";
@@ -19,7 +19,7 @@ import { formatSignupSource } from "./formatSignupSource";
19
19
  */
20
20
  export const useWaitlistTableStructure: UseTableStructureHook<WaitlistInterface, WaitlistFields> = (params) => {
21
21
  const t = useTranslations();
22
- const locale = useI18nLocale();
22
+ const dates = useDateFormatter();
23
23
  const onInvite: ((entry: WaitlistInterface) => void) | undefined = params.context?.onInvite;
24
24
 
25
25
  const tableData = useMemo(() => {
@@ -71,7 +71,7 @@ export const useWaitlistTableStructure: UseTableStructureHook<WaitlistInterface,
71
71
  cell: ({ row }: { row: TableContent<WaitlistInterface> }) => {
72
72
  const entry: WaitlistInterface = row.original.jsonApiData;
73
73
  if (!entry.createdAt) return "-";
74
- return new Date(entry.createdAt).toLocaleDateString(locale);
74
+ return dates.format(entry.createdAt, "date");
75
75
  },
76
76
  enableSorting: false,
77
77
  enableHiding: false,
@@ -146,7 +146,7 @@ export const useWaitlistTableStructure: UseTableStructureHook<WaitlistInterface,
146
146
  if (entry.status === "invited" && entry.invitedAt) {
147
147
  return (
148
148
  <span className="text-muted-foreground text-xs">
149
- {t("waitlist.admin.actions.invited_on", { date: new Date(entry.invitedAt).toLocaleDateString(locale) })}
149
+ {t("waitlist.admin.actions.invited_on", { date: dates.format(entry.invitedAt, "dateMedium") })}
150
150
  </span>
151
151
  );
152
152
  }
@@ -168,7 +168,7 @@ export const useWaitlistTableStructure: UseTableStructureHook<WaitlistInterface,
168
168
  return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
169
169
  TableContent<WaitlistInterface>
170
170
  >[];
171
- }, [params.fields, fieldColumnMap, t, locale]);
171
+ }, [params.fields, fieldColumnMap, t, dates]);
172
172
 
173
173
  return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
174
174
  };
@@ -12,6 +12,8 @@ export interface SessionSnapshot {
12
12
  token?: string;
13
13
  sidebarOpen: boolean;
14
14
  isMobile: boolean;
15
+ /** The request's raw `Accept-Language` header; the date locale is resolved from it on the client. */
16
+ acceptLanguage?: string;
15
17
  }
16
18
 
17
19
  export class ServerSession {
@@ -29,6 +31,7 @@ export class ServerSession {
29
31
  token,
30
32
  sidebarOpen: cookieStore.get("sidebar_state")?.value !== "false",
31
33
  isMobile: viewportCookie ? viewportCookie === "1" : userAgent({ headers: headerStore }).device.type === "mobile",
34
+ acceptLanguage: headerStore.get("accept-language") ?? undefined,
32
35
  };
33
36
  }
34
37
 
@@ -4,13 +4,14 @@ const mocks = vi.hoisted(() => {
4
4
  const state = {
5
5
  cookies: {} as Record<string, string>,
6
6
  deviceType: undefined as string | undefined,
7
+ headers: {} as Record<string, string>,
7
8
  };
8
9
  return {
9
10
  state,
10
11
  cookies: vi.fn(async () => ({
11
12
  get: (name: string) => (name in state.cookies ? { name, value: state.cookies[name] } : undefined),
12
13
  })),
13
- headers: vi.fn(async () => new Headers()),
14
+ headers: vi.fn(async () => new Headers(state.headers)),
14
15
  userAgent: vi.fn(() => ({ device: { type: state.deviceType } })),
15
16
  };
16
17
  });
@@ -27,6 +28,7 @@ describe("ServerSession.read", () => {
27
28
  configureRoles({ Administrator: "role-admin", CompanyAdministrator: "role-company-admin" });
28
29
  mocks.state.cookies = {};
29
30
  mocks.state.deviceType = undefined;
31
+ mocks.state.headers = {};
30
32
  });
31
33
 
32
34
  it("reads a logged-in administrator on a desktop with the sidebar closed", async () => {
@@ -64,4 +66,19 @@ describe("ServerSession.read", () => {
64
66
  isMobile: false,
65
67
  });
66
68
  });
69
+
70
+ it("carries the Accept-Language header", async () => {
71
+ mocks.state.headers = { "accept-language": "en-GB,en;q=0.9" };
72
+
73
+ const snapshot = await ServerSession.read();
74
+
75
+ expect(snapshot.acceptLanguage).toBe("en-GB,en;q=0.9");
76
+ });
77
+
78
+ it("leaves acceptLanguage undefined when the header is absent", async () => {
79
+ const snapshot = await ServerSession.read();
80
+
81
+ expect(snapshot).toHaveProperty("acceptLanguage", undefined);
82
+ expect(snapshot.acceptLanguage).toBeUndefined();
83
+ });
67
84
  });
@@ -1,5 +1,15 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { dateFormatLocale, formatDate, FormatOption } from "../date-formatter";
2
+ import {
3
+ DATE_STYLE_OPTIONS,
4
+ dateFormatLocale,
5
+ type DateFormatStyle,
6
+ formatDate,
7
+ formatDateStyle,
8
+ formatRelativeDate,
9
+ PUBLIC_STYLE_MAP,
10
+ resolveDateLocale,
11
+ usesTwelveHourClock,
12
+ } from "../date-formatter";
3
13
 
4
14
  describe("formatDate", () => {
5
15
  beforeEach(() => {
@@ -117,21 +127,21 @@ describe("formatDate", () => {
117
127
  const date = new Date("2024-06-15T11:59:00Z");
118
128
  const result = formatDate(date, "timeSince");
119
129
 
120
- expect(result).toBe("1 minutes ago");
130
+ expect(result).toBe("1 minute ago");
121
131
  });
122
132
 
123
133
  it("should handle 1 hour ago", () => {
124
134
  const date = new Date("2024-06-15T11:00:00Z");
125
135
  const result = formatDate(date, "timeSince");
126
136
 
127
- expect(result).toBe("1 hours ago");
137
+ expect(result).toBe("1 hour ago");
128
138
  });
129
139
 
130
140
  it("should handle 1 day ago", () => {
131
141
  const date = new Date("2024-06-14T12:00:00Z");
132
142
  const result = formatDate(date, "timeSince");
133
143
 
134
- expect(result).toBe("1 days ago");
144
+ expect(result).toBe("1 day ago");
135
145
  });
136
146
 
137
147
  it("should throw for invalid format option", () => {
@@ -171,14 +181,128 @@ describe("formatDate", () => {
171
181
  });
172
182
 
173
183
  describe("dateFormatLocale", () => {
174
- it("maps English and missing locales to en-GB", () => {
175
- expect(dateFormatLocale("en")).toBe("en-GB");
176
- expect(dateFormatLocale("en-US")).toBe("en-GB");
177
- expect(dateFormatLocale(undefined)).toBe("en-GB");
184
+ it("returns the app locale, English when missing", () => {
185
+ expect(dateFormatLocale(undefined)).toBe("en");
186
+ expect(dateFormatLocale("en")).toBe("en");
187
+ expect(dateFormatLocale("it")).toBe("it");
178
188
  });
189
+ });
179
190
 
180
- it("passes other locales through", () => {
181
- expect(dateFormatLocale("it")).toBe("it");
191
+ describe("explicit now", () => {
192
+ it("measures timeSince against the passed now", () => {
193
+ const now = new Date("2030-01-02T00:00:00Z");
194
+ expect(formatDate(new Date("2030-01-01T00:00:00Z"), "timeSince", "en-GB", now)).toBe("1 day ago");
195
+ });
196
+ });
197
+ });
198
+
199
+ const normalise = (value: string): string => value.replace(/[\u202f\u00a0]/g, " ");
200
+ const sample = new Date("2026-04-25T21:30:00Z");
201
+
202
+ describe("resolveDateLocale", () => {
203
+ it("takes the first browser locale whose language matches the app language", () => {
204
+ expect(resolveDateLocale({ appLocale: "en", acceptLanguage: "en-GB,en;q=0.9" })).toBe("en-GB");
205
+ expect(resolveDateLocale({ appLocale: "en", acceptLanguage: "en-US,en;q=0.9" })).toBe("en-US");
206
+ expect(resolveDateLocale({ appLocale: "en", acceptLanguage: "en" })).toBe("en");
207
+ expect(resolveDateLocale({ appLocale: "en", acceptLanguage: "it-IT,it;q=0.9,en-GB;q=0.8" })).toBe("en-GB");
208
+ });
209
+
210
+ it("falls back to the app language when no browser locale matches", () => {
211
+ expect(resolveDateLocale({ appLocale: "it", acceptLanguage: "en-GB" })).toBe("it");
212
+ expect(resolveDateLocale({ appLocale: "en" })).toBe("en");
213
+ });
214
+
215
+ it("follows quality order, not header order", () => {
216
+ expect(resolveDateLocale({ appLocale: "en", acceptLanguage: "fr-FR,en-US;q=0.5,en-GB;q=0.9" })).toBe("en-GB");
217
+ });
218
+
219
+ it("resolveDateLocale tolerates malformed headers", () => {
220
+ for (const acceptLanguage of ["*", "en-GB;q=abc", "zz-ZZ", "en-GB;q=0", ",".repeat(2000), ""]) {
221
+ expect(() => resolveDateLocale({ appLocale: "en", acceptLanguage })).not.toThrow();
222
+ expect(resolveDateLocale({ appLocale: "en", acceptLanguage })).toBe("en");
223
+ }
224
+ });
225
+ });
226
+
227
+ describe("formatDateStyle", () => {
228
+ const expected: Record<DateFormatStyle, { "en-GB": string; "en-US": string }> = {
229
+ date: { "en-GB": "25/04/2026", "en-US": "04/25/2026" },
230
+ dateMedium: { "en-GB": "25 Apr 2026", "en-US": "Apr 25, 2026" },
231
+ dateLong: { "en-GB": "25 April 2026", "en-US": "April 25, 2026" },
232
+ dateTime: { "en-GB": "25/04/2026, 21:30", "en-US": "04/25/2026, 9:30 PM" },
233
+ dateTimeMedium: { "en-GB": "25 Apr 2026, 21:30", "en-US": "Apr 25, 2026, 9:30 PM" },
234
+ dateTimeLong: { "en-GB": "25 April 2026 at 21:30", "en-US": "April 25, 2026 at 9:30 PM" },
235
+ dateTimeSeconds: { "en-GB": "25/04/2026, 21:30:00", "en-US": "04/25/2026, 9:30:00 PM" },
236
+ time: { "en-GB": "21:30", "en-US": "9:30 PM" },
237
+ weekdayDateTime: { "en-GB": "Sat 25 Apr, 21:30", "en-US": "Sat, Apr 25, 9:30 PM" },
238
+ weekdayDay: { "en-GB": "Sat, 25 April 2026", "en-US": "Sat, April 25, 2026" },
239
+ monthYear: { "en-GB": "Apr 2026", "en-US": "Apr 2026" },
240
+ monthYearLong: { "en-GB": "April 2026", "en-US": "April 2026" },
241
+ month: { "en-GB": "Apr", "en-US": "Apr" },
242
+ monthLong: { "en-GB": "April", "en-US": "April" },
243
+ dayMonth: { "en-GB": "25 Apr", "en-US": "Apr 25" },
244
+ weekday: { "en-GB": "Sat", "en-US": "Sat" },
245
+ weekdayLong: { "en-GB": "Saturday", "en-US": "Saturday" },
246
+ };
247
+
248
+ for (const style of Object.keys(expected) as DateFormatStyle[]) {
249
+ for (const locale of ["en-GB", "en-US"] as const) {
250
+ it(`formats ${style} for ${locale}`, () => {
251
+ expect(normalise(formatDateStyle({ date: sample, style, locale, timeZone: "UTC" }))).toBe(
252
+ expected[style][locale],
253
+ );
254
+ });
255
+ }
256
+ }
257
+
258
+ it("never pins the clock", () => {
259
+ for (const options of Object.values(DATE_STYLE_OPTIONS)) {
260
+ expect(options).not.toHaveProperty("hour12");
261
+ expect(options).not.toHaveProperty("hourCycle");
262
+ }
263
+ });
264
+
265
+ it("maps numeric styles to month-as-word in public mode", () => {
266
+ expect(PUBLIC_STYLE_MAP).toEqual({
267
+ date: "dateMedium",
268
+ dateTime: "dateTimeMedium",
269
+ dateTimeSeconds: "dateTimeMedium",
182
270
  });
271
+ expect(formatDateStyle({ date: sample, style: "date", locale: "en-GB", timeZone: "UTC", isPublic: true })).toBe(
272
+ "25 Apr 2026",
273
+ );
274
+ });
275
+
276
+ it("accepts an ISO string", () => {
277
+ expect(formatDateStyle({ date: "2026-04-25T21:30:00Z", style: "date", locale: "en-GB", timeZone: "UTC" })).toBe(
278
+ "25/04/2026",
279
+ );
280
+ });
281
+
282
+ it("formatDateStyle returns empty for invalid dates", () => {
283
+ expect(formatDateStyle({ date: undefined, style: "date", locale: "en-GB" })).toBe("");
284
+ expect(formatDateStyle({ date: "nope", style: "date", locale: "en-GB" })).toBe("");
285
+ expect(formatDateStyle({ date: new Date(NaN), style: "date", locale: "en-GB" })).toBe("");
286
+ });
287
+ });
288
+
289
+ describe("formatRelativeDate", () => {
290
+ const now = new Date("2026-04-25T21:30:00Z");
291
+
292
+ it("says 1 day ago in English", () => {
293
+ expect(formatRelativeDate({ date: new Date("2026-04-24T21:30:00Z"), locale: "en", now })).toBe("1 day ago");
294
+ });
295
+
296
+ it("uses Italian wording", () => {
297
+ expect(formatRelativeDate({ date: new Date("2026-04-25T18:30:00Z"), locale: "it", now })).toBe("3 ore fa");
298
+ });
299
+ });
300
+
301
+ describe("usesTwelveHourClock", () => {
302
+ it("is true for en-US and en, false for en-GB and it", () => {
303
+ expect(usesTwelveHourClock("en-US")).toBe(true);
304
+ expect(usesTwelveHourClock("en")).toBe(true);
305
+ expect(usesTwelveHourClock("en-GB")).toBe(false);
306
+ expect(usesTwelveHourClock("it")).toBe(false);
183
307
  });
184
308
  });
@@ -18,65 +18,221 @@ export const formatLocalDate = (d: Date): string => {
18
18
  return `${year}-${month}-${day}`;
19
19
  };
20
20
 
21
+ export type DateFormatStyle =
22
+ | "date"
23
+ | "dateMedium"
24
+ | "dateLong"
25
+ | "dateTime"
26
+ | "dateTimeMedium"
27
+ | "dateTimeLong"
28
+ | "dateTimeSeconds"
29
+ | "time"
30
+ | "weekdayDateTime"
31
+ | "weekdayDay"
32
+ | "monthYear"
33
+ | "monthYearLong"
34
+ | "month"
35
+ | "monthLong"
36
+ | "dayMonth"
37
+ | "weekday"
38
+ | "weekdayLong";
39
+
40
+ const NUMERIC_DATE: Intl.DateTimeFormatOptions = { day: "2-digit", month: "2-digit", year: "numeric" };
41
+
21
42
  /**
22
- * The locale `formatDate` should use for an app locale. English keeps "en-GB",
23
- * the formatter's historical default, so English output never changes; any
24
- * other app locale ("it") is used as is.
43
+ * The option set behind each named style. No style pins `hour12` / `hourCycle`:
44
+ * the clock (12 or 24 hours) follows the locale.
25
45
  */
26
- export const dateFormatLocale = (appLocale?: string): string =>
27
- !appLocale || appLocale.split("-")[0] === "en" ? "en-GB" : appLocale;
46
+ export const DATE_STYLE_OPTIONS: Record<DateFormatStyle, Intl.DateTimeFormatOptions> = {
47
+ date: NUMERIC_DATE,
48
+ dateMedium: { dateStyle: "medium" },
49
+ dateLong: { dateStyle: "long" },
50
+ dateTime: { ...NUMERIC_DATE, hour: "numeric", minute: "2-digit" },
51
+ dateTimeMedium: { dateStyle: "medium", timeStyle: "short" },
52
+ dateTimeLong: { day: "numeric", month: "long", year: "numeric", hour: "numeric", minute: "2-digit" },
53
+ dateTimeSeconds: { ...NUMERIC_DATE, hour: "numeric", minute: "2-digit", second: "2-digit" },
54
+ time: { hour: "numeric", minute: "2-digit" },
55
+ weekdayDateTime: { weekday: "short", day: "numeric", month: "short", hour: "numeric", minute: "2-digit" },
56
+ weekdayDay: { weekday: "short", day: "numeric", month: "long", year: "numeric" },
57
+ monthYear: { month: "short", year: "numeric" },
58
+ monthYearLong: { month: "long", year: "numeric" },
59
+ month: { month: "short" },
60
+ monthLong: { month: "long" },
61
+ dayMonth: { day: "numeric", month: "short" },
62
+ weekday: { weekday: "short" },
63
+ weekdayLong: { weekday: "long" },
64
+ };
28
65
 
29
- export const formatDate = (eventDate: Date, formatOption: FormatOption, locale: string = "en-GB"): string => {
30
- const formatPart = (date: Date, options: Intl.DateTimeFormatOptions): string =>
31
- new Intl.DateTimeFormat(locale, options).format(date);
66
+ /** Public pages never show an all-numeric date: the month is spelled out. */
67
+ export const PUBLIC_STYLE_MAP: Partial<Record<DateFormatStyle, DateFormatStyle>> = {
68
+ date: "dateMedium",
69
+ dateTime: "dateTimeMedium",
70
+ dateTimeSeconds: "dateTimeMedium",
71
+ };
32
72
 
33
- const now = new Date();
34
- const diff = now.getTime() - eventDate.getTime();
73
+ const formatterCache = new Map<string, Intl.DateTimeFormat>();
35
74
 
36
- if (formatOption === "default") {
37
- if (diff < 24 * 60 * 60 * 1000) formatOption = "timeSince";
38
- else formatOption = "dateTime";
75
+ /** One cached `Intl.DateTimeFormat` per locale + option-set key + time zone. */
76
+ const cachedDateTimeFormat = (
77
+ locale: string,
78
+ key: string,
79
+ options: Intl.DateTimeFormatOptions,
80
+ timeZone?: string,
81
+ ): Intl.DateTimeFormat => {
82
+ const cacheKey = `${locale}|${key}|${timeZone ?? ""}`;
83
+ let formatter = formatterCache.get(cacheKey);
84
+ if (!formatter) {
85
+ formatter = new Intl.DateTimeFormat(locale, timeZone ? { ...options, timeZone } : options);
86
+ formatterCache.set(cacheKey, formatter);
87
+ }
88
+ return formatter;
89
+ };
90
+
91
+ const relativeCache = new Map<string, Intl.RelativeTimeFormat>();
92
+
93
+ const cachedRelativeTimeFormat = (locale: string): Intl.RelativeTimeFormat => {
94
+ let formatter = relativeCache.get(locale);
95
+ if (!formatter) {
96
+ formatter = new Intl.RelativeTimeFormat(locale, { numeric: "always" });
97
+ relativeCache.set(locale, formatter);
39
98
  }
99
+ return formatter;
100
+ };
101
+
102
+ const toValidDate = (date: Date | string | undefined): Date | undefined => {
103
+ if (date === undefined || date === null) return undefined;
104
+ const value = date instanceof Date ? date : new Date(date);
105
+ return Number.isNaN(value.getTime()) ? undefined : value;
106
+ };
107
+
108
+ const languageOf = (tag: string): string => tag.split("-")[0].toLowerCase();
109
+
110
+ const MAX_ACCEPT_LANGUAGE_ENTRIES = 50;
111
+
112
+ /**
113
+ * The locale every real-world date is formatted in.
114
+ *
115
+ * The first `Accept-Language` entry (quality order) whose language equals the
116
+ * app language's and that `Intl` supports; otherwise the app language. A
117
+ * malformed entry (`*`, an unparseable or zero quality, an unknown tag) is
118
+ * skipped, never thrown on.
119
+ */
120
+ export function resolveDateLocale(params: { appLocale: string; acceptLanguage?: string }): string {
121
+ const { appLocale, acceptLanguage } = params;
122
+ if (!acceptLanguage) return appLocale;
123
+
124
+ const appLanguage = languageOf(appLocale);
125
+ const entries: { tag: string; quality: number; index: number }[] = [];
126
+
127
+ const parts = acceptLanguage.split(",");
128
+ for (let index = 0; index < parts.length && entries.length < MAX_ACCEPT_LANGUAGE_ENTRIES; index++) {
129
+ const [rawTag, ...rawParams] = parts[index].split(";");
130
+ const tag = rawTag.trim();
131
+ if (!tag || tag === "*") continue;
40
132
 
41
- if (formatOption === "timeSince") {
42
- const seconds = Math.floor(diff / 1000);
43
- const minutes = Math.floor(seconds / 60);
44
- const hours = Math.floor(minutes / 60);
45
- const days = Math.floor(hours / 24);
46
-
47
- if (locale.split("-")[0] !== "en") {
48
- const relative = new Intl.RelativeTimeFormat(locale, { numeric: "always" });
49
- if (days > 0) return relative.format(-days, "day");
50
- if (hours > 0) return relative.format(-hours, "hour");
51
- if (minutes > 0) return relative.format(-minutes, "minute");
52
- return relative.format(-seconds, "second");
133
+ let quality = 1;
134
+ const qualityParam = rawParams.map((param) => param.trim()).find((param) => param.startsWith("q="));
135
+ if (qualityParam !== undefined) {
136
+ const raw = qualityParam.slice(2);
137
+ quality = /^\d+(\.\d+)?$/.test(raw) ? Number(raw) : NaN;
53
138
  }
139
+ if (!(quality > 0)) continue;
140
+ if (languageOf(tag) !== appLanguage) continue;
54
141
 
55
- if (days > 0) return `${days} days ago`;
56
- if (hours > 0) return `${hours} hours ago`;
57
- if (minutes > 0) return `${minutes} minutes ago`;
58
- return `${seconds} seconds ago`;
142
+ entries.push({ tag, quality, index });
143
+ }
144
+
145
+ entries.sort((a, b) => b.quality - a.quality || a.index - b.index);
146
+
147
+ for (const entry of entries) {
148
+ try {
149
+ const [supported] = Intl.DateTimeFormat.supportedLocalesOf([entry.tag]);
150
+ if (supported && languageOf(supported) === appLanguage) return supported;
151
+ } catch {
152
+ // An invalid language tag: skip it.
153
+ }
154
+ }
155
+
156
+ return appLocale;
157
+ }
158
+
159
+ /**
160
+ * Formats a date in a named style. Returns "" for `undefined` or a value that
161
+ * does not parse. `isPublic` swaps the all-numeric styles for month-as-word.
162
+ */
163
+ export function formatDateStyle(params: {
164
+ date: Date | string | undefined;
165
+ style: DateFormatStyle;
166
+ locale: string;
167
+ timeZone?: string;
168
+ isPublic?: boolean;
169
+ }): string {
170
+ const value = toValidDate(params.date);
171
+ if (!value) return "";
172
+ const style = (params.isPublic && PUBLIC_STYLE_MAP[params.style]) || params.style;
173
+ return cachedDateTimeFormat(params.locale, style, DATE_STYLE_OPTIONS[style], params.timeZone).format(value);
174
+ }
175
+
176
+ /** "3 hours ago" / "3 ore fa", always numeric ("1 day ago", never "yesterday"). */
177
+ export function formatRelativeDate(params: { date: Date | string; locale: string; now?: Date }): string {
178
+ const value = toValidDate(params.date);
179
+ if (!value) return "";
180
+ const now = params.now ?? new Date();
181
+ const diff = now.getTime() - value.getTime();
182
+ const sign = diff < 0 ? 1 : -1;
183
+ const seconds = Math.floor(Math.abs(diff) / 1000);
184
+ const minutes = Math.floor(seconds / 60);
185
+ const hours = Math.floor(minutes / 60);
186
+ const days = Math.floor(hours / 24);
187
+
188
+ const relative = cachedRelativeTimeFormat(params.locale);
189
+ if (days > 0) return relative.format(sign * days, "day");
190
+ if (hours > 0) return relative.format(sign * hours, "hour");
191
+ if (minutes > 0) return relative.format(sign * minutes, "minute");
192
+ return relative.format(sign * seconds, "second");
193
+ }
194
+
195
+ /** True when the locale's clock is 12-hour (hour cycle h11 / h12). */
196
+ export function usesTwelveHourClock(locale: string): boolean {
197
+ const hourCycle = cachedDateTimeFormat(locale, "__hourCycle", { hour: "numeric" }).resolvedOptions().hourCycle;
198
+ return hourCycle === "h11" || hourCycle === "h12";
199
+ }
200
+
201
+ /** The locale to format dates in for an app locale: the app locale itself, English when missing. */
202
+ export const dateFormatLocale = (appLocale?: string): string => appLocale ?? "en";
203
+
204
+ const LEGACY_DATE_OPTIONS: Intl.DateTimeFormatOptions = { year: "numeric", month: "2-digit", day: "2-digit" };
205
+ const LEGACY_TIME_OPTIONS: Intl.DateTimeFormatOptions = { hour: "2-digit", minute: "2-digit" };
206
+
207
+ /**
208
+ * @deprecated Use `formatDateStyle` (or `useDateFormatter().format`) and
209
+ * `formatRelativeDate` (or `useDateFormatter().relative`). Kept for other nja
210
+ * apps: same default locale and the same "date" / "time" / "dateTime" output.
211
+ * Pass `now` from render code; the `new Date()` fallback is for legacy callers.
212
+ */
213
+ export const formatDate = (
214
+ eventDate: Date,
215
+ formatOption: FormatOption,
216
+ locale: string = "en-GB",
217
+ now?: Date,
218
+ ): string => {
219
+ const reference = now ?? new Date();
220
+ const diff = reference.getTime() - eventDate.getTime();
221
+
222
+ if (formatOption === "default") {
223
+ if (diff < 24 * 60 * 60 * 1000) formatOption = "timeSince";
224
+ else formatOption = "dateTime";
59
225
  }
60
226
 
61
- // Define formatting options
62
- const dateOptions: Intl.DateTimeFormatOptions = {
63
- year: "numeric",
64
- month: "2-digit",
65
- day: "2-digit",
66
- };
67
- const timeOptions: Intl.DateTimeFormatOptions = {
68
- hour: "2-digit",
69
- minute: "2-digit",
70
- };
71
-
72
- // Format based on the option
73
227
  switch (formatOption) {
228
+ case "timeSince":
229
+ return formatRelativeDate({ date: eventDate, locale, now: reference });
74
230
  case "date":
75
- return formatPart(eventDate, dateOptions);
231
+ return cachedDateTimeFormat(locale, "__legacyDate", LEGACY_DATE_OPTIONS).format(eventDate);
76
232
  case "time":
77
- return formatPart(eventDate, timeOptions);
233
+ return cachedDateTimeFormat(locale, "__legacyTime", LEGACY_TIME_OPTIONS).format(eventDate);
78
234
  case "dateTime":
79
- return `${formatPart(eventDate, dateOptions)} ${formatPart(eventDate, timeOptions)}`;
235
+ return `${cachedDateTimeFormat(locale, "__legacyDate", LEGACY_DATE_OPTIONS).format(eventDate)} ${cachedDateTimeFormat(locale, "__legacyTime", LEGACY_TIME_OPTIONS).format(eventDate)}`;
80
236
  default:
81
237
  throw new Error("Invalid format option");
82
238
  }
@@ -4,7 +4,17 @@ export { composeRefs, useComposedRefs } from "./compose-refs";
4
4
  export { useIsMobile, MOBILE_BREAKPOINT, VIEWPORT_COOKIE_NAME } from "./use-mobile";
5
5
 
6
6
  // New utilities
7
- export { dateFormatLocale, formatDate, formatLocalDate, type FormatOption } from "./date-formatter";
7
+ export {
8
+ dateFormatLocale,
9
+ formatDate,
10
+ formatDateStyle,
11
+ formatLocalDate,
12
+ formatRelativeDate,
13
+ resolveDateLocale,
14
+ usesTwelveHourClock,
15
+ type DateFormatStyle,
16
+ type FormatOption,
17
+ } from "./date-formatter";
8
18
  export { exists } from "./exists";
9
19
  export { getTableComponents, getTableOptions, TableOptions } from "./table-options";
10
20