intl-formatter 1.0.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.
@@ -0,0 +1,468 @@
1
+ const DEFAULT_RULES = {
2
+ compactThreshold: 10000,
3
+ minimumFractionDigits: 0,
4
+ maximumFractionDigits: 2,
5
+ currencyDisplay: "narrowSymbol",
6
+ numberFormat: {},
7
+ currencyFormat: {},
8
+ relativeTimeFormat: { style: "long", numeric: "auto" },
9
+ dateFormat: { year: "numeric", month: "short", day: "2-digit" },
10
+ dateTimeFormat: {
11
+ year: "numeric",
12
+ month: "short",
13
+ day: "2-digit",
14
+ hour: "2-digit",
15
+ minute: "2-digit",
16
+ hour12: true,
17
+ },
18
+ fallback: "—",
19
+ numberFallback: "",
20
+ currencyFallback: "",
21
+ percentageFallback: "",
22
+ durationFallback: "",
23
+ dateFallback: "",
24
+ dateTimeFallback: "",
25
+ relativeTimeFallback: "",
26
+ };
27
+ const DEFAULT_DURATION_LABELS = {
28
+ h: "h",
29
+ m: "m",
30
+ s: "s",
31
+ hour: { one: "hour", other: "hours" },
32
+ minute: { one: "minute", other: "minutes" },
33
+ second: { one: "second", other: "seconds" }
34
+ };
35
+ // ─── Module-Level High Performance Global Caches ──────────────────────────────
36
+ const MAX_CACHE_SIZE = 1000;
37
+ const numberFormatters = new Map();
38
+ const dateTimeFormatters = new Map();
39
+ const relativeTimeFormatters = new Map();
40
+ const pluralRulesFormatters = new Map();
41
+ function serializeOptions(locale, opts) {
42
+ const keys = Object.keys(opts);
43
+ if (keys.length === 0)
44
+ return locale;
45
+ keys.sort();
46
+ let parts = [locale];
47
+ for (let i = 0; i < keys.length; i++) {
48
+ const k = keys[i];
49
+ const v = opts[k];
50
+ if (v === undefined)
51
+ continue;
52
+ // escape backslashes, pipes, commas, and colons to prevent cache key collisions
53
+ const safeK = k.replace(/[\\|:,]/g, '\\$&');
54
+ const safeV = String(v).replace(/[\\|:,]/g, '\\$&');
55
+ parts.push(`${safeK}:${safeV}`);
56
+ }
57
+ return parts.join("|");
58
+ }
59
+ function getNumberFormatter(locale, options) {
60
+ const key = serializeOptions(locale, options);
61
+ if (numberFormatters.has(key)) {
62
+ const entry = numberFormatters.get(key);
63
+ numberFormatters.delete(key);
64
+ numberFormatters.set(key, entry);
65
+ return entry;
66
+ }
67
+ const formatter = new Intl.NumberFormat(locale, options);
68
+ if (numberFormatters.size >= MAX_CACHE_SIZE) {
69
+ const oldestKey = numberFormatters.keys().next().value;
70
+ if (oldestKey !== undefined)
71
+ numberFormatters.delete(oldestKey);
72
+ }
73
+ numberFormatters.set(key, formatter);
74
+ return formatter;
75
+ }
76
+ function getDateTimeFormatter(locale, options) {
77
+ const key = serializeOptions(locale, options);
78
+ if (dateTimeFormatters.has(key)) {
79
+ const entry = dateTimeFormatters.get(key);
80
+ dateTimeFormatters.delete(key);
81
+ dateTimeFormatters.set(key, entry);
82
+ return entry;
83
+ }
84
+ const formatter = new Intl.DateTimeFormat(locale, options);
85
+ if (dateTimeFormatters.size >= MAX_CACHE_SIZE) {
86
+ const oldestKey = dateTimeFormatters.keys().next().value;
87
+ if (oldestKey !== undefined)
88
+ dateTimeFormatters.delete(oldestKey);
89
+ }
90
+ dateTimeFormatters.set(key, formatter);
91
+ return formatter;
92
+ }
93
+ function getRelativeTimeFormatter(locale, options) {
94
+ const key = serializeOptions(locale, options);
95
+ if (relativeTimeFormatters.has(key)) {
96
+ const entry = relativeTimeFormatters.get(key);
97
+ relativeTimeFormatters.delete(key);
98
+ relativeTimeFormatters.set(key, entry);
99
+ return entry;
100
+ }
101
+ const formatter = new Intl.RelativeTimeFormat(locale, options);
102
+ if (relativeTimeFormatters.size >= MAX_CACHE_SIZE) {
103
+ const oldestKey = relativeTimeFormatters.keys().next().value;
104
+ if (oldestKey !== undefined)
105
+ relativeTimeFormatters.delete(oldestKey);
106
+ }
107
+ relativeTimeFormatters.set(key, formatter);
108
+ return formatter;
109
+ }
110
+ function getPluralRulesFormatter(locale, options) {
111
+ const key = serializeOptions(locale, options);
112
+ if (pluralRulesFormatters.has(key)) {
113
+ const entry = pluralRulesFormatters.get(key);
114
+ pluralRulesFormatters.delete(key);
115
+ pluralRulesFormatters.set(key, entry);
116
+ return entry;
117
+ }
118
+ const rulesInstance = new Intl.PluralRules(locale, options);
119
+ if (pluralRulesFormatters.size >= MAX_CACHE_SIZE) {
120
+ const oldestKey = pluralRulesFormatters.keys().next().value;
121
+ if (oldestKey !== undefined)
122
+ pluralRulesFormatters.delete(oldestKey);
123
+ }
124
+ pluralRulesFormatters.set(key, rulesInstance);
125
+ return rulesInstance;
126
+ }
127
+ export function createFormatter(config = {}) {
128
+ const locale = config.locale ?? "en-US";
129
+ let defaultCurrency = config.currency ?? "USD";
130
+ if (typeof defaultCurrency === "string") {
131
+ defaultCurrency = defaultCurrency.toUpperCase();
132
+ if (!/^[A-Z]{3}$/.test(defaultCurrency)) {
133
+ console.warn(`[intl-formatter] Invalid currency code "${defaultCurrency}". Falling back to "USD".`);
134
+ defaultCurrency = "USD";
135
+ }
136
+ }
137
+ const fallback = config.fallback ?? "—";
138
+ const rules = {
139
+ ...DEFAULT_RULES,
140
+ ...config.rules,
141
+ numberFormat: { ...DEFAULT_RULES.numberFormat, ...config.rules?.numberFormat },
142
+ currencyFormat: { ...DEFAULT_RULES.currencyFormat, ...config.rules?.currencyFormat },
143
+ relativeTimeFormat: { ...DEFAULT_RULES.relativeTimeFormat, ...config.rules?.relativeTimeFormat },
144
+ dateFormat: { ...DEFAULT_RULES.dateFormat, ...config.rules?.dateFormat },
145
+ dateTimeFormat: { ...DEFAULT_RULES.dateTimeFormat, ...config.rules?.dateTimeFormat },
146
+ };
147
+ // Specialized Fallbacks Resolution
148
+ const defaultNumberFallback = rules.numberFallback || rules.fallback || fallback;
149
+ const defaultCurrencyFallback = rules.currencyFallback || rules.fallback || fallback;
150
+ const defaultPercentageFallback = rules.percentageFallback || rules.fallback || fallback;
151
+ const defaultDurationFallback = rules.durationFallback || rules.fallback || fallback;
152
+ const defaultDateFallback = rules.dateFallback || rules.fallback || fallback;
153
+ const defaultDateTimeFallback = rules.dateTimeFallback || rules.fallback || fallback;
154
+ const defaultRelativeTimeFallback = rules.relativeTimeFallback || rules.fallback || fallback;
155
+ function toNumber(value) {
156
+ if (value == null || value === "")
157
+ return null;
158
+ try {
159
+ if (typeof value === "boolean")
160
+ return null;
161
+ if (Array.isArray(value))
162
+ return null;
163
+ const n = typeof value === "number" ? value : Number(value);
164
+ return Number.isNaN(n) ? null : n;
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ }
170
+ function toValidDate(value) {
171
+ if (value == null || value === "")
172
+ return null;
173
+ try {
174
+ if (value instanceof Date) {
175
+ return Number.isNaN(value.getTime()) ? null : value;
176
+ }
177
+ if (typeof value === "number") {
178
+ const d = new Date(value);
179
+ return Number.isNaN(d.getTime()) ? null : d;
180
+ }
181
+ if (typeof value === "string") {
182
+ const isDigitsOnly = /^-?\d+$/.test(value);
183
+ const isCalendarDate = /^(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])(?:[01]\d|2[0-3])?$/.test(value);
184
+ if (isDigitsOnly && value.length === 10 && !isCalendarDate) {
185
+ // Second UNIX timestamp: convert to milliseconds
186
+ const parsedValue = Number(value) * 1000;
187
+ const d = new Date(parsedValue);
188
+ return Number.isNaN(d.getTime()) ? null : d;
189
+ }
190
+ if (isDigitsOnly && value.length >= 12) {
191
+ // Millisecond UNIX timestamp
192
+ const parsedValue = Number(value);
193
+ const d = new Date(parsedValue);
194
+ return Number.isNaN(d.getTime()) ? null : d;
195
+ }
196
+ const d = new Date(value);
197
+ return Number.isNaN(d.getTime()) ? null : d;
198
+ }
199
+ return null;
200
+ }
201
+ catch {
202
+ return null;
203
+ }
204
+ }
205
+ function isLarge(n) {
206
+ return Math.abs(n) >= rules.compactThreshold;
207
+ }
208
+ function normalizeICU(str, localeStr) {
209
+ // Normalize both narrow no-break space and standard non-breaking space to regular space
210
+ let normalized = str.replace(/[\u202f\u00a0]/g, " ").trim();
211
+ // Only apply English-style compact suffix normalizer if locale is English to avoid breaking other languages
212
+ if (localeStr.startsWith("en")) {
213
+ normalized = normalized.replace(/(\d)\s*([kKmMbBtT])\b/g, (match, p1, p2) => p1 + p2.toUpperCase());
214
+ }
215
+ return normalized;
216
+ }
217
+ function safeFormat(formatterFn, fallbackValue) {
218
+ try {
219
+ return formatterFn();
220
+ }
221
+ catch (err) {
222
+ console.error(`[intl-formatter] Formatting failed. Returning fallback. Error:`, err);
223
+ return fallbackValue;
224
+ }
225
+ }
226
+ function resolvePluralLabel(v, form, localeStr) {
227
+ if (typeof form === "string") {
228
+ // Singular/plural backwards fallback
229
+ return v === 1 ? (form === "hours" ? "hour" : form === "minutes" ? "minute" : form === "seconds" ? "second" : form) : form;
230
+ }
231
+ // Backward compatibility for legacy option keys { singular, plural }
232
+ const legacyForm = form;
233
+ if (legacyForm.singular !== undefined || legacyForm.plural !== undefined) {
234
+ return v === 1 ? (legacyForm.singular ?? legacyForm.other) : (legacyForm.plural ?? legacyForm.other);
235
+ }
236
+ const rulesInstance = getPluralRulesFormatter(localeStr, { type: "cardinal" });
237
+ const category = rulesInstance.select(v);
238
+ return form[category] ?? form.other;
239
+ }
240
+ const formatters = {
241
+ number(value, options = {}) {
242
+ const n = toNumber(value);
243
+ const callFallback = options.fallback ?? defaultNumberFallback;
244
+ if (n == null)
245
+ return callFallback;
246
+ return safeFormat(() => {
247
+ const { fallback: _, ...rest } = options;
248
+ const resolvedOptions = {
249
+ style: "decimal",
250
+ notation: isLarge(n) ? "compact" : "standard",
251
+ minimumFractionDigits: rules.minimumFractionDigits,
252
+ maximumFractionDigits: rules.maximumFractionDigits,
253
+ ...rules.numberFormat,
254
+ ...rest,
255
+ };
256
+ return normalizeICU(getNumberFormatter(locale, resolvedOptions).format(n), locale);
257
+ }, callFallback);
258
+ },
259
+ currency(value, options = {}) {
260
+ const n = toNumber(value);
261
+ const callFallback = options.fallback ?? defaultCurrencyFallback;
262
+ if (n == null)
263
+ return callFallback;
264
+ return safeFormat(() => {
265
+ const { currency: overrideCurrency, currencyDisplay, fallback: _, ...rest } = options;
266
+ let finalCurrency = overrideCurrency ?? defaultCurrency;
267
+ if (typeof finalCurrency === "string") {
268
+ finalCurrency = finalCurrency.toUpperCase();
269
+ if (!/^[A-Z]{3}$/.test(finalCurrency)) {
270
+ console.warn(`[intl-formatter] Invalid override currency code "${finalCurrency}". Falling back to default: "${defaultCurrency}".`);
271
+ finalCurrency = defaultCurrency;
272
+ }
273
+ }
274
+ const resolvedOptions = {
275
+ style: "currency",
276
+ currency: finalCurrency,
277
+ notation: isLarge(n) ? "compact" : "standard",
278
+ minimumFractionDigits: rules.minimumFractionDigits,
279
+ maximumFractionDigits: rules.maximumFractionDigits,
280
+ currencyDisplay: currencyDisplay ?? rules.currencyDisplay,
281
+ ...rules.currencyFormat,
282
+ ...rest,
283
+ };
284
+ return normalizeICU(getNumberFormatter(locale, resolvedOptions).format(n), locale);
285
+ }, callFallback);
286
+ },
287
+ percentage(value, options = {}) {
288
+ const n = toNumber(value);
289
+ const callFallback = options.fallback ?? defaultPercentageFallback;
290
+ if (n == null)
291
+ return callFallback;
292
+ return safeFormat(() => {
293
+ const { inputMode, fallback: _, ...intlOptions } = options;
294
+ const normalized = inputMode === "fraction" ? n : n / 100;
295
+ const checkMaxFraction = intlOptions.maximumFractionDigits ?? rules.maximumFractionDigits;
296
+ const wouldBeZero = Math.abs(normalized) > 0 && parseFloat(Math.abs(normalized).toFixed(checkMaxFraction + 2)) === 0;
297
+ const sigDigits = Math.max(checkMaxFraction, 4);
298
+ const resolvedOptions = {
299
+ style: "percent",
300
+ ...(wouldBeZero
301
+ ? { maximumSignificantDigits: sigDigits }
302
+ : {
303
+ minimumFractionDigits: rules.minimumFractionDigits,
304
+ maximumFractionDigits: rules.maximumFractionDigits,
305
+ }),
306
+ ...intlOptions,
307
+ };
308
+ return normalizeICU(getNumberFormatter(locale, resolvedOptions).format(normalized), locale);
309
+ }, callFallback);
310
+ },
311
+ duration(value, options = {}) {
312
+ const n = toNumber(value);
313
+ const callFallback = options.fallback ?? defaultDurationFallback;
314
+ if (n == null)
315
+ return callFallback;
316
+ return safeFormat(() => {
317
+ const absN = Math.abs(n);
318
+ const fracDigits = options.fractionalDigits ?? 0;
319
+ const roundedAbsN = fracDigits > 0
320
+ ? Math.round(absN * Math.pow(10, fracDigits)) / Math.pow(10, fracDigits)
321
+ : Math.round(absN);
322
+ const totalSec = Math.floor(roundedAbsN);
323
+ const h = Math.floor(totalSec / 3600);
324
+ const m = Math.floor((totalSec % 3600) / 60);
325
+ let s;
326
+ let sVal;
327
+ if (fracDigits > 0) {
328
+ sVal = roundedAbsN % 60;
329
+ s = sVal.toFixed(fracDigits);
330
+ }
331
+ else {
332
+ sVal = totalSec % 60;
333
+ s = String(sVal);
334
+ }
335
+ const sign = n < 0 && absN >= 0.0001 ? "-" : "";
336
+ const userHour = options.labels?.hour;
337
+ const resolvedHour = typeof userHour === "string"
338
+ ? userHour
339
+ : { ...DEFAULT_DURATION_LABELS.hour, ...userHour };
340
+ const userMinute = options.labels?.minute;
341
+ const resolvedMinute = typeof userMinute === "string"
342
+ ? userMinute
343
+ : { ...DEFAULT_DURATION_LABELS.minute, ...userMinute };
344
+ const userSecond = options.labels?.second;
345
+ const resolvedSecond = typeof userSecond === "string"
346
+ ? userSecond
347
+ : { ...DEFAULT_DURATION_LABELS.second, ...userSecond };
348
+ const labels = {
349
+ h: options.labels?.h ?? DEFAULT_DURATION_LABELS.h,
350
+ m: options.labels?.m ?? DEFAULT_DURATION_LABELS.m,
351
+ s: options.labels?.s ?? DEFAULT_DURATION_LABELS.s,
352
+ hour: resolvedHour,
353
+ minute: resolvedMinute,
354
+ second: resolvedSecond,
355
+ };
356
+ if (options.format === "verbose") {
357
+ const parts = [];
358
+ if (h)
359
+ parts.push(`${h} ${resolvePluralLabel(h, labels.hour, locale)}`);
360
+ if (m)
361
+ parts.push(`${m} ${resolvePluralLabel(m, labels.minute, locale)}`);
362
+ if (sVal > 0 || fracDigits > 0 || !parts.length) {
363
+ parts.push(`${s} ${resolvePluralLabel(sVal, labels.second, locale)}`);
364
+ }
365
+ return sign + parts.join(", ");
366
+ }
367
+ if (h > 0)
368
+ return `${sign}${h}${labels.h} ${m}${labels.m}`;
369
+ if (m > 0)
370
+ return `${sign}${m}${labels.m} ${s}${labels.s}`;
371
+ return `${sign}${s}${labels.s}`;
372
+ }, callFallback);
373
+ },
374
+ date(value, options = {}) {
375
+ const date = toValidDate(value);
376
+ const callFallback = options.fallback ?? defaultDateFallback;
377
+ if (!date)
378
+ return callFallback;
379
+ return safeFormat(() => {
380
+ const { fallback: _, ...rest } = options;
381
+ const resolvedOptions = rest.dateStyle
382
+ ? rest
383
+ : { ...rules.dateFormat, ...rest };
384
+ return normalizeICU(getDateTimeFormatter(locale, resolvedOptions).format(date), locale);
385
+ }, callFallback);
386
+ },
387
+ dateTime(value, options = {}) {
388
+ const date = toValidDate(value);
389
+ const callFallback = options.fallback ?? defaultDateTimeFallback;
390
+ if (!date)
391
+ return callFallback;
392
+ return safeFormat(() => {
393
+ const { fallback: _, ...rest } = options;
394
+ const resolvedOptions = rest.dateStyle || rest.timeStyle
395
+ ? rest
396
+ : { ...rules.dateTimeFormat, ...rest };
397
+ return normalizeICU(getDateTimeFormatter(locale, resolvedOptions).format(date), locale);
398
+ }, callFallback);
399
+ },
400
+ relativeTime(value, optionsOrNow) {
401
+ const date = toValidDate(value);
402
+ let opts = {};
403
+ let now = Date.now();
404
+ if (typeof optionsOrNow === "number") {
405
+ now = optionsOrNow;
406
+ }
407
+ else if (optionsOrNow && typeof optionsOrNow === "object") {
408
+ const { now: overrideNow, ...rest } = optionsOrNow;
409
+ opts = rest;
410
+ if (overrideNow !== undefined) {
411
+ now = overrideNow;
412
+ }
413
+ }
414
+ const callFallback = opts.fallback ?? defaultRelativeTimeFallback;
415
+ if (!date)
416
+ return callFallback;
417
+ return safeFormat(() => {
418
+ const diffSec = Math.round((date.getTime() - now) / 1000);
419
+ const abs = Math.abs(diffSec);
420
+ const resolvedOptions = {
421
+ style: opts.style ?? rules.relativeTimeFormat?.style ?? "long",
422
+ numeric: opts.numeric ?? rules.relativeTimeFormat?.numeric ?? "auto",
423
+ };
424
+ const rtfInstance = getRelativeTimeFormatter(locale, resolvedOptions);
425
+ let formatted;
426
+ if (abs < 60) {
427
+ formatted = rtfInstance.format(diffSec, "second");
428
+ }
429
+ else {
430
+ const min = Math.round(diffSec / 60);
431
+ if (Math.abs(min) < 60) {
432
+ formatted = rtfInstance.format(min, "minute");
433
+ }
434
+ else {
435
+ const hr = Math.round(min / 60);
436
+ if (Math.abs(hr) < 24) {
437
+ formatted = rtfInstance.format(hr, "hour");
438
+ }
439
+ else {
440
+ const day = Math.round(hr / 24);
441
+ if (Math.abs(day) < 7) {
442
+ formatted = rtfInstance.format(day, "day");
443
+ }
444
+ else {
445
+ const week = Math.round(day / 7);
446
+ if (Math.abs(week) < 4) {
447
+ formatted = rtfInstance.format(week, "week");
448
+ }
449
+ else {
450
+ const month = Math.round(day / 30);
451
+ if (Math.abs(month) < 12) {
452
+ formatted = rtfInstance.format(month, "month");
453
+ }
454
+ else {
455
+ const year = Math.round(day / 365);
456
+ formatted = rtfInstance.format(year, "year");
457
+ }
458
+ }
459
+ }
460
+ }
461
+ }
462
+ }
463
+ return normalizeICU(formatted, locale);
464
+ }, callFallback);
465
+ },
466
+ };
467
+ return Object.freeze(formatters);
468
+ }
@@ -0,0 +1,164 @@
1
+ export type NumericInput = number | string | null | undefined;
2
+ export type DateInput = string | number | Date | null | undefined;
3
+ export type NumberOptions = Pick<Intl.NumberFormatOptions, "notation" | "minimumFractionDigits" | "maximumFractionDigits" | "minimumSignificantDigits" | "maximumSignificantDigits" | "minimumIntegerDigits" | "useGrouping" | "signDisplay"> & {
4
+ /** Override global fallback string for this call */
5
+ fallback?: string;
6
+ };
7
+ export type CurrencyOptions = NumberOptions & {
8
+ currency?: string;
9
+ currencyDisplay?: "symbol" | "narrowSymbol" | "code" | "name";
10
+ currencySign?: "standard" | "accounting";
11
+ };
12
+ export type PercentageOptions = Pick<Intl.NumberFormatOptions, "minimumFractionDigits" | "maximumFractionDigits" | "minimumSignificantDigits" | "maximumSignificantDigits" | "signDisplay"> & {
13
+ /** "percent" (default): 50 → "50%". "fraction": 0.5 → "50%" */
14
+ inputMode?: "percent" | "fraction";
15
+ /** Override global fallback string for this call */
16
+ fallback?: string;
17
+ };
18
+ export type DateOptions = Intl.DateTimeFormatOptions & {
19
+ /** Override global fallback string for this call */
20
+ fallback?: string;
21
+ };
22
+ export type DateTimeOptions = Intl.DateTimeFormatOptions & {
23
+ /** Override global fallback string for this call */
24
+ fallback?: string;
25
+ };
26
+ export type PluralForm = string | {
27
+ zero?: string;
28
+ one?: string;
29
+ two?: string;
30
+ few?: string;
31
+ many?: string;
32
+ other: string;
33
+ };
34
+ export type DurationLabels = {
35
+ h?: string;
36
+ m?: string;
37
+ s?: string;
38
+ hour?: PluralForm;
39
+ minute?: PluralForm;
40
+ second?: PluralForm;
41
+ };
42
+ export type DurationOptions = {
43
+ /**
44
+ * "compact" (default): produces compact output like "2m 30s" or "1h 0m".
45
+ * "verbose": produces spelled-out output like "1 hour, 1 minute, 1 second".
46
+ */
47
+ format?: "compact" | "verbose";
48
+ /** Override global fallback string for this call */
49
+ fallback?: string;
50
+ /** Custom localization labels for durations */
51
+ labels?: DurationLabels;
52
+ /** Number of decimal places for seconds. Default: 0 */
53
+ fractionalDigits?: number;
54
+ };
55
+ export type RelativeTimeOptions = {
56
+ /** The format style. Default: "long" */
57
+ style?: "long" | "short" | "narrow";
58
+ /** The numeric values display format. Default: "auto" */
59
+ numeric?: "always" | "auto";
60
+ /** Override global fallback string for this call */
61
+ fallback?: string;
62
+ };
63
+ export type FormatterRules = {
64
+ /** Numbers >= this use compact notation (1.2K, 3.4M). Default: 10000 */
65
+ compactThreshold?: number;
66
+ /** Minimum fraction digits for number, currency, percentage. Default: 0 */
67
+ minimumFractionDigits?: number;
68
+ /** Maximum fraction digits for number, currency, percentage. Default: 2 */
69
+ maximumFractionDigits?: number;
70
+ /** Default currency display style. Default: "narrowSymbol" */
71
+ currencyDisplay?: "symbol" | "narrowSymbol" | "code" | "name";
72
+ /** Default date format options */
73
+ dateFormat?: Intl.DateTimeFormatOptions;
74
+ /** Default dateTime format options */
75
+ dateTimeFormat?: Intl.DateTimeFormatOptions;
76
+ /** Default number format options — merged with per-call overrides */
77
+ numberFormat?: NumberOptions;
78
+ /** Default currency format options — merged with per-call overrides */
79
+ currencyFormat?: CurrencyOptions;
80
+ /** Default relativeTime format options */
81
+ relativeTimeFormat?: RelativeTimeOptions;
82
+ /** Specialized Default Fallbacks */
83
+ fallback?: string;
84
+ numberFallback?: string;
85
+ currencyFallback?: string;
86
+ percentageFallback?: string;
87
+ durationFallback?: string;
88
+ dateFallback?: string;
89
+ dateTimeFallback?: string;
90
+ relativeTimeFallback?: string;
91
+ };
92
+ export type FormatterConfig = {
93
+ locale?: string;
94
+ currency?: string;
95
+ fallback?: string;
96
+ rules?: FormatterRules;
97
+ };
98
+ export interface Formatter {
99
+ /**
100
+ * Format a plain number. Uses compact notation above compactThreshold.
101
+ * @example
102
+ * fmt.number(1234567) // "1.2M"
103
+ * fmt.number(1234, { notation: "standard" }) // "1,234"
104
+ * fmt.number(1.5678, { maximumFractionDigits: 1 }) // "1.6"
105
+ */
106
+ number(value: NumericInput, options?: NumberOptions): string;
107
+ /**
108
+ * Format a currency value. Uses compact notation above compactThreshold.
109
+ * @example
110
+ * fmt.currency(49900) // "$49.9K"
111
+ * fmt.currency(1234, { currency: "EUR" }) // "€1,234"
112
+ * fmt.currency(1234, { currencyDisplay: "code" }) // "USD 1,234"
113
+ * fmt.currency(1234, { minimumFractionDigits: 2 }) // "$1,234.00"
114
+ */
115
+ currency(value: NumericInput, options?: CurrencyOptions): string;
116
+ /**
117
+ * Format a percentage. Input treated as raw percentage by default (50 → "50%").
118
+ * For values that round to zero, uses significantDigits to preserve precision.
119
+ * @example
120
+ * fmt.percentage(12.5) // "12.5%"
121
+ * fmt.percentage(0.001) // "0.001%"
122
+ * fmt.percentage(0.5, { inputMode: "fraction" }) // "50%"
123
+ * fmt.percentage(12.5, { maximumFractionDigits: 0 }) // "13%"
124
+ */
125
+ percentage(value: NumericInput, options?: PercentageOptions): string;
126
+ /**
127
+ * Format a duration in seconds. Pure math — no Intl, no ICU risk.
128
+ * Output is always in English by default, but customizable via labels.
129
+ * Negative values are supported and produce a leading "-" sign.
130
+ * @example
131
+ * fmt.duration(150) // "2m 30s"
132
+ * fmt.duration(3661, { format: "verbose" }) // "1 hour, 1 minute, 1 second"
133
+ * fmt.duration(-90) // "-1m 30s"
134
+ */
135
+ duration(value: NumericInput, options?: DurationOptions): string;
136
+ /**
137
+ * Format a Date or ISO string as a human-readable date.
138
+ * @example
139
+ * fmt.date("2024-01-15") // "Jan 15, 2024"
140
+ * fmt.date("2024-01-15", { dateStyle: "full" }) // "Monday, January 15, 2024"
141
+ */
142
+ date(value: DateInput, options?: DateOptions): string;
143
+ /**
144
+ * Format a Date or ISO string as a human-readable date and time.
145
+ * @example
146
+ * fmt.dateTime("2024-01-15T14:30:00") // "Jan 15, 2024, 2:30 PM"
147
+ */
148
+ dateTime(value: DateInput, options?: DateTimeOptions): string;
149
+ /**
150
+ * Format a Date or ISO string as relative time.
151
+ * `now` defaults to `Date.now()` at call time. In Server Components, always pass
152
+ * a fixed timestamp to prevent SSR/client hydration clock drift.
153
+ * Supports an options object for custom styling or a backward-compatible timestamp.
154
+ * @example
155
+ * const now = Date.now(); // capture once per request
156
+ * fmt.relativeTime("2024-01-15T10:00:00Z", { now }) // "2 hours ago"
157
+ * fmt.relativeTime("2024-01-15T10:00:00Z", { now, style: "short" }) // "2 hr. ago"
158
+ * fmt.relativeTime(new Date()) // "just now"
159
+ */
160
+ relativeTime(value: DateInput, optionsOrNow?: (RelativeTimeOptions & {
161
+ now?: number;
162
+ }) | number): string;
163
+ }
164
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export { createFormatter } from "./core/formatter.js";
2
+ export type { Formatter, FormatterConfig } from "./core/types.js";
3
+ export type { NumericInput, DateInput, NumberOptions, CurrencyOptions, PercentageOptions, DurationOptions, DateOptions, DateTimeOptions, FormatterRules, } from "./core/types.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClE,YAAY,EACV,YAAY,EACZ,SAAS,EACT,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,eAAe,EACf,cAAc,GACf,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1 @@
1
+ export { createFormatter } from "./core/formatter.js";
@@ -0,0 +1,4 @@
1
+ import type { Formatter } from "./core/types.js";
2
+ export declare const mockFormatter: Formatter;
3
+ export type { Formatter, FormatterConfig } from "./core/types.js";
4
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EASV,MAAM,iBAAiB,CAAC;AAezB,eAAO,MAAM,aAAa,EAAE,SA0C1B,CAAC;AAEH,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC"}