@bagelink/vue 1.9.192 → 1.10.1

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,63 +1,9 @@
1
- import type { DateLike } from '@vueuse/core'
2
- import type { DateTimeAcceptedFormats, TimeUnit } from '../../types/timeAgoT'
1
+ import type { TimeUnit } from '../../types/timeAgoT'
2
+ import { timeDelta, formatDate, fmtDate, handleTimezone, getDatePartsMap } from '@bagelink/utils'
3
3
  import { getI18n } from '../../i18n'
4
4
 
5
- interface TimeDeltaOptions {
6
- Day?: number
7
- Hour?: number
8
- Minute?: number
9
- Second?: number
10
- Week?: number
11
- Month?: number
12
- Year?: number
13
- days?: number
14
- hours?: number
15
- minutes?: number
16
- seconds?: number
17
- weeks?: number
18
- months?: number
19
- years?: number
20
- day?: number
21
- hour?: number
22
- minute?: number
23
- second?: number
24
- week?: number
25
- month?: number
26
- year?: number
27
- }
28
-
29
- export function timeDelta(date: string | Date, options: TimeDeltaOptions) {
30
- date = new Date(date)
31
- const day = options.Day || options.day || options.days || 0
32
- const hour = options.Hour || options.hour || options.hours || 0
33
- const minute = options.Minute || options.minute || options.minutes || 0
34
- const second = options.Second || options.second || options.seconds || 0
35
- const week = options.Week || options.week || options.weeks || 0
36
- const month = options.Month || options.month || options.months || 0
37
- const year = options.Year || options.year || options.years || 0
38
- if (day) {
39
- date.setDate(date.getDate() + day)
40
- }
41
- if (hour) {
42
- date.setHours(date.getHours() + hour)
43
- }
44
- if (minute) {
45
- date.setMinutes(date.getMinutes() + minute)
46
- }
47
- if (second) {
48
- date.setSeconds(date.getSeconds() + second)
49
- }
50
- if (week) {
51
- date.setDate(date.getDate() + week * 7)
52
- }
53
- if (month) {
54
- date.setMonth(date.getMonth() + month)
55
- }
56
- if (year) {
57
- date.setFullYear(date.getFullYear() + year)
58
- }
59
- return date
60
- }
5
+ export { fmtDate, formatDate, getDatePartsMap, handleTimezone, timeDelta }
6
+ export type { DateTimeAcceptedFormats, FormatDateOptions } from '@bagelink/utils'
61
7
 
62
8
  type TimeAgoLang = 'en' | 'es' | 'fr' | 'he' | 'it' | 'ru'
63
9
 
@@ -74,61 +20,36 @@ interface TimeAgoTranslations {
74
20
  justNow: string
75
21
  }
76
22
 
77
- /**
78
- * Get timeAgo translations from i18n instance
79
- */
80
23
  function getTimeAgoTranslations(lang: TimeAgoLang): TimeAgoTranslations {
24
+ const fallback: TimeAgoTranslations = {
25
+ year: 'year',
26
+ month: 'month',
27
+ week: 'week',
28
+ day: 'day',
29
+ hour: 'hour',
30
+ minute: 'minute',
31
+ second: 'second',
32
+ ago: 'ago',
33
+ in: 'in',
34
+ justNow: 'Just now',
35
+ }
81
36
  try {
82
37
  const i18n = getI18n()
83
38
  const messages = i18n.global.messages as Record<string, { timeAgo?: TimeAgoTranslations }>
84
39
  const localeMessages = messages[lang] || messages.en
85
- if (localeMessages?.timeAgo) {
86
- return localeMessages.timeAgo
87
- }
88
- // Fallback to English if timeAgo not found
89
- return {
90
- year: 'year',
91
- month: 'month',
92
- week: 'week',
93
- day: 'day',
94
- hour: 'hour',
95
- minute: 'minute',
96
- second: 'second',
97
- ago: 'ago',
98
- in: 'in',
99
- justNow: 'Just now',
100
- }
40
+ return localeMessages?.timeAgo ?? fallback
101
41
  } catch {
102
- // Fallback if i18n not initialized
103
- return {
104
- year: 'year',
105
- month: 'month',
106
- week: 'week',
107
- day: 'day',
108
- hour: 'hour',
109
- minute: 'minute',
110
- second: 'second',
111
- ago: 'ago',
112
- in: 'in',
113
- justNow: 'Just now',
114
- }
42
+ return fallback
115
43
  }
116
44
  }
117
45
 
118
- /**
119
- * Get current locale from i18n instance
120
- */
121
46
  function getCurrentLocale(): TimeAgoLang {
122
47
  try {
123
48
  const i18n = getI18n()
124
49
  const { locale } = i18n.global
125
50
  const lang = typeof locale === 'string' ? locale : locale.value
126
- // Map to supported timeAgo languages
127
- const supportedLangs: TimeAgoLang[] = ['en', 'es', 'fr', 'he', 'it', 'ru']
128
- if (supportedLangs.includes(lang as TimeAgoLang)) {
129
- return lang as TimeAgoLang
130
- }
131
- return 'en'
51
+ const supported: TimeAgoLang[] = ['en', 'es', 'fr', 'he', 'it', 'ru']
52
+ return supported.includes(lang as TimeAgoLang) ? lang as TimeAgoLang : 'en'
132
53
  } catch {
133
54
  return 'en'
134
55
  }
@@ -138,21 +59,16 @@ export function timeAgo(
138
59
  date: string | Date,
139
60
  langOrConfig?: TimeAgoLang | { lang?: TimeAgoLang, mode?: 'relative' | 'absolute' }
140
61
  ) {
141
- if (!date) { return '' }
142
- if (typeof date === 'string') { date = new Date(date) }
62
+ if (!date) return ''
63
+ if (typeof date === 'string') date = new Date(date)
143
64
 
144
- // Parse config - use global i18n locale if no lang specified
145
65
  const lang = typeof langOrConfig === 'string'
146
66
  ? langOrConfig
147
67
  : (langOrConfig?.lang || getCurrentLocale())
148
68
  const mode = typeof langOrConfig === 'object' ? (langOrConfig.mode || 'relative') : 'relative'
149
69
  const isRelative = mode === 'relative'
150
70
 
151
- const now: Date = new Date()
152
- const nowNumber = now.getTime()
153
- const dateNumber = date.getTime()
154
- const seconds = Math.floor((dateNumber - nowNumber) / 1000)
155
-
71
+ const seconds = Math.floor((date.getTime() - Date.now()) / 1000)
156
72
  const intervals = [
157
73
  { label: 'year', seconds: 31536000 },
158
74
  { label: 'month', seconds: 2592000 },
@@ -160,293 +76,45 @@ export function timeAgo(
160
76
  { label: 'day', seconds: 86400 },
161
77
  { label: 'hour', seconds: 3600 },
162
78
  { label: 'minute', seconds: 60 },
163
- { label: 'second', seconds: 1 }
79
+ { label: 'second', seconds: 1 },
164
80
  ]
165
81
 
166
- const selectedLang = getTimeAgoTranslations(lang)
167
-
168
- // Helper to get time unit translation
169
- const getTimeUnit = (label: string): string | TimeUnit => {
170
- const key = label as keyof TimeAgoTranslations
171
- return selectedLang[key] as string | TimeUnit
172
- }
82
+ const t = getTimeAgoTranslations(lang)
83
+ const getUnit = (label: string): string | TimeUnit => t[label as keyof TimeAgoTranslations] as string | TimeUnit
173
84
 
174
85
  for (const interval of intervals) {
175
86
  const count = Math.floor(Math.abs(seconds) / interval.seconds)
176
87
  if (count >= 1) {
177
- const suffix = isRelative && seconds < 0 ? ` ${selectedLang.ago}` : ''
178
- const prefix = isRelative && seconds > 0 && selectedLang.in !== 'in' ? `${selectedLang.in} ` : ''
88
+ const suffix = isRelative && seconds < 0 ? ` ${t.ago}` : ''
89
+ const prefix = isRelative && seconds > 0 && t.in !== 'in' ? `${t.in} ` : ''
179
90
 
180
- // Handle Hebrew plural forms
181
91
  if (lang === 'he') {
182
- const timeUnit = getTimeUnit(interval.label) as TimeUnit
183
- const form = count === 1 ? timeUnit.singular : timeUnit.plural
184
-
185
- // For Hebrew, we place the prefix/suffix before the number
186
- const hebrewPrefix = isRelative
187
- ? (seconds < 0 ? `${selectedLang.ago} ` : (seconds > 0 ? `${selectedLang.in} ` : ''))
92
+ const unit = getUnit(interval.label) as TimeUnit
93
+ const form = count === 1 ? unit.singular : unit.plural
94
+ const hePrefix = isRelative
95
+ ? (seconds < 0 ? `${t.ago} ` : (seconds > 0 ? `${t.in} ` : ''))
188
96
  : ''
189
97
 
190
98
  if (interval.label === 'day' && seconds > 0) {
191
99
  const hours = Math.floor((Math.abs(seconds) % 86400) / 3600)
192
- const hourUnit = selectedLang.hour as TimeUnit
100
+ const hourUnit = t.hour as TimeUnit
193
101
  const hourForm = hours === 1 ? hourUnit.singular : hourUnit.plural
194
- const mainPart = count === 1 ? form : `${count} ${form}`
195
- const hoursPart = hours > 0 ? (hours === 1 ? ` ${hourForm}` : ` ${hours} ${hourForm}`) : ''
196
- return `${hebrewPrefix}${mainPart}${hoursPart}`
102
+ const main = count === 1 ? form : `${count} ${form}`
103
+ const hourPart = hours > 0 ? (hours === 1 ? ` ${hourForm}` : ` ${hours} ${hourForm}`) : ''
104
+ return `${hePrefix}${main}${hourPart}`
197
105
  }
198
- return `${hebrewPrefix}${count === 1 ? form : `${count} ${form}`}`
106
+ return `${hePrefix}${count === 1 ? form : `${count} ${form}`}`
199
107
  }
200
108
 
201
- // Handle other languages
202
- const intervalLabel = getTimeUnit(interval.label) as string
109
+ const label = getUnit(interval.label) as string
203
110
  if (interval.label === 'day' && seconds > 0) {
204
111
  const hours = Math.floor((Math.abs(seconds) % 86400) / 3600)
205
- const hourLabel = hours > 1 ? `${selectedLang.hour}s` : selectedLang.hour as string
206
- return `${prefix}${count} ${intervalLabel}${hours > 0 ? ` ${hours} ${hourLabel}` : ''}${suffix}`
112
+ const hourLabel = hours > 1 ? `${t.hour}s` : t.hour as string
113
+ return `${prefix}${count} ${label}${hours > 0 ? ` ${hours} ${hourLabel}` : ''}${suffix}`
207
114
  }
208
- return `${prefix}${count} ${intervalLabel}${suffix}`
115
+ return `${prefix}${count} ${label}${suffix}`
209
116
  }
210
117
  }
211
118
 
212
- return selectedLang.justNow as string
213
- }
214
-
215
- function getBrowserNavigatorLocale(): string {
216
- if (typeof navigator !== 'object') { return 'en-US' }
217
- return navigator.languages && navigator.languages.length
218
- ? navigator.languages[0]
219
- : navigator.language
220
- }
221
-
222
- /**
223
- * Returns the active i18n locale if initialized, otherwise falls back to
224
- * the browser's navigator locale. Used as the default for formatDate.
225
- */
226
- function getActiveLocale(): string {
227
- try {
228
- const { locale } = getI18n().global
229
- return typeof locale === 'string' ? locale : locale.value
230
- } catch {
231
- return getBrowserNavigatorLocale()
232
- }
233
- }
234
-
235
- /**
236
- * Adjust a date for a specific timezone
237
- * @param date The date to adjust
238
- * @param timeZone The timezone to use (e.g., 'UTC', 'America/New_York')
239
- * @returns Date parts with timezone adjustment applied
240
- */
241
- export function handleTimezone(date: Date, intFmtOpt: Intl.DateTimeFormatOptions): Date {
242
- // If timeZone is UTC, convert to UTC directly
243
- if (intFmtOpt.timeZone === 'UTC') {
244
- const utcDate = new Date(date.getTime())
245
- utcDate.setMinutes(utcDate.getMinutes() + date.getTimezoneOffset())
246
- return utcDate
247
- }
248
-
249
- // For other timezones, use the Intl API
250
- try {
251
- // Always use en-US locale for parsing date parts to ensure Latin numerals
252
- const formatter = new Intl.DateTimeFormat('en-US', {
253
- ...intFmtOpt,
254
- month: 'numeric' // Force numeric month format
255
- })
256
-
257
- // Format the date in the target timezone
258
- const formattedParts = formatter.formatToParts(date)
259
-
260
- // Extract date components from formatted parts
261
- const parts: Record<string, number> = {}
262
-
263
- formattedParts.forEach((part) => {
264
- if (part.type !== 'literal' && part.type !== 'timeZoneName') {
265
- parts[part.type] = Number.parseInt(part.value, 10)
266
- }
267
- })
268
-
269
- // Create a new date with these components
270
- const adjustedDate = new Date(
271
- parts.year,
272
- (parts.month || 1) - 1, // Month is 0-based in JS
273
- parts.day,
274
- parts.hour || 0,
275
- parts.minute || 0,
276
- parts.second || 0
277
- )
278
-
279
- return adjustedDate
280
- } catch (error) {
281
- console.warn(`Error handling timezone ${intFmtOpt.timeZone}:`, error)
282
- return date // Return original date on error
283
- }
284
- }
285
-
286
- export function getDatePartsMap(date: Date, locale: Intl.LocalesArgument, intFmtOpt?: Intl.DateTimeFormatOptions) {
287
- // Apply timezone adjustment if specified
288
- const d = intFmtOpt?.timeZone ? handleTimezone(date, intFmtOpt) : date
289
-
290
- // Use numeric formatting for year to ensure consistency across locales
291
- const year = d.getFullYear().toString()
292
-
293
- /// keep-sorted
294
- return {
295
- AmPm: d.toLocaleString(locale, { hour: 'numeric', hour12: true, minute: 'numeric' }).split(' ')[1],
296
- DD: String(d.getDate()).padStart(2, '0'),
297
- DDD: d.toLocaleString(locale, { weekday: 'short' }),
298
- DDDD: d.toLocaleString(locale, { weekday: 'long' }),
299
- HH: String(d.getHours()).padStart(2, '0'),
300
- mm: String(d.getMinutes()).padStart(2, '0'),
301
- MM: String(d.getMonth() + 1).padStart(2, '0'),
302
- MMM: d.toLocaleString(locale, { month: 'short' }),
303
- MMMM: d.toLocaleString(locale, { month: 'long' }),
304
- ss: String(d.getSeconds()).padStart(2, '0'),
305
- sss: String(d.getMilliseconds()).padStart(3, '0'),
306
-
307
- // Always use Latin numerals for year to ensure consistency
308
- YY: year.slice(-2),
309
- YYYY: year,
310
- }
311
- }
312
-
313
- // ? Avoid creating the date parts map on every call
314
- const _datePartsMapNow = getDatePartsMap(new Date(), getBrowserNavigatorLocale())
315
-
316
- // Sort tokens by length (longest first) to avoid partial replacements
317
- const _orderedDateTokens = (
318
- Object.keys(_datePartsMapNow).sort((a, b) => b.length - a.length)
319
- ) as (keyof typeof _datePartsMapNow)[]
320
-
321
- // ? Avoid creating the token regex pattern on every call
322
- // ? no longer creating the Regex Objs it in a loop
323
- const _tokenRegExPattern = new RegExp(_orderedDateTokens.map(token => token).join('|'), 'g')
324
-
325
- export interface FormatDateOptions extends Partial<Pick<Intl.DateTimeFormatOptions, 'hour12'>> {
326
- format?: DateTimeAcceptedFormats
327
- locale?: Intl.LocalesArgument
328
- tz?: string
329
- }
330
-
331
- /**
332
- * Formats a date based on the provided format string, locale, and timezone.
333
- * @param date The date to format (string or Date object).
334
- * @param format The format string (default is 'DD.MM.YY').
335
- * @returns Formatted date string.
336
- */
337
- export function formatDate(date?: DateLike, format?: DateTimeAcceptedFormats): string
338
-
339
- /**
340
- * Formats a date based on the provided options.
341
- * @param date The date to format (string or Date object).
342
- * @param opts Options for formatting the date.
343
- * @param opts.format The format string (default is 'DD.MM.YY').
344
- * @param opts.locale The locale to use for formatting (default is browser's locale).
345
- * @param opts.tz The timezone to use for formatting (default is local timezone).
346
- * @returns Formatted date string.
347
- */
348
- export function formatDate(date?: DateLike, opts?: FormatDateOptions): string
349
-
350
- /**
351
- * Formats a date based on the provided format string, locale, and timezone.
352
- * @param date The date to format (string or Date object).
353
- * @param formatOrOpts The format string or options object.
354
- * @returns Formatted date string.
355
- */
356
- export function formatDate(
357
- date?: DateLike,
358
- formatOrOpts?: DateTimeAcceptedFormats | FormatDateOptions,
359
- ): string {
360
- let format: DateTimeAcceptedFormats | undefined
361
- let locale: Intl.LocalesArgument | undefined
362
- let timeZone: string | undefined
363
- let rest: Partial<Pick<Intl.DateTimeFormatOptions, 'hour12'>> = {}
364
- // Handle both overloads
365
- if (typeof formatOrOpts === 'string') {
366
- // First overload: format string directly
367
- format = formatOrOpts
368
- } else if (formatOrOpts && typeof formatOrOpts === 'object') {
369
- // Second overload: options object
370
- format = formatOrOpts.format
371
- locale = formatOrOpts.locale
372
- timeZone = formatOrOpts.tz
373
- rest = formatOrOpts
374
- }
375
-
376
- if (!date) { return '' }
377
-
378
- // Handle named format shortcuts before token replacement
379
- if (format === 'ISO' || format === 'ISO8601') {
380
- const d = typeof date === 'string' || typeof date === 'number' ? new Date(date) : date
381
- return Number.isNaN(d.getTime()) ? '' : d.toISOString()
382
- }
383
-
384
- format = format || 'DD.MM.YY'
385
-
386
- locale = locale || getActiveLocale()
387
- try {
388
- // Validate the date
389
- const d = typeof date === 'string' || typeof date === 'number' ? new Date(date) : date
390
-
391
- // Check if date is valid
392
- if (Number.isNaN(d.getTime())) {
393
- console.warn('Invalid date provided to formatDate:', date)
394
- return ''
395
- }
396
-
397
- // For more complex formats that need localization, use Intl.DateTimeFormat
398
- /// keep-sorted
399
- const intFmtOpt: Intl.DateTimeFormatOptions = {
400
- day: 'numeric',
401
- hour: '2-digit',
402
-
403
- // Set default hour12 to true if not explicitly set
404
- // hour12: true,
405
- hour12: rest.hour12 === undefined ? true : rest.hour12,
406
- minute: '2-digit',
407
- month: 'long',
408
- second: '2-digit',
409
- timeZone, // Add timeZone if provided
410
- weekday: 'long',
411
- year: 'numeric',
412
- }
413
-
414
- const datePartsMap = getDatePartsMap(d, locale, intFmtOpt)
415
-
416
- const formatter = new Intl.DateTimeFormat(locale, intFmtOpt)
417
-
418
- const formattedParts = formatter.formatToParts(d)
419
- const partsMap: Partial<Record<Intl.DateTimeFormatPartTypes, string>> = {}
420
-
421
- formattedParts.forEach((part) => {
422
- if (part.type !== 'literal') {
423
- partsMap[part.type] = part.value
424
- }
425
- })
426
-
427
- // Add localized formats to our map
428
- if (partsMap.month) {
429
- datePartsMap.MMM = partsMap.month.substring(0, 3)
430
- datePartsMap.MMMM = partsMap.month
431
- }
432
-
433
- if (partsMap.weekday) {
434
- datePartsMap.DDD = partsMap.weekday.substring(0, 3)
435
- datePartsMap.DDDD = partsMap.weekday
436
- }
437
-
438
- if (partsMap.dayPeriod) {
439
- datePartsMap.AmPm = partsMap.dayPeriod
440
- }
441
-
442
- // Process the format string by replacing each token with its value
443
- return format.replace(
444
- _tokenRegExPattern,
445
- match => datePartsMap[match as keyof typeof datePartsMap]
446
- )
447
- } catch (error) {
448
- console.warn(`Error formatting date: ${date} with format: ${format}`, error)
449
- return ''
450
- }
119
+ return t.justNow as string
451
120
  }
452
- export const fmtDate = formatDate