ts-time-utils 0.0.1 → 1.1.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.
- package/README.md +590 -1
- package/dist/age.d.ts +1 -10
- package/dist/age.d.ts.map +1 -1
- package/dist/calculate.d.ts.map +1 -1
- package/dist/calculate.js +24 -10
- package/dist/constants.d.ts +2 -21
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +12 -13
- package/dist/countdown.d.ts +217 -0
- package/dist/countdown.d.ts.map +1 -0
- package/dist/countdown.js +298 -0
- package/dist/dateRange.d.ts +266 -0
- package/dist/dateRange.d.ts.map +1 -0
- package/dist/dateRange.js +433 -0
- package/dist/duration.d.ts +171 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +382 -0
- package/dist/esm/age.d.ts +1 -10
- package/dist/esm/age.d.ts.map +1 -1
- package/dist/esm/calculate.d.ts.map +1 -1
- package/dist/esm/calculate.js +24 -10
- package/dist/esm/constants.d.ts +2 -21
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.js +12 -13
- package/dist/esm/countdown.d.ts +217 -0
- package/dist/esm/countdown.d.ts.map +1 -0
- package/dist/esm/countdown.js +298 -0
- package/dist/esm/dateRange.d.ts +266 -0
- package/dist/esm/dateRange.d.ts.map +1 -0
- package/dist/esm/dateRange.js +433 -0
- package/dist/esm/duration.d.ts +171 -0
- package/dist/esm/duration.d.ts.map +1 -0
- package/dist/esm/duration.js +382 -0
- package/dist/esm/format.d.ts.map +1 -1
- package/dist/esm/index.d.ts +14 -6
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +16 -0
- package/dist/esm/interval.d.ts +3 -6
- package/dist/esm/interval.d.ts.map +1 -1
- package/dist/esm/locale.d.ts +94 -0
- package/dist/esm/locale.d.ts.map +1 -0
- package/dist/esm/locale.js +1087 -0
- package/dist/esm/naturalLanguage.d.ts +107 -0
- package/dist/esm/naturalLanguage.d.ts.map +1 -0
- package/dist/esm/naturalLanguage.js +344 -0
- package/dist/esm/performance.d.ts +2 -9
- package/dist/esm/performance.d.ts.map +1 -1
- package/dist/esm/performance.js +7 -8
- package/dist/esm/rangePresets.d.ts +7 -8
- package/dist/esm/rangePresets.d.ts.map +1 -1
- package/dist/esm/rangePresets.js +11 -9
- package/dist/esm/recurrence.d.ts +149 -0
- package/dist/esm/recurrence.d.ts.map +1 -0
- package/dist/esm/recurrence.js +404 -0
- package/dist/esm/serialize.d.ts +73 -0
- package/dist/esm/serialize.d.ts.map +1 -0
- package/dist/esm/serialize.js +365 -0
- package/dist/esm/timezone.d.ts +2 -6
- package/dist/esm/timezone.d.ts.map +1 -1
- package/dist/esm/timezone.js +1 -1
- package/dist/esm/types.d.ts +250 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +25 -0
- package/dist/esm/workingHours.d.ts +4 -13
- package/dist/esm/workingHours.d.ts.map +1 -1
- package/dist/esm/workingHours.js +3 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/index.d.ts +14 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -0
- package/dist/interval.d.ts +3 -6
- package/dist/interval.d.ts.map +1 -1
- package/dist/locale.d.ts +94 -0
- package/dist/locale.d.ts.map +1 -0
- package/dist/locale.js +1087 -0
- package/dist/naturalLanguage.d.ts +107 -0
- package/dist/naturalLanguage.d.ts.map +1 -0
- package/dist/naturalLanguage.js +344 -0
- package/dist/performance.d.ts +2 -9
- package/dist/performance.d.ts.map +1 -1
- package/dist/performance.js +7 -8
- package/dist/rangePresets.d.ts +7 -8
- package/dist/rangePresets.d.ts.map +1 -1
- package/dist/rangePresets.js +11 -9
- package/dist/recurrence.d.ts +149 -0
- package/dist/recurrence.d.ts.map +1 -0
- package/dist/recurrence.js +404 -0
- package/dist/serialize.d.ts +73 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +365 -0
- package/dist/timezone.d.ts +2 -6
- package/dist/timezone.d.ts.map +1 -1
- package/dist/timezone.js +1 -1
- package/dist/types.d.ts +250 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +25 -0
- package/dist/workingHours.d.ts +4 -13
- package/dist/workingHours.d.ts.map +1 -1
- package/dist/workingHours.js +3 -1
- package/package.json +67 -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
|
+
}
|