ts-time-utils 0.0.1 → 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.
Files changed (72) hide show
  1. package/README.md +365 -1
  2. package/dist/age.d.ts +1 -10
  3. package/dist/age.d.ts.map +1 -1
  4. package/dist/constants.d.ts +2 -21
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/constants.js +12 -13
  7. package/dist/duration.d.ts +171 -0
  8. package/dist/duration.d.ts.map +1 -0
  9. package/dist/duration.js +382 -0
  10. package/dist/esm/age.d.ts +1 -10
  11. package/dist/esm/age.d.ts.map +1 -1
  12. package/dist/esm/constants.d.ts +2 -21
  13. package/dist/esm/constants.d.ts.map +1 -1
  14. package/dist/esm/constants.js +12 -13
  15. package/dist/esm/duration.d.ts +171 -0
  16. package/dist/esm/duration.d.ts.map +1 -0
  17. package/dist/esm/duration.js +382 -0
  18. package/dist/esm/format.d.ts.map +1 -1
  19. package/dist/esm/index.d.ts +10 -6
  20. package/dist/esm/index.d.ts.map +1 -1
  21. package/dist/esm/index.js +8 -0
  22. package/dist/esm/interval.d.ts +3 -6
  23. package/dist/esm/interval.d.ts.map +1 -1
  24. package/dist/esm/locale.d.ts +94 -0
  25. package/dist/esm/locale.d.ts.map +1 -0
  26. package/dist/esm/locale.js +1087 -0
  27. package/dist/esm/performance.d.ts +2 -9
  28. package/dist/esm/performance.d.ts.map +1 -1
  29. package/dist/esm/performance.js +7 -8
  30. package/dist/esm/rangePresets.d.ts +7 -8
  31. package/dist/esm/rangePresets.d.ts.map +1 -1
  32. package/dist/esm/rangePresets.js +11 -9
  33. package/dist/esm/serialize.d.ts +73 -0
  34. package/dist/esm/serialize.d.ts.map +1 -0
  35. package/dist/esm/serialize.js +365 -0
  36. package/dist/esm/timezone.d.ts +2 -6
  37. package/dist/esm/timezone.d.ts.map +1 -1
  38. package/dist/esm/timezone.js +1 -1
  39. package/dist/esm/types.d.ts +229 -0
  40. package/dist/esm/types.d.ts.map +1 -0
  41. package/dist/esm/types.js +25 -0
  42. package/dist/esm/workingHours.d.ts +4 -13
  43. package/dist/esm/workingHours.d.ts.map +1 -1
  44. package/dist/esm/workingHours.js +3 -1
  45. package/dist/format.d.ts.map +1 -1
  46. package/dist/index.d.ts +10 -6
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +8 -0
  49. package/dist/interval.d.ts +3 -6
  50. package/dist/interval.d.ts.map +1 -1
  51. package/dist/locale.d.ts +94 -0
  52. package/dist/locale.d.ts.map +1 -0
  53. package/dist/locale.js +1087 -0
  54. package/dist/performance.d.ts +2 -9
  55. package/dist/performance.d.ts.map +1 -1
  56. package/dist/performance.js +7 -8
  57. package/dist/rangePresets.d.ts +7 -8
  58. package/dist/rangePresets.d.ts.map +1 -1
  59. package/dist/rangePresets.js +11 -9
  60. package/dist/serialize.d.ts +73 -0
  61. package/dist/serialize.d.ts.map +1 -0
  62. package/dist/serialize.js +365 -0
  63. package/dist/timezone.d.ts +2 -6
  64. package/dist/timezone.d.ts.map +1 -1
  65. package/dist/timezone.js +1 -1
  66. package/dist/types.d.ts +229 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/types.js +25 -0
  69. package/dist/workingHours.d.ts +4 -13
  70. package/dist/workingHours.d.ts.map +1 -1
  71. package/dist/workingHours.js +3 -1
  72. package/package.json +39 -3
package/dist/locale.js ADDED
@@ -0,0 +1,1087 @@
1
+ /**
2
+ * Internationalization and localization utilities for time formatting
3
+ */
4
+ // Default locale configurations
5
+ const DEFAULT_LOCALES = {
6
+ 'en': {
7
+ locale: 'en',
8
+ dateFormats: {
9
+ short: 'M/d/yyyy',
10
+ medium: 'MMM d, yyyy',
11
+ long: 'MMMM d, yyyy',
12
+ full: 'EEEE, MMMM d, yyyy'
13
+ },
14
+ timeFormats: {
15
+ short: 'h:mm a',
16
+ medium: 'h:mm:ss a',
17
+ long: 'h:mm:ss a z',
18
+ full: 'h:mm:ss a zzzz'
19
+ },
20
+ relativeTime: {
21
+ future: 'in {0}',
22
+ past: '{0} ago',
23
+ units: {
24
+ second: 'second',
25
+ seconds: 'seconds',
26
+ minute: 'minute',
27
+ minutes: 'minutes',
28
+ hour: 'hour',
29
+ hours: 'hours',
30
+ day: 'day',
31
+ days: 'days',
32
+ week: 'week',
33
+ weeks: 'weeks',
34
+ month: 'month',
35
+ months: 'months',
36
+ year: 'year',
37
+ years: 'years'
38
+ }
39
+ },
40
+ calendar: {
41
+ weekStartsOn: 0,
42
+ monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
43
+ monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
44
+ dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
45
+ dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
46
+ },
47
+ numbers: {
48
+ decimal: '.',
49
+ thousands: ','
50
+ }
51
+ },
52
+ 'es': {
53
+ locale: 'es',
54
+ dateFormats: {
55
+ short: 'd/M/yyyy',
56
+ medium: 'd MMM yyyy',
57
+ long: 'd \'de\' MMMM \'de\' yyyy',
58
+ full: 'EEEE, d \'de\' MMMM \'de\' yyyy'
59
+ },
60
+ timeFormats: {
61
+ short: 'H:mm',
62
+ medium: 'H:mm:ss',
63
+ long: 'H:mm:ss z',
64
+ full: 'H:mm:ss zzzz'
65
+ },
66
+ relativeTime: {
67
+ future: 'en {0}',
68
+ past: 'hace {0}',
69
+ units: {
70
+ second: 'segundo',
71
+ seconds: 'segundos',
72
+ minute: 'minuto',
73
+ minutes: 'minutos',
74
+ hour: 'hora',
75
+ hours: 'horas',
76
+ day: 'día',
77
+ days: 'días',
78
+ week: 'semana',
79
+ weeks: 'semanas',
80
+ month: 'mes',
81
+ months: 'meses',
82
+ year: 'año',
83
+ years: 'años'
84
+ }
85
+ },
86
+ calendar: {
87
+ weekStartsOn: 1,
88
+ monthNames: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
89
+ monthNamesShort: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
90
+ dayNames: ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'],
91
+ dayNamesShort: ['dom', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb']
92
+ },
93
+ numbers: {
94
+ decimal: ',',
95
+ thousands: '.'
96
+ }
97
+ },
98
+ 'fr': {
99
+ locale: 'fr',
100
+ dateFormats: {
101
+ short: 'dd/MM/yyyy',
102
+ medium: 'd MMM yyyy',
103
+ long: 'd MMMM yyyy',
104
+ full: 'EEEE d MMMM yyyy'
105
+ },
106
+ timeFormats: {
107
+ short: 'HH:mm',
108
+ medium: 'HH:mm:ss',
109
+ long: 'HH:mm:ss z',
110
+ full: 'HH:mm:ss zzzz'
111
+ },
112
+ relativeTime: {
113
+ future: 'dans {0}',
114
+ past: 'il y a {0}',
115
+ units: {
116
+ second: 'seconde',
117
+ seconds: 'secondes',
118
+ minute: 'minute',
119
+ minutes: 'minutes',
120
+ hour: 'heure',
121
+ hours: 'heures',
122
+ day: 'jour',
123
+ days: 'jours',
124
+ week: 'semaine',
125
+ weeks: 'semaines',
126
+ month: 'mois',
127
+ months: 'mois',
128
+ year: 'année',
129
+ years: 'années'
130
+ }
131
+ },
132
+ calendar: {
133
+ weekStartsOn: 1,
134
+ monthNames: ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
135
+ monthNamesShort: ['janv', 'févr', 'mars', 'avr', 'mai', 'juin', 'juil', 'août', 'sept', 'oct', 'nov', 'déc'],
136
+ dayNames: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
137
+ dayNamesShort: ['dim', 'lun', 'mar', 'mer', 'jeu', 'ven', 'sam']
138
+ },
139
+ numbers: {
140
+ decimal: ',',
141
+ thousands: ' '
142
+ }
143
+ },
144
+ 'de': {
145
+ locale: 'de',
146
+ dateFormats: {
147
+ short: 'dd.MM.yyyy',
148
+ medium: 'd. MMM yyyy',
149
+ long: 'd. MMMM yyyy',
150
+ full: 'EEEE, d. MMMM yyyy'
151
+ },
152
+ timeFormats: {
153
+ short: 'HH:mm',
154
+ medium: 'HH:mm:ss',
155
+ long: 'HH:mm:ss z',
156
+ full: 'HH:mm:ss zzzz'
157
+ },
158
+ relativeTime: {
159
+ future: 'in {0}',
160
+ past: 'vor {0}',
161
+ units: {
162
+ second: 'Sekunde',
163
+ seconds: 'Sekunden',
164
+ minute: 'Minute',
165
+ minutes: 'Minuten',
166
+ hour: 'Stunde',
167
+ hours: 'Stunden',
168
+ day: 'Tag',
169
+ days: 'Tagen',
170
+ week: 'Woche',
171
+ weeks: 'Wochen',
172
+ month: 'Monat',
173
+ months: 'Monaten',
174
+ year: 'Jahr',
175
+ years: 'Jahren'
176
+ }
177
+ },
178
+ calendar: {
179
+ weekStartsOn: 1,
180
+ monthNames: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
181
+ monthNamesShort: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
182
+ dayNames: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
183
+ dayNamesShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']
184
+ },
185
+ numbers: {
186
+ decimal: ',',
187
+ thousands: '.'
188
+ }
189
+ },
190
+ 'zh': {
191
+ locale: 'zh',
192
+ dateFormats: {
193
+ short: 'yyyy/M/d',
194
+ medium: 'yyyy年M月d日',
195
+ long: 'yyyy年M月d日',
196
+ full: 'yyyy年M月d日 EEEE'
197
+ },
198
+ timeFormats: {
199
+ short: 'H:mm',
200
+ medium: 'H:mm:ss',
201
+ long: 'H:mm:ss z',
202
+ full: 'H:mm:ss zzzz'
203
+ },
204
+ relativeTime: {
205
+ future: '{0}后',
206
+ past: '{0}前',
207
+ units: {
208
+ second: '秒',
209
+ seconds: '秒',
210
+ minute: '分钟',
211
+ minutes: '分钟',
212
+ hour: '小时',
213
+ hours: '小时',
214
+ day: '天',
215
+ days: '天',
216
+ week: '周',
217
+ weeks: '周',
218
+ month: '个月',
219
+ months: '个月',
220
+ year: '年',
221
+ years: '年'
222
+ }
223
+ },
224
+ calendar: {
225
+ weekStartsOn: 1,
226
+ monthNames: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
227
+ monthNamesShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
228
+ dayNames: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
229
+ dayNamesShort: ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
230
+ },
231
+ numbers: {
232
+ decimal: '.',
233
+ thousands: ','
234
+ }
235
+ },
236
+ 'ja': {
237
+ locale: 'ja',
238
+ dateFormats: {
239
+ short: 'yyyy/MM/dd',
240
+ medium: 'yyyy年M月d日',
241
+ long: 'yyyy年M月d日',
242
+ full: 'yyyy年M月d日 EEEE'
243
+ },
244
+ timeFormats: {
245
+ short: 'H:mm',
246
+ medium: 'H:mm:ss',
247
+ long: 'H:mm:ss z',
248
+ full: 'H:mm:ss zzzz'
249
+ },
250
+ relativeTime: {
251
+ future: '{0}後',
252
+ past: '{0}前',
253
+ units: {
254
+ second: '秒',
255
+ seconds: '秒',
256
+ minute: '分',
257
+ minutes: '分',
258
+ hour: '時間',
259
+ hours: '時間',
260
+ day: '日',
261
+ days: '日',
262
+ week: '週間',
263
+ weeks: '週間',
264
+ month: 'ヶ月',
265
+ months: 'ヶ月',
266
+ year: '年',
267
+ years: '年'
268
+ }
269
+ },
270
+ calendar: {
271
+ weekStartsOn: 0,
272
+ monthNames: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
273
+ monthNamesShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
274
+ dayNames: ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'],
275
+ dayNamesShort: ['日', '月', '火', '水', '木', '金', '土']
276
+ },
277
+ numbers: {
278
+ decimal: '.',
279
+ thousands: ','
280
+ }
281
+ },
282
+ 'fa': {
283
+ locale: 'fa',
284
+ dateFormats: {
285
+ short: 'yyyy/M/d',
286
+ medium: 'd MMM yyyy',
287
+ long: 'd MMMM yyyy',
288
+ full: 'EEEE، d MMMM yyyy'
289
+ },
290
+ timeFormats: {
291
+ short: 'H:mm',
292
+ medium: 'H:mm:ss',
293
+ long: 'H:mm:ss z',
294
+ full: 'H:mm:ss zzzz'
295
+ },
296
+ relativeTime: {
297
+ future: '{0} دیگر',
298
+ past: '{0} پیش',
299
+ units: {
300
+ second: 'ثانیه',
301
+ seconds: 'ثانیه',
302
+ minute: 'دقیقه',
303
+ minutes: 'دقیقه',
304
+ hour: 'ساعت',
305
+ hours: 'ساعت',
306
+ day: 'روز',
307
+ days: 'روز',
308
+ week: 'هفته',
309
+ weeks: 'هفته',
310
+ month: 'ماه',
311
+ months: 'ماه',
312
+ year: 'سال',
313
+ years: 'سال'
314
+ }
315
+ },
316
+ calendar: {
317
+ weekStartsOn: 6, // Saturday starts the week in Persian calendar
318
+ monthNames: ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند'],
319
+ monthNamesShort: ['فرو', 'ارد', 'خرد', 'تیر', 'مرد', 'شهر', 'مهر', 'آبا', 'آذر', 'دی', 'بهم', 'اسف'],
320
+ dayNames: ['یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه', 'شنبه'],
321
+ dayNamesShort: ['یک', 'دو', 'سه', 'چهار', 'پنج', 'جمع', 'شنب']
322
+ },
323
+ numbers: {
324
+ decimal: '.',
325
+ thousands: ','
326
+ }
327
+ },
328
+ 'nl': {
329
+ locale: 'nl',
330
+ dateFormats: {
331
+ short: 'd-M-yyyy',
332
+ medium: 'd MMM yyyy',
333
+ long: 'd MMMM yyyy',
334
+ full: 'EEEE d MMMM yyyy'
335
+ },
336
+ timeFormats: {
337
+ short: 'HH:mm',
338
+ medium: 'HH:mm:ss',
339
+ long: 'HH:mm:ss z',
340
+ full: 'HH:mm:ss zzzz'
341
+ },
342
+ relativeTime: {
343
+ future: 'over {0}',
344
+ past: '{0} geleden',
345
+ units: {
346
+ second: 'seconde',
347
+ seconds: 'seconden',
348
+ minute: 'minuut',
349
+ minutes: 'minuten',
350
+ hour: 'uur',
351
+ hours: 'uur',
352
+ day: 'dag',
353
+ days: 'dagen',
354
+ week: 'week',
355
+ weeks: 'weken',
356
+ month: 'maand',
357
+ months: 'maanden',
358
+ year: 'jaar',
359
+ years: 'jaar'
360
+ }
361
+ },
362
+ calendar: {
363
+ weekStartsOn: 1, // Monday starts the week in Netherlands
364
+ monthNames: ['januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'],
365
+ monthNamesShort: ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
366
+ dayNames: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'],
367
+ dayNamesShort: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za']
368
+ },
369
+ numbers: {
370
+ decimal: ',',
371
+ thousands: '.'
372
+ }
373
+ },
374
+ 'it': {
375
+ locale: 'it',
376
+ dateFormats: {
377
+ short: 'dd/MM/yyyy',
378
+ medium: 'd MMM yyyy',
379
+ long: 'd MMMM yyyy',
380
+ full: 'EEEE d MMMM yyyy'
381
+ },
382
+ timeFormats: {
383
+ short: 'HH:mm',
384
+ medium: 'HH:mm:ss',
385
+ long: 'HH:mm:ss z',
386
+ full: 'HH:mm:ss zzzz'
387
+ },
388
+ relativeTime: {
389
+ future: 'tra {0}',
390
+ past: '{0} fa',
391
+ units: {
392
+ second: 'secondo',
393
+ seconds: 'secondi',
394
+ minute: 'minuto',
395
+ minutes: 'minuti',
396
+ hour: 'ora',
397
+ hours: 'ore',
398
+ day: 'giorno',
399
+ days: 'giorni',
400
+ week: 'settimana',
401
+ weeks: 'settimane',
402
+ month: 'mese',
403
+ months: 'mesi',
404
+ year: 'anno',
405
+ years: 'anni'
406
+ }
407
+ },
408
+ calendar: {
409
+ weekStartsOn: 1, // Monday starts the week in Italy
410
+ monthNames: ['gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno', 'luglio', 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'],
411
+ monthNamesShort: ['gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', 'set', 'ott', 'nov', 'dic'],
412
+ dayNames: ['domenica', 'lunedì', 'martedì', 'mercoledì', 'giovedì', 'venerdì', 'sabato'],
413
+ dayNamesShort: ['dom', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab']
414
+ },
415
+ numbers: {
416
+ decimal: ',',
417
+ thousands: '.'
418
+ }
419
+ }
420
+ };
421
+ // Global locale registry
422
+ const localeRegistry = new Map(Object.entries(DEFAULT_LOCALES));
423
+ /**
424
+ * Register a custom locale configuration
425
+ */
426
+ export function registerLocale(config) {
427
+ localeRegistry.set(config.locale, config);
428
+ // Also register base language if this is a region-specific locale
429
+ const baseLang = config.locale.split('-')[0];
430
+ if (baseLang && baseLang !== config.locale && !localeRegistry.has(baseLang)) {
431
+ localeRegistry.set(baseLang, config);
432
+ }
433
+ }
434
+ /**
435
+ * Get locale configuration, with fallback to base language or English
436
+ */
437
+ export function getLocaleConfig(locale) {
438
+ // Try exact match first
439
+ if (localeRegistry.has(locale)) {
440
+ return localeRegistry.get(locale);
441
+ }
442
+ // Try base language (e.g., 'en' for 'en-US')
443
+ const baseLang = locale.split('-')[0];
444
+ if (baseLang && localeRegistry.has(baseLang)) {
445
+ return localeRegistry.get(baseLang);
446
+ }
447
+ // Fallback to English
448
+ return localeRegistry.get('en');
449
+ }
450
+ /**
451
+ * Get list of all registered locales
452
+ */
453
+ export function getSupportedLocales() {
454
+ return Array.from(localeRegistry.keys());
455
+ }
456
+ /**
457
+ * Format relative time in the specified locale
458
+ */
459
+ export function formatRelativeTime(date, options = {}) {
460
+ const { locale = 'en', maxUnit = 'years', minUnit = 'seconds', precision = 0, short = false, numeric = 'always', style = 'long' } = options;
461
+ const targetDate = normalizeDate(date);
462
+ if (!targetDate) {
463
+ throw new Error('Invalid date provided for relative time formatting');
464
+ }
465
+ const now = new Date();
466
+ const diffMs = targetDate.getTime() - now.getTime();
467
+ const isPast = diffMs < 0;
468
+ const absDiffMs = Math.abs(diffMs);
469
+ const config = getLocaleConfig(locale);
470
+ const units = getTimeUnits();
471
+ // Find the most appropriate unit
472
+ let selectedUnit = 'seconds';
473
+ let value = 0;
474
+ // Find maxUnit index to limit our search
475
+ const maxUnitIndex = units.findIndex(u => u.name === maxUnit);
476
+ const minUnitIndex = units.findIndex(u => u.name === minUnit);
477
+ // Find the most appropriate unit by iterating from largest to smallest
478
+ for (let i = 0; i < units.length; i++) {
479
+ const unit = units[i];
480
+ const unitValue = absDiffMs / unit.ms;
481
+ // Skip units larger than maxUnit
482
+ if (maxUnitIndex >= 0 && i < maxUnitIndex) {
483
+ continue;
484
+ }
485
+ // If this unit gives us a value >= 1, use it
486
+ if (unitValue >= 1) {
487
+ const roundedValue = precision > 0 ?
488
+ parseFloat(unitValue.toFixed(precision)) :
489
+ Math.round(unitValue);
490
+ selectedUnit = roundedValue === 1 ? unit.singular : unit.plural;
491
+ value = roundedValue;
492
+ break;
493
+ }
494
+ // If we've reached the minimum unit, use it even if value < 1
495
+ if (unit.name === minUnit) {
496
+ // For minimum unit, use floor to avoid rounding up very small values
497
+ const flooredValue = precision > 0 ?
498
+ parseFloat(unitValue.toFixed(precision)) :
499
+ Math.max(0, Math.floor(unitValue));
500
+ selectedUnit = flooredValue === 1 ? unit.singular : unit.plural;
501
+ value = flooredValue;
502
+ break;
503
+ }
504
+ // If we're at the last unit (seconds) and haven't broken yet, use it
505
+ if (i === units.length - 1) {
506
+ // For the last unit (seconds), use floor to be precise
507
+ const flooredValue = precision > 0 ?
508
+ parseFloat(unitValue.toFixed(precision)) :
509
+ Math.max(0, Math.floor(unitValue));
510
+ selectedUnit = flooredValue === 1 ? unit.singular : unit.plural;
511
+ value = flooredValue;
512
+ break;
513
+ }
514
+ }
515
+ // Handle special cases for numeric='auto'
516
+ if (numeric === 'auto' && Math.abs(value) <= 1) {
517
+ return getRelativeWords(selectedUnit, isPast, config, locale);
518
+ }
519
+ // Format the number
520
+ const formattedValue = formatNumber(value, config, precision > 0 ? precision : undefined);
521
+ // Get unit text
522
+ const unitText = getUnitText(selectedUnit, value, config, short, style);
523
+ // Combine value and unit - for Chinese/Japanese, no space between number and unit
524
+ const needsSpace = !short && !['zh', 'ja'].includes(locale.split('-')[0]);
525
+ const combined = needsSpace ? `${formattedValue} ${unitText}` : `${formattedValue}${unitText}`;
526
+ // Apply past/future template
527
+ const template = isPast ? config.relativeTime?.past : config.relativeTime?.future;
528
+ return template?.replace('{0}', combined) || combined;
529
+ }
530
+ /**
531
+ * Format date in locale-specific format
532
+ */
533
+ export function formatDateLocale(date, locale = 'en', style = 'medium') {
534
+ const targetDate = normalizeDate(date);
535
+ if (!targetDate) {
536
+ throw new Error('Invalid date provided for locale formatting');
537
+ }
538
+ const config = getLocaleConfig(locale);
539
+ const pattern = config.dateFormats?.[style] || config.dateFormats?.medium || 'MMM d, yyyy';
540
+ return formatWithPattern(targetDate, pattern, config);
541
+ }
542
+ /**
543
+ * Format time in locale-specific format
544
+ */
545
+ export function formatTimeLocale(date, locale = 'en', style = 'medium') {
546
+ const targetDate = normalizeDate(date);
547
+ if (!targetDate) {
548
+ throw new Error('Invalid date provided for time locale formatting');
549
+ }
550
+ const config = getLocaleConfig(locale);
551
+ const pattern = config.timeFormats?.[style] || config.timeFormats?.medium || 'h:mm:ss a';
552
+ return formatWithPattern(targetDate, pattern, config);
553
+ }
554
+ /**
555
+ * Format both date and time in locale-specific format
556
+ */
557
+ export function formatDateTimeLocale(date, locale = 'en', dateStyle = 'medium', timeStyle = 'medium') {
558
+ const dateStr = formatDateLocale(date, locale, dateStyle);
559
+ const timeStr = formatTimeLocale(date, locale, timeStyle);
560
+ // Simple concatenation - could be made more sophisticated per locale
561
+ return `${dateStr} ${timeStr}`;
562
+ }
563
+ /**
564
+ * Get localized month names
565
+ */
566
+ export function getMonthNames(locale = 'en', short = false) {
567
+ const config = getLocaleConfig(locale);
568
+ return short ?
569
+ (config.calendar?.monthNamesShort || config.calendar?.monthNames || []) :
570
+ (config.calendar?.monthNames || []);
571
+ }
572
+ /**
573
+ * Get localized day names
574
+ */
575
+ export function getDayNames(locale = 'en', short = false) {
576
+ const config = getLocaleConfig(locale);
577
+ return short ?
578
+ (config.calendar?.dayNamesShort || config.calendar?.dayNames || []) :
579
+ (config.calendar?.dayNames || []);
580
+ }
581
+ /**
582
+ * Get the first day of week for a locale (0 = Sunday, 1 = Monday, etc.)
583
+ */
584
+ export function getFirstDayOfWeek(locale = 'en') {
585
+ const config = getLocaleConfig(locale);
586
+ return config.calendar?.weekStartsOn ?? 0;
587
+ }
588
+ /**
589
+ * Check if a locale is supported
590
+ */
591
+ export function isLocaleSupported(locale) {
592
+ return localeRegistry.has(locale) || localeRegistry.has(locale.split('-')[0]);
593
+ }
594
+ /**
595
+ * Get the best matching locale from a list of preferences
596
+ */
597
+ export function getBestMatchingLocale(preferences, fallback = 'en') {
598
+ for (const pref of preferences) {
599
+ // Try exact match first
600
+ if (isLocaleSupported(pref)) {
601
+ // If it's a region-specific locale that's not in our registry,
602
+ // but the base language is, return the base language
603
+ if (localeRegistry.has(pref)) {
604
+ return pref;
605
+ }
606
+ }
607
+ // Try base language
608
+ const baseLang = pref.split('-')[0];
609
+ if (baseLang && baseLang !== pref && isLocaleSupported(baseLang)) {
610
+ return baseLang;
611
+ }
612
+ }
613
+ return fallback;
614
+ }
615
+ /**
616
+ * Auto-detect locale from browser or system (if available)
617
+ */
618
+ export function detectLocale(fallback = 'en') {
619
+ // In browser environment
620
+ if (typeof navigator !== 'undefined' && navigator.languages) {
621
+ return getBestMatchingLocale(Array.from(navigator.languages), fallback);
622
+ }
623
+ // Single language fallback
624
+ if (typeof navigator !== 'undefined' && navigator.language) {
625
+ return getBestMatchingLocale([navigator.language], fallback);
626
+ }
627
+ // Node.js environment
628
+ if (typeof globalThis !== 'undefined' &&
629
+ 'process' in globalThis &&
630
+ typeof globalThis.process === 'object' &&
631
+ globalThis.process.env) {
632
+ const env = globalThis.process.env;
633
+ const envLocales = [
634
+ env.LC_ALL,
635
+ env.LC_MESSAGES,
636
+ env.LANG,
637
+ env.LANGUAGE
638
+ ].filter(Boolean).map(loc => loc.split('.')[0]);
639
+ if (envLocales.length > 0) {
640
+ return getBestMatchingLocale(envLocales, fallback);
641
+ }
642
+ }
643
+ return fallback;
644
+ }
645
+ // Helper functions
646
+ function normalizeDate(date) {
647
+ if (date instanceof Date) {
648
+ return isNaN(date.getTime()) ? null : date;
649
+ }
650
+ if (typeof date === 'string' || typeof date === 'number') {
651
+ const parsed = new Date(date);
652
+ return isNaN(parsed.getTime()) ? null : parsed;
653
+ }
654
+ return null;
655
+ }
656
+ function getTimeUnits() {
657
+ return [
658
+ { name: 'years', singular: 'year', plural: 'years', ms: 365.25 * 24 * 60 * 60 * 1000 },
659
+ { name: 'months', singular: 'month', plural: 'months', ms: 30.44 * 24 * 60 * 60 * 1000 },
660
+ { name: 'weeks', singular: 'week', plural: 'weeks', ms: 7 * 24 * 60 * 60 * 1000 },
661
+ { name: 'days', singular: 'day', plural: 'days', ms: 24 * 60 * 60 * 1000 },
662
+ { name: 'hours', singular: 'hour', plural: 'hours', ms: 60 * 60 * 1000 },
663
+ { name: 'minutes', singular: 'minute', plural: 'minutes', ms: 60 * 1000 },
664
+ { name: 'seconds', singular: 'second', plural: 'seconds', ms: 1000 }
665
+ ];
666
+ }
667
+ function formatNumber(value, config, precision) {
668
+ let str = value.toString();
669
+ // If precision is specified and value is a whole number that should show decimals
670
+ if (precision !== undefined && precision > 0 && Number.isInteger(value)) {
671
+ str = value.toFixed(precision);
672
+ }
673
+ const decimal = config.numbers?.decimal || '.';
674
+ const thousands = config.numbers?.thousands || ',';
675
+ const parts = str.split('.');
676
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousands);
677
+ return parts.join(decimal);
678
+ }
679
+ function getUnitText(unit, value, config, short, style) {
680
+ const unitText = config.relativeTime?.units?.[unit] || unit;
681
+ if (short || style === 'short') {
682
+ // Return abbreviated form - this could be more sophisticated
683
+ const abbreviations = {
684
+ 'second': 's', 'seconds': 's',
685
+ 'minute': 'm', 'minutes': 'm',
686
+ 'hour': 'h', 'hours': 'h',
687
+ 'day': 'd', 'days': 'd',
688
+ 'week': 'w', 'weeks': 'w',
689
+ 'month': 'mo', 'months': 'mo',
690
+ 'year': 'y', 'years': 'y'
691
+ };
692
+ return abbreviations[unit] || unitText;
693
+ }
694
+ return unitText;
695
+ }
696
+ function getRelativeWords(unit, isPast, config, locale) {
697
+ // Special relative words for common cases
698
+ const specialWords = {
699
+ 'en': {
700
+ 'day': { past: 'yesterday', future: 'tomorrow' },
701
+ 'days': { past: 'yesterday', future: 'tomorrow' }
702
+ },
703
+ 'es': {
704
+ 'day': { past: 'ayer', future: 'mañana' },
705
+ 'days': { past: 'ayer', future: 'mañana' }
706
+ },
707
+ 'fr': {
708
+ 'day': { past: 'hier', future: 'demain' },
709
+ 'days': { past: 'hier', future: 'demain' }
710
+ },
711
+ 'de': {
712
+ 'day': { past: 'gestern', future: 'morgen' },
713
+ 'days': { past: 'gestern', future: 'morgen' }
714
+ },
715
+ 'fa': {
716
+ 'day': { past: 'دیروز', future: 'فردا' },
717
+ 'days': { past: 'دیروز', future: 'فردا' }
718
+ },
719
+ 'nl': {
720
+ 'day': { past: 'gisteren', future: 'morgen' },
721
+ 'days': { past: 'gisteren', future: 'morgen' }
722
+ },
723
+ 'it': {
724
+ 'day': { past: 'ieri', future: 'domani' },
725
+ 'days': { past: 'ieri', future: 'domani' }
726
+ }
727
+ };
728
+ const baseLang = locale.split('-')[0];
729
+ const words = specialWords[baseLang]?.[unit];
730
+ if (words) {
731
+ return isPast ? words.past : words.future;
732
+ }
733
+ // Fallback to regular format
734
+ const unitText = config.relativeTime?.units?.[unit] || unit;
735
+ const template = isPast ? config.relativeTime?.past : config.relativeTime?.future;
736
+ return template?.replace('{0}', `1 ${unitText}`) || `1 ${unitText}`;
737
+ }
738
+ function formatWithPattern(date, pattern, config) {
739
+ const formatMap = {
740
+ 'yyyy': date.getFullYear().toString(),
741
+ 'MMMM': config.calendar?.monthNames?.[date.getMonth()] || (date.getMonth() + 1).toString(),
742
+ 'MMM': config.calendar?.monthNamesShort?.[date.getMonth()] || (date.getMonth() + 1).toString(),
743
+ 'MM': (date.getMonth() + 1).toString().padStart(2, '0'),
744
+ 'M': (date.getMonth() + 1).toString(),
745
+ 'dd': date.getDate().toString().padStart(2, '0'),
746
+ 'd': date.getDate().toString(),
747
+ 'EEEE': config.calendar?.dayNames?.[date.getDay()] || date.getDay().toString(),
748
+ 'HH': date.getHours().toString().padStart(2, '0'),
749
+ 'H': date.getHours().toString(),
750
+ 'h': ((date.getHours() % 12) || 12).toString(),
751
+ 'mm': date.getMinutes().toString().padStart(2, '0'),
752
+ 'ss': date.getSeconds().toString().padStart(2, '0'),
753
+ 'a': date.getHours() < 12 ? 'AM' : 'PM'
754
+ };
755
+ // Sort by length (longest first) to handle overlapping tokens
756
+ const tokens = Object.keys(formatMap).sort((a, b) => b.length - a.length);
757
+ let result = pattern;
758
+ for (const token of tokens) {
759
+ // Replace token only when it appears as a complete token (not part of another)
760
+ // Use a more sophisticated approach to avoid conflicts
761
+ const tokenRegex = new RegExp(`(?<!\\w)${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?!\\w)`, 'g');
762
+ result = result.replace(tokenRegex, formatMap[token]);
763
+ }
764
+ return result;
765
+ }
766
+ // ========================================
767
+ // LOCALE CONVERSION UTILITIES
768
+ // ========================================
769
+ /**
770
+ * Convert a relative time string from one locale to another
771
+ * Attempts to parse the relative time and reformat in target locale
772
+ */
773
+ export function convertRelativeTime(relativeTimeString, fromLocale, toLocale) {
774
+ if (fromLocale === toLocale) {
775
+ return relativeTimeString;
776
+ }
777
+ const parsedTime = parseRelativeTime(relativeTimeString, fromLocale);
778
+ if (!parsedTime) {
779
+ return null;
780
+ }
781
+ return formatRelativeTime(parsedTime.date, {
782
+ locale: toLocale,
783
+ maxUnit: parsedTime.unit,
784
+ precision: parsedTime.precision,
785
+ short: parsedTime.isShort,
786
+ numeric: parsedTime.numeric
787
+ });
788
+ }
789
+ /**
790
+ * Detect the locale of a formatted relative time string
791
+ * Returns the most likely locale or null if detection fails
792
+ */
793
+ export function detectLocaleFromRelativeTime(relativeTimeString) {
794
+ const supportedLocales = getSupportedLocales();
795
+ for (const locale of supportedLocales) {
796
+ if (parseRelativeTime(relativeTimeString, locale)) {
797
+ return locale;
798
+ }
799
+ }
800
+ return null;
801
+ }
802
+ /**
803
+ * Convert a date format pattern from one locale's convention to another
804
+ */
805
+ export function convertFormatPattern(pattern, fromLocale, toLocale, style) {
806
+ if (fromLocale === toLocale) {
807
+ return pattern;
808
+ }
809
+ const toConfig = getLocaleConfig(toLocale);
810
+ // If style is specified, return the target locale's pattern for that style
811
+ if (style && toConfig.dateFormats?.[style]) {
812
+ return toConfig.dateFormats[style];
813
+ }
814
+ // Try to map common patterns between locales
815
+ const patternMappings = {
816
+ 'en': {
817
+ 'M/d/yyyy': 'short',
818
+ 'MMM d, yyyy': 'medium',
819
+ 'MMMM d, yyyy': 'long',
820
+ 'EEEE, MMMM d, yyyy': 'full'
821
+ },
822
+ 'es': {
823
+ 'd/M/yyyy': 'short',
824
+ 'd MMM yyyy': 'medium',
825
+ 'd \'de\' MMMM \'de\' yyyy': 'long',
826
+ 'EEEE, d \'de\' MMMM \'de\' yyyy': 'full'
827
+ },
828
+ 'fr': {
829
+ 'dd/MM/yyyy': 'short',
830
+ 'd MMM yyyy': 'medium',
831
+ 'd MMMM yyyy': 'long',
832
+ 'EEEE d MMMM yyyy': 'full'
833
+ },
834
+ 'de': {
835
+ 'd.M.yyyy': 'short',
836
+ 'd. MMM yyyy': 'medium',
837
+ 'd. MMMM yyyy': 'long',
838
+ 'EEEE, d. MMMM yyyy': 'full'
839
+ }
840
+ };
841
+ // Find matching style from source pattern
842
+ const fromMappings = patternMappings[fromLocale];
843
+ if (fromMappings) {
844
+ const matchedStyle = fromMappings[pattern];
845
+ if (matchedStyle && toConfig.dateFormats?.[matchedStyle]) {
846
+ return toConfig.dateFormats[matchedStyle];
847
+ }
848
+ }
849
+ // Fallback: return target locale's medium format
850
+ return toConfig.dateFormats?.medium || pattern;
851
+ }
852
+ /**
853
+ * Convert a formatted date string from one locale to another
854
+ * Attempts to parse the date and reformat in target locale
855
+ */
856
+ export function convertFormattedDate(formattedDate, fromLocale, toLocale, targetStyle) {
857
+ if (fromLocale === toLocale) {
858
+ return formattedDate;
859
+ }
860
+ const parsedDate = parseFormattedDate(formattedDate, fromLocale);
861
+ if (!parsedDate) {
862
+ return null;
863
+ }
864
+ return formatDateLocale(parsedDate, toLocale, targetStyle || 'medium');
865
+ }
866
+ /**
867
+ * Bulk convert an array of relative time strings to a different locale
868
+ */
869
+ export function convertRelativeTimeArray(relativeTimeStrings, fromLocale, toLocale) {
870
+ return relativeTimeStrings.map(str => convertRelativeTime(str, fromLocale, toLocale));
871
+ }
872
+ /**
873
+ * Get format pattern differences between two locales
874
+ */
875
+ export function compareLocaleFormats(locale1, locale2) {
876
+ const config1 = getLocaleConfig(locale1);
877
+ const config2 = getLocaleConfig(locale2);
878
+ const result = {
879
+ dateFormats: {},
880
+ timeFormats: {},
881
+ weekStartsOn: {
882
+ locale1: config1.calendar?.weekStartsOn || 0,
883
+ locale2: config2.calendar?.weekStartsOn || 0
884
+ }
885
+ };
886
+ // Compare date formats
887
+ const styles = ['short', 'medium', 'long', 'full'];
888
+ for (const style of styles) {
889
+ if (config1.dateFormats?.[style] || config2.dateFormats?.[style]) {
890
+ result.dateFormats[style] = {
891
+ locale1: config1.dateFormats?.[style] || 'N/A',
892
+ locale2: config2.dateFormats?.[style] || 'N/A'
893
+ };
894
+ }
895
+ }
896
+ // Compare time formats
897
+ for (const style of styles) {
898
+ if (config1.timeFormats?.[style] || config2.timeFormats?.[style]) {
899
+ result.timeFormats[style] = {
900
+ locale1: config1.timeFormats?.[style] || 'N/A',
901
+ locale2: config2.timeFormats?.[style] || 'N/A'
902
+ };
903
+ }
904
+ }
905
+ return result;
906
+ }
907
+ // ========================================
908
+ // HELPER FUNCTIONS FOR CONVERSIONS
909
+ // ========================================
910
+ /**
911
+ * Parse a relative time string and extract its components
912
+ */
913
+ function parseRelativeTime(relativeTimeString, locale) {
914
+ const config = getLocaleConfig(locale);
915
+ const trimmed = relativeTimeString.trim();
916
+ // Try simple patterns first: "2 hours ago", "hace 2 horas", etc.
917
+ const pastTemplate = config.relativeTime?.past || '{0} ago';
918
+ const futureTemplate = config.relativeTime?.future || 'in {0}';
919
+ // Check if it matches past pattern
920
+ const pastPrefix = pastTemplate.split('{0}')[0];
921
+ const pastSuffix = pastTemplate.split('{0}')[1] || '';
922
+ const futurePrefix = futureTemplate.split('{0}')[0];
923
+ const futureSuffix = futureTemplate.split('{0}')[1] || '';
924
+ let valueAndUnit = '';
925
+ let isPast = false;
926
+ // Try to extract the value and unit part
927
+ if (pastPrefix && trimmed.startsWith(pastPrefix.trim())) {
928
+ const remaining = trimmed.substring(pastPrefix.trim().length).trim();
929
+ if (!pastSuffix || remaining.endsWith(pastSuffix.trim())) {
930
+ valueAndUnit = pastSuffix ? remaining.substring(0, remaining.length - pastSuffix.trim().length).trim() : remaining;
931
+ isPast = true;
932
+ }
933
+ }
934
+ else if (pastSuffix && trimmed.endsWith(pastSuffix.trim())) {
935
+ const remaining = trimmed.substring(0, trimmed.length - pastSuffix.trim().length).trim();
936
+ if (!pastPrefix || remaining.startsWith(pastPrefix.trim())) {
937
+ valueAndUnit = pastPrefix ? remaining.substring(pastPrefix.trim().length).trim() : remaining;
938
+ isPast = true;
939
+ }
940
+ }
941
+ else if (futurePrefix && trimmed.startsWith(futurePrefix.trim())) {
942
+ const remaining = trimmed.substring(futurePrefix.trim().length).trim();
943
+ if (!futureSuffix || remaining.endsWith(futureSuffix.trim())) {
944
+ valueAndUnit = futureSuffix ? remaining.substring(0, remaining.length - futureSuffix.trim().length).trim() : remaining;
945
+ isPast = false;
946
+ }
947
+ }
948
+ else if (futureSuffix && trimmed.endsWith(futureSuffix.trim())) {
949
+ const remaining = trimmed.substring(0, trimmed.length - futureSuffix.trim().length).trim();
950
+ if (!futurePrefix || remaining.startsWith(futurePrefix.trim())) {
951
+ valueAndUnit = futurePrefix ? remaining.substring(futurePrefix.trim().length).trim() : remaining;
952
+ isPast = false;
953
+ }
954
+ }
955
+ if (!valueAndUnit)
956
+ return null;
957
+ // Extract number and unit from something like "2 hours" or "2h"
958
+ const match = valueAndUnit.match(/^(\d+(?:\.\d+)?)\s*(.+)$/);
959
+ if (!match)
960
+ return null;
961
+ const value = parseFloat(match[1]);
962
+ const unitText = match[2].trim();
963
+ if (isNaN(value))
964
+ return null;
965
+ // Find matching unit
966
+ const unit = findRelativeTimeUnit(unitText, config);
967
+ if (!unit)
968
+ return null;
969
+ // Calculate the date
970
+ const now = new Date();
971
+ const unitMs = getUnitMilliseconds(unit);
972
+ const offsetMs = value * unitMs * (isPast ? -1 : 1);
973
+ const date = new Date(now.getTime() + offsetMs);
974
+ return {
975
+ date,
976
+ unit,
977
+ precision: value % 1 === 0 ? 0 : 1,
978
+ isShort: unitText.length <= 2, // heuristic for short format like "h", "m", "d"
979
+ numeric: 'always'
980
+ };
981
+ }
982
+ /**
983
+ * Parse a formatted date string using locale-specific patterns
984
+ */
985
+ function parseFormattedDate(formattedDate, locale) {
986
+ const config = getLocaleConfig(locale);
987
+ const trimmed = formattedDate.trim();
988
+ // Try different date format patterns
989
+ const patterns = Object.values(config.dateFormats || {});
990
+ for (const pattern of patterns) {
991
+ const date = tryParseWithPattern(trimmed, pattern, config);
992
+ if (date) {
993
+ return date;
994
+ }
995
+ }
996
+ return null;
997
+ }
998
+ /**
999
+ * Find a RelativeTimeUnit from unit text
1000
+ */
1001
+ function findRelativeTimeUnit(unitText, config) {
1002
+ const units = config.relativeTime?.units;
1003
+ if (!units)
1004
+ return null;
1005
+ // Check exact matches first
1006
+ for (const [key, value] of Object.entries(units)) {
1007
+ if (value === unitText) {
1008
+ return key;
1009
+ }
1010
+ }
1011
+ // Check abbreviations for English and other common cases
1012
+ const abbreviations = {
1013
+ // English abbreviations
1014
+ 's': 'seconds',
1015
+ 'sec': 'seconds',
1016
+ 'secs': 'seconds',
1017
+ 'm': 'minutes',
1018
+ 'min': 'minutes',
1019
+ 'mins': 'minutes',
1020
+ 'h': 'hours',
1021
+ 'hr': 'hours',
1022
+ 'hrs': 'hours',
1023
+ 'd': 'days',
1024
+ 'day': 'day',
1025
+ 'days': 'days',
1026
+ 'w': 'weeks',
1027
+ 'wk': 'weeks',
1028
+ 'wks': 'weeks',
1029
+ 'mo': 'months',
1030
+ 'mos': 'months',
1031
+ 'y': 'years',
1032
+ 'yr': 'years',
1033
+ 'yrs': 'years',
1034
+ // Persian abbreviations
1035
+ 'ث': 'seconds',
1036
+ 'د': 'minutes',
1037
+ 'س': 'hours',
1038
+ 'ر': 'days',
1039
+ 'ه': 'weeks',
1040
+ 'م': 'months',
1041
+ 'ل': 'years'
1042
+ };
1043
+ return abbreviations[unitText] || null;
1044
+ }
1045
+ /**
1046
+ * Get milliseconds for a time unit
1047
+ */
1048
+ function getUnitMilliseconds(unit) {
1049
+ const unitMap = {
1050
+ 'second': 1000,
1051
+ 'seconds': 1000,
1052
+ 'minute': 60 * 1000,
1053
+ 'minutes': 60 * 1000,
1054
+ 'hour': 60 * 60 * 1000,
1055
+ 'hours': 60 * 60 * 1000,
1056
+ 'day': 24 * 60 * 60 * 1000,
1057
+ 'days': 24 * 60 * 60 * 1000,
1058
+ 'week': 7 * 24 * 60 * 60 * 1000,
1059
+ 'weeks': 7 * 24 * 60 * 60 * 1000,
1060
+ 'month': 30.44 * 24 * 60 * 60 * 1000,
1061
+ 'months': 30.44 * 24 * 60 * 60 * 1000,
1062
+ 'year': 365.25 * 24 * 60 * 60 * 1000,
1063
+ 'years': 365.25 * 24 * 60 * 60 * 1000
1064
+ };
1065
+ return unitMap[unit] || 1000;
1066
+ }
1067
+ /**
1068
+ * Try to parse a date string with a specific pattern
1069
+ */
1070
+ function tryParseWithPattern(dateString, pattern, config) {
1071
+ // This is a simplified parser - could be made more sophisticated
1072
+ // For now, try common patterns
1073
+ if (pattern === 'M/d/yyyy' || pattern === 'd/M/yyyy') {
1074
+ const parts = dateString.split('/');
1075
+ if (parts.length === 3) {
1076
+ const [first, second, year] = parts.map(p => parseInt(p, 10));
1077
+ if (!isNaN(first) && !isNaN(second) && !isNaN(year)) {
1078
+ const month = pattern === 'M/d/yyyy' ? first - 1 : second - 1;
1079
+ const day = pattern === 'M/d/yyyy' ? second : first;
1080
+ return new Date(year, month, day);
1081
+ }
1082
+ }
1083
+ }
1084
+ // Try ISO date parsing as fallback
1085
+ const isoDate = new Date(dateString);
1086
+ return isNaN(isoDate.getTime()) ? null : isoDate;
1087
+ }