next-formatter 2.0.2 → 2.0.5

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