next-formatter 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,13 +2,206 @@
2
2
  'use strict';
3
3
 
4
4
  var react = require('react');
5
- var core = require('./core');
6
5
  var jsxRuntime = require('react/jsx-runtime');
7
6
 
7
+ // src/core.ts
8
+ var DEFAULT_RULES = {
9
+ compactThreshold: 1e4,
10
+ minimumFractionDigits: 0,
11
+ maximumFractionDigits: 2,
12
+ currencyDisplay: "narrowSymbol",
13
+ numberFormat: {},
14
+ dateFormat: { year: "numeric", month: "short", day: "2-digit" },
15
+ dateTimeFormat: {
16
+ year: "numeric",
17
+ month: "short",
18
+ day: "2-digit",
19
+ hour: "2-digit",
20
+ minute: "2-digit",
21
+ hour12: true
22
+ }
23
+ };
24
+ function createFormatters(config = {}) {
25
+ const locale = config.locale ?? "en-US";
26
+ const defaultCurrency = config.currency ?? "USD";
27
+ const fallback = config.fallback ?? "\u2014";
28
+ const rules = {
29
+ ...DEFAULT_RULES,
30
+ ...config.rules
31
+ };
32
+ function toNumber(value) {
33
+ if (value == null || value === "") return null;
34
+ const n = typeof value === "number" ? value : Number(value);
35
+ return Number.isNaN(n) ? null : n;
36
+ }
37
+ function toValidDate(value) {
38
+ if (value == null || value === "") return null;
39
+ const date = value instanceof Date ? value : new Date(value);
40
+ return Number.isNaN(date.getTime()) ? null : date;
41
+ }
42
+ function isLarge(n) {
43
+ return Math.abs(n) >= rules.compactThreshold;
44
+ }
45
+ function normalizeICU(str) {
46
+ return str.replace(/\u202f/g, " ").replace(/\u00a0/g, " ").trim();
47
+ }
48
+ function alignFractionDigits(opts) {
49
+ const { minimumFractionDigits, maximumFractionDigits, maximumSignificantDigits, minimumSignificantDigits } = opts;
50
+ if (minimumFractionDigits !== void 0 && maximumFractionDigits !== void 0 && minimumFractionDigits !== maximumFractionDigits && maximumSignificantDigits === void 0 && minimumSignificantDigits === void 0) {
51
+ return { ...opts, minimumFractionDigits: maximumFractionDigits };
52
+ }
53
+ return opts;
54
+ }
55
+ const numberCache = /* @__PURE__ */ new Map();
56
+ function getNumberFormatter(options) {
57
+ const key = JSON.stringify({ locale, ...options });
58
+ if (!numberCache.has(key)) {
59
+ numberCache.set(key, new Intl.NumberFormat(locale, options));
60
+ }
61
+ return numberCache.get(key);
62
+ }
63
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto", style: "long" });
64
+ const formatters = {
65
+ /**
66
+ * Format a plain number.
67
+ * @example
68
+ * fmt.number(1234567) // "1.2M"
69
+ * fmt.number(1234, { notation: "standard" }) // "1,234"
70
+ * fmt.number(1.5678, { maximumFractionDigits: 1 }) // "1.6"
71
+ */
72
+ number(value, options = {}) {
73
+ const n = toNumber(value);
74
+ if (n == null) return fallback;
75
+ return normalizeICU(
76
+ getNumberFormatter(
77
+ alignFractionDigits({
78
+ style: "decimal",
79
+ notation: isLarge(n) ? "compact" : "standard",
80
+ minimumFractionDigits: rules.minimumFractionDigits,
81
+ maximumFractionDigits: rules.maximumFractionDigits,
82
+ ...rules.numberFormat,
83
+ ...options
84
+ })
85
+ ).format(n)
86
+ );
87
+ },
88
+ /**
89
+ * Format a currency value.
90
+ * @example
91
+ * fmt.currency(49900) // "$49.9K"
92
+ * fmt.currency(1234, { currency: "EUR" }) // "€1,234"
93
+ * fmt.currency(1234, { currencyDisplay: "code" }) // "USD 1,234"
94
+ * fmt.currency(1234, { minimumFractionDigits: 2 }) // "$1,234.00"
95
+ */
96
+ currency(value, options = {}) {
97
+ const n = toNumber(value);
98
+ if (n == null) return fallback;
99
+ const { currency: overrideCurrency, currencyDisplay, ...rest } = options;
100
+ return normalizeICU(
101
+ getNumberFormatter(
102
+ alignFractionDigits({
103
+ style: "currency",
104
+ currency: overrideCurrency ?? defaultCurrency,
105
+ notation: isLarge(n) ? "compact" : "standard",
106
+ minimumFractionDigits: rules.minimumFractionDigits,
107
+ maximumFractionDigits: rules.maximumFractionDigits,
108
+ currencyDisplay: currencyDisplay ?? rules.currencyDisplay,
109
+ ...rules.numberFormat,
110
+ ...rest
111
+ })
112
+ ).format(n)
113
+ );
114
+ },
115
+ /**
116
+ * Format a percentage. Input is treated as a raw percentage (50 → "50%").
117
+ * Uses minimumFractionDigits and maximumFractionDigits from rules.
118
+ * For values that would round to zero, maximumSignificantDigits is set to
119
+ * max(maximumFractionDigits, 4) to preserve meaningful precision.
120
+ * @example
121
+ * fmt.percentage(12.5) // "12.50%" (with default rules min=0, max=2 → aligned to 2)
122
+ * fmt.percentage(0.001) // "0.001%" (wouldBeZero → uses significantDigits)
123
+ * fmt.percentage(12.5, { maximumFractionDigits: 0 }) // "13%"
124
+ */
125
+ percentage(value, options = {}) {
126
+ const n = toNumber(value);
127
+ if (n == null) return fallback;
128
+ const normalized = n / 100;
129
+ const wouldBeZero = parseFloat(normalized.toFixed(3)) === 0;
130
+ const sigDigits = Math.max(rules.maximumFractionDigits, 4);
131
+ return getNumberFormatter({
132
+ style: "percent",
133
+ ...wouldBeZero ? { maximumSignificantDigits: sigDigits } : alignFractionDigits({
134
+ minimumFractionDigits: rules.minimumFractionDigits,
135
+ maximumFractionDigits: rules.maximumFractionDigits
136
+ }),
137
+ ...options
138
+ }).format(normalized);
139
+ },
140
+ /**
141
+ * Format a duration in seconds. Pure math — no Intl, no ICU risk.
142
+ * @example
143
+ * fmt.duration(150) // "2m 30s"
144
+ * fmt.duration(45) // "45s"
145
+ */
146
+ duration(value) {
147
+ const n = toNumber(value);
148
+ if (n == null) return fallback;
149
+ const mins = Math.floor(n / 60);
150
+ const secs = n % 60;
151
+ return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
152
+ },
153
+ /**
154
+ * Format a date.
155
+ * @example
156
+ * fmt.date("2024-01-15") // "Jan 15, 2024"
157
+ * fmt.date("2024-01-15", { dateStyle: "full" }) // "Monday, January 15, 2024"
158
+ */
159
+ date(value, options = {}) {
160
+ const date = toValidDate(value);
161
+ if (!date) return fallback;
162
+ const resolvedOptions = options.dateStyle ? options : { ...rules.dateFormat, ...options };
163
+ return normalizeICU(new Intl.DateTimeFormat(locale, resolvedOptions).format(date));
164
+ },
165
+ dateTime(value, options = {}) {
166
+ const date = toValidDate(value);
167
+ if (!date) return fallback;
168
+ const resolvedOptions = options.dateStyle || options.timeStyle ? options : { ...rules.dateTimeFormat, ...options };
169
+ return normalizeICU(new Intl.DateTimeFormat(locale, resolvedOptions).format(date));
170
+ },
171
+ /**
172
+ * Format a date as relative time.
173
+ * Always pass an explicit `now` timestamp — this ensures consistent
174
+ * output whether called from a server or client component.
175
+ * @example
176
+ * const now = Date.now();
177
+ * fmt.relativeTime("2024-01-15", now) // "3 months ago"
178
+ */
179
+ relativeTime(value, now = Date.now()) {
180
+ const date = toValidDate(value);
181
+ if (!date) return fallback;
182
+ const diffSec = Math.round((date.getTime() - now) / 1e3);
183
+ const abs = Math.abs(diffSec);
184
+ if (abs < 60) return rtf.format(diffSec, "second");
185
+ const min = Math.round(diffSec / 60);
186
+ if (Math.abs(min) < 60) return rtf.format(min, "minute");
187
+ const hr = Math.round(min / 60);
188
+ if (Math.abs(hr) < 24) return rtf.format(hr, "hour");
189
+ const day = Math.round(hr / 24);
190
+ if (Math.abs(day) < 7) return rtf.format(day, "day");
191
+ const week = Math.round(day / 7);
192
+ if (Math.abs(week) < 4) return rtf.format(week, "week");
193
+ const month = Math.round(day / 30);
194
+ if (Math.abs(month) < 12) return rtf.format(month, "month");
195
+ const year = Math.round(day / 365);
196
+ return rtf.format(year, "year");
197
+ }
198
+ };
199
+ return formatters;
200
+ }
8
201
  var FormatterContext = react.createContext(null);
9
202
  function FormattersProvider({ config, children }) {
10
203
  const formatters = react.useMemo(
11
- () => core.createFormatters(config),
204
+ () => createFormatters(config),
12
205
  // eslint-disable-next-line react-hooks/exhaustive-deps
13
206
  [config.locale, config.currency, config.fallback, JSON.stringify(config.rules)]
14
207
  );
@@ -1,20 +1,245 @@
1
1
  'use strict';
2
2
 
3
- var client = require('./client');
4
- var core = require('./core');
5
- var create = require('./create');
3
+ var react = require('react');
6
4
  var jsxRuntime = require('react/jsx-runtime');
7
5
 
8
- // src/server.tsx
6
+ // src/client.tsx
7
+
8
+ // src/core.ts
9
+ var DEFAULT_RULES = {
10
+ compactThreshold: 1e4,
11
+ minimumFractionDigits: 0,
12
+ maximumFractionDigits: 2,
13
+ currencyDisplay: "narrowSymbol",
14
+ numberFormat: {},
15
+ dateFormat: { year: "numeric", month: "short", day: "2-digit" },
16
+ dateTimeFormat: {
17
+ year: "numeric",
18
+ month: "short",
19
+ day: "2-digit",
20
+ hour: "2-digit",
21
+ minute: "2-digit",
22
+ hour12: true
23
+ }
24
+ };
25
+ function createFormatters(config = {}) {
26
+ const locale = config.locale ?? "en-US";
27
+ const defaultCurrency = config.currency ?? "USD";
28
+ const fallback = config.fallback ?? "\u2014";
29
+ const rules = {
30
+ ...DEFAULT_RULES,
31
+ ...config.rules
32
+ };
33
+ function toNumber(value) {
34
+ if (value == null || value === "") return null;
35
+ const n = typeof value === "number" ? value : Number(value);
36
+ return Number.isNaN(n) ? null : n;
37
+ }
38
+ function toValidDate(value) {
39
+ if (value == null || value === "") return null;
40
+ const date = value instanceof Date ? value : new Date(value);
41
+ return Number.isNaN(date.getTime()) ? null : date;
42
+ }
43
+ function isLarge(n) {
44
+ return Math.abs(n) >= rules.compactThreshold;
45
+ }
46
+ function normalizeICU(str) {
47
+ return str.replace(/\u202f/g, " ").replace(/\u00a0/g, " ").trim();
48
+ }
49
+ function alignFractionDigits(opts) {
50
+ const { minimumFractionDigits, maximumFractionDigits, maximumSignificantDigits, minimumSignificantDigits } = opts;
51
+ if (minimumFractionDigits !== void 0 && maximumFractionDigits !== void 0 && minimumFractionDigits !== maximumFractionDigits && maximumSignificantDigits === void 0 && minimumSignificantDigits === void 0) {
52
+ return { ...opts, minimumFractionDigits: maximumFractionDigits };
53
+ }
54
+ return opts;
55
+ }
56
+ const numberCache = /* @__PURE__ */ new Map();
57
+ function getNumberFormatter(options) {
58
+ const key = JSON.stringify({ locale, ...options });
59
+ if (!numberCache.has(key)) {
60
+ numberCache.set(key, new Intl.NumberFormat(locale, options));
61
+ }
62
+ return numberCache.get(key);
63
+ }
64
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto", style: "long" });
65
+ const formatters = {
66
+ /**
67
+ * Format a plain number.
68
+ * @example
69
+ * fmt.number(1234567) // "1.2M"
70
+ * fmt.number(1234, { notation: "standard" }) // "1,234"
71
+ * fmt.number(1.5678, { maximumFractionDigits: 1 }) // "1.6"
72
+ */
73
+ number(value, options = {}) {
74
+ const n = toNumber(value);
75
+ if (n == null) return fallback;
76
+ return normalizeICU(
77
+ getNumberFormatter(
78
+ alignFractionDigits({
79
+ style: "decimal",
80
+ notation: isLarge(n) ? "compact" : "standard",
81
+ minimumFractionDigits: rules.minimumFractionDigits,
82
+ maximumFractionDigits: rules.maximumFractionDigits,
83
+ ...rules.numberFormat,
84
+ ...options
85
+ })
86
+ ).format(n)
87
+ );
88
+ },
89
+ /**
90
+ * Format a currency value.
91
+ * @example
92
+ * fmt.currency(49900) // "$49.9K"
93
+ * fmt.currency(1234, { currency: "EUR" }) // "€1,234"
94
+ * fmt.currency(1234, { currencyDisplay: "code" }) // "USD 1,234"
95
+ * fmt.currency(1234, { minimumFractionDigits: 2 }) // "$1,234.00"
96
+ */
97
+ currency(value, options = {}) {
98
+ const n = toNumber(value);
99
+ if (n == null) return fallback;
100
+ const { currency: overrideCurrency, currencyDisplay, ...rest } = options;
101
+ return normalizeICU(
102
+ getNumberFormatter(
103
+ alignFractionDigits({
104
+ style: "currency",
105
+ currency: overrideCurrency ?? defaultCurrency,
106
+ notation: isLarge(n) ? "compact" : "standard",
107
+ minimumFractionDigits: rules.minimumFractionDigits,
108
+ maximumFractionDigits: rules.maximumFractionDigits,
109
+ currencyDisplay: currencyDisplay ?? rules.currencyDisplay,
110
+ ...rules.numberFormat,
111
+ ...rest
112
+ })
113
+ ).format(n)
114
+ );
115
+ },
116
+ /**
117
+ * Format a percentage. Input is treated as a raw percentage (50 → "50%").
118
+ * Uses minimumFractionDigits and maximumFractionDigits from rules.
119
+ * For values that would round to zero, maximumSignificantDigits is set to
120
+ * max(maximumFractionDigits, 4) to preserve meaningful precision.
121
+ * @example
122
+ * fmt.percentage(12.5) // "12.50%" (with default rules min=0, max=2 → aligned to 2)
123
+ * fmt.percentage(0.001) // "0.001%" (wouldBeZero → uses significantDigits)
124
+ * fmt.percentage(12.5, { maximumFractionDigits: 0 }) // "13%"
125
+ */
126
+ percentage(value, options = {}) {
127
+ const n = toNumber(value);
128
+ if (n == null) return fallback;
129
+ const normalized = n / 100;
130
+ const wouldBeZero = parseFloat(normalized.toFixed(3)) === 0;
131
+ const sigDigits = Math.max(rules.maximumFractionDigits, 4);
132
+ return getNumberFormatter({
133
+ style: "percent",
134
+ ...wouldBeZero ? { maximumSignificantDigits: sigDigits } : alignFractionDigits({
135
+ minimumFractionDigits: rules.minimumFractionDigits,
136
+ maximumFractionDigits: rules.maximumFractionDigits
137
+ }),
138
+ ...options
139
+ }).format(normalized);
140
+ },
141
+ /**
142
+ * Format a duration in seconds. Pure math — no Intl, no ICU risk.
143
+ * @example
144
+ * fmt.duration(150) // "2m 30s"
145
+ * fmt.duration(45) // "45s"
146
+ */
147
+ duration(value) {
148
+ const n = toNumber(value);
149
+ if (n == null) return fallback;
150
+ const mins = Math.floor(n / 60);
151
+ const secs = n % 60;
152
+ return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
153
+ },
154
+ /**
155
+ * Format a date.
156
+ * @example
157
+ * fmt.date("2024-01-15") // "Jan 15, 2024"
158
+ * fmt.date("2024-01-15", { dateStyle: "full" }) // "Monday, January 15, 2024"
159
+ */
160
+ date(value, options = {}) {
161
+ const date = toValidDate(value);
162
+ if (!date) return fallback;
163
+ const resolvedOptions = options.dateStyle ? options : { ...rules.dateFormat, ...options };
164
+ return normalizeICU(new Intl.DateTimeFormat(locale, resolvedOptions).format(date));
165
+ },
166
+ dateTime(value, options = {}) {
167
+ const date = toValidDate(value);
168
+ if (!date) return fallback;
169
+ const resolvedOptions = options.dateStyle || options.timeStyle ? options : { ...rules.dateTimeFormat, ...options };
170
+ return normalizeICU(new Intl.DateTimeFormat(locale, resolvedOptions).format(date));
171
+ },
172
+ /**
173
+ * Format a date as relative time.
174
+ * Always pass an explicit `now` timestamp — this ensures consistent
175
+ * output whether called from a server or client component.
176
+ * @example
177
+ * const now = Date.now();
178
+ * fmt.relativeTime("2024-01-15", now) // "3 months ago"
179
+ */
180
+ relativeTime(value, now = Date.now()) {
181
+ const date = toValidDate(value);
182
+ if (!date) return fallback;
183
+ const diffSec = Math.round((date.getTime() - now) / 1e3);
184
+ const abs = Math.abs(diffSec);
185
+ if (abs < 60) return rtf.format(diffSec, "second");
186
+ const min = Math.round(diffSec / 60);
187
+ if (Math.abs(min) < 60) return rtf.format(min, "minute");
188
+ const hr = Math.round(min / 60);
189
+ if (Math.abs(hr) < 24) return rtf.format(hr, "hour");
190
+ const day = Math.round(hr / 24);
191
+ if (Math.abs(day) < 7) return rtf.format(day, "day");
192
+ const week = Math.round(day / 7);
193
+ if (Math.abs(week) < 4) return rtf.format(week, "week");
194
+ const month = Math.round(day / 30);
195
+ if (Math.abs(month) < 12) return rtf.format(month, "month");
196
+ const year = Math.round(day / 365);
197
+ return rtf.format(year, "year");
198
+ }
199
+ };
200
+ return formatters;
201
+ }
202
+ var FormatterContext = react.createContext(null);
203
+ function FormattersProvider({ config, children }) {
204
+ const formatters = react.useMemo(
205
+ () => createFormatters(config),
206
+ // eslint-disable-next-line react-hooks/exhaustive-deps
207
+ [config.locale, config.currency, config.fallback, JSON.stringify(config.rules)]
208
+ );
209
+ return /* @__PURE__ */ jsxRuntime.jsx(FormatterContext.Provider, { value: formatters, children });
210
+ }
211
+
212
+ // src/create.ts
213
+ async function resolveLocale(resolver, defaultLocale) {
214
+ const resolved = await resolver?.();
215
+ if (resolved) return resolved;
216
+ if (defaultLocale) return defaultLocale;
217
+ try {
218
+ const { headers } = await import('next/headers');
219
+ const h = await headers();
220
+ const acceptLanguage = h.get("accept-language");
221
+ if (acceptLanguage) {
222
+ const locale = acceptLanguage.split(",")[0]?.trim().split(";")[0]?.trim();
223
+ if (locale) return locale;
224
+ }
225
+ } catch {
226
+ }
227
+ return "en-US";
228
+ }
229
+ async function resolveCurrency(resolver, defaultCurrency) {
230
+ const resolved = await resolver?.();
231
+ if (resolved) return resolved;
232
+ return defaultCurrency ?? "USD";
233
+ }
9
234
  async function NextFormatterProvider({ config = {}, children }) {
10
- const locale = await create.resolveLocale(config.getLocale, config.locale);
11
- const currency = await create.resolveCurrency(config.getCurrency, config.currency);
12
- return /* @__PURE__ */ jsxRuntime.jsx(client.FormattersProvider, { config: { locale, currency, fallback: config.fallback, rules: config.rules }, children });
235
+ const locale = await resolveLocale(config.getLocale, config.locale);
236
+ const currency = await resolveCurrency(config.getCurrency, config.currency);
237
+ return /* @__PURE__ */ jsxRuntime.jsx(FormattersProvider, { config: { locale, currency, fallback: config.fallback, rules: config.rules }, children });
13
238
  }
14
239
  async function getFormatter(config = {}) {
15
- const locale = await create.resolveLocale(config.getLocale, config.locale);
16
- const currency = await create.resolveCurrency(config.getCurrency, config.currency);
17
- return core.createFormatters({
240
+ const locale = await resolveLocale(config.getLocale, config.locale);
241
+ const currency = await resolveCurrency(config.getCurrency, config.currency);
242
+ return createFormatters({
18
243
  locale,
19
244
  currency,
20
245
  fallback: config.fallback,
@@ -1,8 +1,201 @@
1
1
  "use client";
2
2
  import { createContext, useMemo, useContext } from 'react';
3
- import { createFormatters } from './core';
4
3
  import { jsx } from 'react/jsx-runtime';
5
4
 
5
+ // src/core.ts
6
+ var DEFAULT_RULES = {
7
+ compactThreshold: 1e4,
8
+ minimumFractionDigits: 0,
9
+ maximumFractionDigits: 2,
10
+ currencyDisplay: "narrowSymbol",
11
+ numberFormat: {},
12
+ dateFormat: { year: "numeric", month: "short", day: "2-digit" },
13
+ dateTimeFormat: {
14
+ year: "numeric",
15
+ month: "short",
16
+ day: "2-digit",
17
+ hour: "2-digit",
18
+ minute: "2-digit",
19
+ hour12: true
20
+ }
21
+ };
22
+ function createFormatters(config = {}) {
23
+ const locale = config.locale ?? "en-US";
24
+ const defaultCurrency = config.currency ?? "USD";
25
+ const fallback = config.fallback ?? "\u2014";
26
+ const rules = {
27
+ ...DEFAULT_RULES,
28
+ ...config.rules
29
+ };
30
+ function toNumber(value) {
31
+ if (value == null || value === "") return null;
32
+ const n = typeof value === "number" ? value : Number(value);
33
+ return Number.isNaN(n) ? null : n;
34
+ }
35
+ function toValidDate(value) {
36
+ if (value == null || value === "") return null;
37
+ const date = value instanceof Date ? value : new Date(value);
38
+ return Number.isNaN(date.getTime()) ? null : date;
39
+ }
40
+ function isLarge(n) {
41
+ return Math.abs(n) >= rules.compactThreshold;
42
+ }
43
+ function normalizeICU(str) {
44
+ return str.replace(/\u202f/g, " ").replace(/\u00a0/g, " ").trim();
45
+ }
46
+ function alignFractionDigits(opts) {
47
+ const { minimumFractionDigits, maximumFractionDigits, maximumSignificantDigits, minimumSignificantDigits } = opts;
48
+ if (minimumFractionDigits !== void 0 && maximumFractionDigits !== void 0 && minimumFractionDigits !== maximumFractionDigits && maximumSignificantDigits === void 0 && minimumSignificantDigits === void 0) {
49
+ return { ...opts, minimumFractionDigits: maximumFractionDigits };
50
+ }
51
+ return opts;
52
+ }
53
+ const numberCache = /* @__PURE__ */ new Map();
54
+ function getNumberFormatter(options) {
55
+ const key = JSON.stringify({ locale, ...options });
56
+ if (!numberCache.has(key)) {
57
+ numberCache.set(key, new Intl.NumberFormat(locale, options));
58
+ }
59
+ return numberCache.get(key);
60
+ }
61
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto", style: "long" });
62
+ const formatters = {
63
+ /**
64
+ * Format a plain number.
65
+ * @example
66
+ * fmt.number(1234567) // "1.2M"
67
+ * fmt.number(1234, { notation: "standard" }) // "1,234"
68
+ * fmt.number(1.5678, { maximumFractionDigits: 1 }) // "1.6"
69
+ */
70
+ number(value, options = {}) {
71
+ const n = toNumber(value);
72
+ if (n == null) return fallback;
73
+ return normalizeICU(
74
+ getNumberFormatter(
75
+ alignFractionDigits({
76
+ style: "decimal",
77
+ notation: isLarge(n) ? "compact" : "standard",
78
+ minimumFractionDigits: rules.minimumFractionDigits,
79
+ maximumFractionDigits: rules.maximumFractionDigits,
80
+ ...rules.numberFormat,
81
+ ...options
82
+ })
83
+ ).format(n)
84
+ );
85
+ },
86
+ /**
87
+ * Format a currency value.
88
+ * @example
89
+ * fmt.currency(49900) // "$49.9K"
90
+ * fmt.currency(1234, { currency: "EUR" }) // "€1,234"
91
+ * fmt.currency(1234, { currencyDisplay: "code" }) // "USD 1,234"
92
+ * fmt.currency(1234, { minimumFractionDigits: 2 }) // "$1,234.00"
93
+ */
94
+ currency(value, options = {}) {
95
+ const n = toNumber(value);
96
+ if (n == null) return fallback;
97
+ const { currency: overrideCurrency, currencyDisplay, ...rest } = options;
98
+ return normalizeICU(
99
+ getNumberFormatter(
100
+ alignFractionDigits({
101
+ style: "currency",
102
+ currency: overrideCurrency ?? defaultCurrency,
103
+ notation: isLarge(n) ? "compact" : "standard",
104
+ minimumFractionDigits: rules.minimumFractionDigits,
105
+ maximumFractionDigits: rules.maximumFractionDigits,
106
+ currencyDisplay: currencyDisplay ?? rules.currencyDisplay,
107
+ ...rules.numberFormat,
108
+ ...rest
109
+ })
110
+ ).format(n)
111
+ );
112
+ },
113
+ /**
114
+ * Format a percentage. Input is treated as a raw percentage (50 → "50%").
115
+ * Uses minimumFractionDigits and maximumFractionDigits from rules.
116
+ * For values that would round to zero, maximumSignificantDigits is set to
117
+ * max(maximumFractionDigits, 4) to preserve meaningful precision.
118
+ * @example
119
+ * fmt.percentage(12.5) // "12.50%" (with default rules min=0, max=2 → aligned to 2)
120
+ * fmt.percentage(0.001) // "0.001%" (wouldBeZero → uses significantDigits)
121
+ * fmt.percentage(12.5, { maximumFractionDigits: 0 }) // "13%"
122
+ */
123
+ percentage(value, options = {}) {
124
+ const n = toNumber(value);
125
+ if (n == null) return fallback;
126
+ const normalized = n / 100;
127
+ const wouldBeZero = parseFloat(normalized.toFixed(3)) === 0;
128
+ const sigDigits = Math.max(rules.maximumFractionDigits, 4);
129
+ return getNumberFormatter({
130
+ style: "percent",
131
+ ...wouldBeZero ? { maximumSignificantDigits: sigDigits } : alignFractionDigits({
132
+ minimumFractionDigits: rules.minimumFractionDigits,
133
+ maximumFractionDigits: rules.maximumFractionDigits
134
+ }),
135
+ ...options
136
+ }).format(normalized);
137
+ },
138
+ /**
139
+ * Format a duration in seconds. Pure math — no Intl, no ICU risk.
140
+ * @example
141
+ * fmt.duration(150) // "2m 30s"
142
+ * fmt.duration(45) // "45s"
143
+ */
144
+ duration(value) {
145
+ const n = toNumber(value);
146
+ if (n == null) return fallback;
147
+ const mins = Math.floor(n / 60);
148
+ const secs = n % 60;
149
+ return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
150
+ },
151
+ /**
152
+ * Format a date.
153
+ * @example
154
+ * fmt.date("2024-01-15") // "Jan 15, 2024"
155
+ * fmt.date("2024-01-15", { dateStyle: "full" }) // "Monday, January 15, 2024"
156
+ */
157
+ date(value, options = {}) {
158
+ const date = toValidDate(value);
159
+ if (!date) return fallback;
160
+ const resolvedOptions = options.dateStyle ? options : { ...rules.dateFormat, ...options };
161
+ return normalizeICU(new Intl.DateTimeFormat(locale, resolvedOptions).format(date));
162
+ },
163
+ dateTime(value, options = {}) {
164
+ const date = toValidDate(value);
165
+ if (!date) return fallback;
166
+ const resolvedOptions = options.dateStyle || options.timeStyle ? options : { ...rules.dateTimeFormat, ...options };
167
+ return normalizeICU(new Intl.DateTimeFormat(locale, resolvedOptions).format(date));
168
+ },
169
+ /**
170
+ * Format a date as relative time.
171
+ * Always pass an explicit `now` timestamp — this ensures consistent
172
+ * output whether called from a server or client component.
173
+ * @example
174
+ * const now = Date.now();
175
+ * fmt.relativeTime("2024-01-15", now) // "3 months ago"
176
+ */
177
+ relativeTime(value, now = Date.now()) {
178
+ const date = toValidDate(value);
179
+ if (!date) return fallback;
180
+ const diffSec = Math.round((date.getTime() - now) / 1e3);
181
+ const abs = Math.abs(diffSec);
182
+ if (abs < 60) return rtf.format(diffSec, "second");
183
+ const min = Math.round(diffSec / 60);
184
+ if (Math.abs(min) < 60) return rtf.format(min, "minute");
185
+ const hr = Math.round(min / 60);
186
+ if (Math.abs(hr) < 24) return rtf.format(hr, "hour");
187
+ const day = Math.round(hr / 24);
188
+ if (Math.abs(day) < 7) return rtf.format(day, "day");
189
+ const week = Math.round(day / 7);
190
+ if (Math.abs(week) < 4) return rtf.format(week, "week");
191
+ const month = Math.round(day / 30);
192
+ if (Math.abs(month) < 12) return rtf.format(month, "month");
193
+ const year = Math.round(day / 365);
194
+ return rtf.format(year, "year");
195
+ }
196
+ };
197
+ return formatters;
198
+ }
6
199
  var FormatterContext = createContext(null);
7
200
  function FormattersProvider({ config, children }) {
8
201
  const formatters = useMemo(