@lukso/core 0.1.0-dev.0f1bea5

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 (95) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +159 -0
  3. package/dist/chunk-3WGYJTN4.js +19 -0
  4. package/dist/chunk-3WGYJTN4.js.map +1 -0
  5. package/dist/chunk-4TNWG4ME.js +106 -0
  6. package/dist/chunk-4TNWG4ME.js.map +1 -0
  7. package/dist/chunk-AMRGSLR5.cjs +1 -0
  8. package/dist/chunk-AMRGSLR5.cjs.map +1 -0
  9. package/dist/chunk-CC3LFUYY.cjs +19 -0
  10. package/dist/chunk-CC3LFUYY.cjs.map +1 -0
  11. package/dist/chunk-DFMMMF62.cjs +1 -0
  12. package/dist/chunk-DFMMMF62.cjs.map +1 -0
  13. package/dist/chunk-DKEXQFNE.js +1 -0
  14. package/dist/chunk-DKEXQFNE.js.map +1 -0
  15. package/dist/chunk-DKXHVRHM.js +84 -0
  16. package/dist/chunk-DKXHVRHM.js.map +1 -0
  17. package/dist/chunk-FR74YPGJ.cjs +87 -0
  18. package/dist/chunk-FR74YPGJ.cjs.map +1 -0
  19. package/dist/chunk-LEL6VWU4.js +1 -0
  20. package/dist/chunk-LEL6VWU4.js.map +1 -0
  21. package/dist/chunk-MBIRTPNM.cjs +84 -0
  22. package/dist/chunk-MBIRTPNM.cjs.map +1 -0
  23. package/dist/chunk-NJQVWIZL.cjs +49 -0
  24. package/dist/chunk-NJQVWIZL.cjs.map +1 -0
  25. package/dist/chunk-RM42NG7E.cjs +106 -0
  26. package/dist/chunk-RM42NG7E.cjs.map +1 -0
  27. package/dist/chunk-SV4TVR2K.js +87 -0
  28. package/dist/chunk-SV4TVR2K.js.map +1 -0
  29. package/dist/chunk-X2QNFZU7.js +49 -0
  30. package/dist/chunk-X2QNFZU7.js.map +1 -0
  31. package/dist/index.cjs +37 -0
  32. package/dist/index.cjs.map +1 -0
  33. package/dist/index.d.cts +8 -0
  34. package/dist/index.d.ts +8 -0
  35. package/dist/index.js +37 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/mixins/device.cjs +8 -0
  38. package/dist/mixins/device.cjs.map +1 -0
  39. package/dist/mixins/device.d.cts +34 -0
  40. package/dist/mixins/device.d.ts +34 -0
  41. package/dist/mixins/device.js +8 -0
  42. package/dist/mixins/device.js.map +1 -0
  43. package/dist/mixins/index.cjs +14 -0
  44. package/dist/mixins/index.cjs.map +1 -0
  45. package/dist/mixins/index.d.cts +3 -0
  46. package/dist/mixins/index.d.ts +3 -0
  47. package/dist/mixins/index.js +14 -0
  48. package/dist/mixins/index.js.map +1 -0
  49. package/dist/mixins/intl.cjs +8 -0
  50. package/dist/mixins/intl.cjs.map +1 -0
  51. package/dist/mixins/intl.d.cts +37 -0
  52. package/dist/mixins/intl.d.ts +37 -0
  53. package/dist/mixins/intl.js +8 -0
  54. package/dist/mixins/intl.js.map +1 -0
  55. package/dist/services/device.cjs +7 -0
  56. package/dist/services/device.cjs.map +1 -0
  57. package/dist/services/device.d.cts +71 -0
  58. package/dist/services/device.d.ts +71 -0
  59. package/dist/services/device.js +7 -0
  60. package/dist/services/device.js.map +1 -0
  61. package/dist/services/index.cjs +20 -0
  62. package/dist/services/index.cjs.map +1 -0
  63. package/dist/services/index.d.cts +4 -0
  64. package/dist/services/index.d.ts +4 -0
  65. package/dist/services/index.js +20 -0
  66. package/dist/services/index.js.map +1 -0
  67. package/dist/services/intl.cjs +15 -0
  68. package/dist/services/intl.cjs.map +1 -0
  69. package/dist/services/intl.d.cts +170 -0
  70. package/dist/services/intl.d.ts +170 -0
  71. package/dist/services/intl.js +15 -0
  72. package/dist/services/intl.js.map +1 -0
  73. package/dist/utils/index.cjs +11 -0
  74. package/dist/utils/index.cjs.map +1 -0
  75. package/dist/utils/index.d.cts +32 -0
  76. package/dist/utils/index.d.ts +32 -0
  77. package/dist/utils/index.js +11 -0
  78. package/dist/utils/index.js.map +1 -0
  79. package/package.json +114 -0
  80. package/src/index.ts +10 -0
  81. package/src/mixins/__tests__/device.spec.ts +119 -0
  82. package/src/mixins/__tests__/intl.spec.ts +198 -0
  83. package/src/mixins/device.ts +48 -0
  84. package/src/mixins/index.ts +8 -0
  85. package/src/mixins/intl.ts +112 -0
  86. package/src/services/__tests__/device.spec.ts +36 -0
  87. package/src/services/__tests__/intl.spec.ts +536 -0
  88. package/src/services/device.ts +121 -0
  89. package/src/services/index.ts +17 -0
  90. package/src/services/intl.ts +326 -0
  91. package/src/utils/__tests__/browserInfo.spec.ts +310 -0
  92. package/src/utils/__tests__/slug.spec.ts +26 -0
  93. package/src/utils/browserInfo.ts +102 -0
  94. package/src/utils/index.ts +4 -0
  95. package/src/utils/slug.ts +13 -0
@@ -0,0 +1,536 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import {
3
+ clearIntlService,
4
+ createIntlService,
5
+ defaultConfig,
6
+ getIntlService,
7
+ setIntlService,
8
+ } from '../intl'
9
+
10
+ describe('Intl service', () => {
11
+ beforeEach(() => {
12
+ // Mock the date to 2024-12-01 12:00 UTC for consistent testing
13
+ // TZ env var is set to UTC in vitest config
14
+ vi.useFakeTimers()
15
+ vi.setSystemTime(new Date('2024-12-01T12:00:00Z'))
16
+ })
17
+
18
+ afterEach(() => {
19
+ vi.useRealTimers()
20
+ clearIntlService()
21
+ })
22
+
23
+ describe('createIntlService', () => {
24
+ it('should create an intl service instance with default config', () => {
25
+ const intl = createIntlService()
26
+ expect(intl).toBeDefined()
27
+ expect(intl.getLocale()).toBe('en-US')
28
+ })
29
+
30
+ it('should create an intl service instance with custom config', () => {
31
+ const customMessages = { greeting: 'Hello, {name}!' }
32
+ const intl = createIntlService({
33
+ locale: 'de-DE',
34
+ messages: customMessages,
35
+ })
36
+ expect(intl.getLocale()).toBe('de-DE')
37
+ })
38
+
39
+ it('should have all required methods', () => {
40
+ const intl = createIntlService()
41
+ expect(intl.formatMessage).toBeDefined()
42
+ expect(intl.formatNumber).toBeDefined()
43
+ expect(intl.formatDate).toBeDefined()
44
+ expect(intl.formatTime).toBeDefined()
45
+ expect(intl.formatTimestamp).toBeDefined()
46
+ expect(intl.setLocale).toBeDefined()
47
+ expect(intl.getLocale).toBeDefined()
48
+ expect(intl.localeChanged).toBeDefined()
49
+ })
50
+ })
51
+
52
+ describe('formatMessage', () => {
53
+ it('should return the key if message is not found', () => {
54
+ const intl = createIntlService()
55
+ expect(intl.formatMessage('non_existent_key')).toBe('non_existent_key')
56
+ })
57
+
58
+ it('should return translated message when found', () => {
59
+ const messages = { greeting: 'Hello' }
60
+ const intl = createIntlService({
61
+ locale: 'en-US',
62
+ messages,
63
+ })
64
+ expect(intl.formatMessage('greeting')).toBe('Hello')
65
+ })
66
+
67
+ it('should interpolate variables in messages', () => {
68
+ const messages = { greeting: 'Hello, {name}!' }
69
+ const intl = createIntlService({
70
+ locale: 'en-US',
71
+ messages,
72
+ })
73
+ const result = intl.formatMessage('greeting', { name: 'John' })
74
+ expect(result).toBe('Hello, John!')
75
+ })
76
+
77
+ it('should not interpolate variables in HTML messages', () => {
78
+ const messages = { greeting: 'Hello, <b>{name}!</b>' }
79
+ const intl = createIntlService({
80
+ locale: 'en-US',
81
+ messages,
82
+ })
83
+ const result = intl.formatMessage('greeting', { name: 'John' })
84
+ expect(result).toBe('Hello, <b>{name}!</b>')
85
+ })
86
+
87
+ it('should interpolate variables in injected HTML messages', () => {
88
+ const messages = { greeting: 'Hello, {name}!' }
89
+ const intl = createIntlService({
90
+ locale: 'en-US',
91
+ messages,
92
+ })
93
+ const result = intl.formatMessage('greeting', { name: '<b>John</b>' })
94
+ expect(result).toBe('Hello, <b>John</b>!')
95
+ })
96
+
97
+ it('should handle missing translation gracefully', () => {
98
+ const intl = createIntlService()
99
+ expect(() => intl.formatMessage('missing_key')).not.toThrow()
100
+ })
101
+
102
+ it('should handle dot notation keys', () => {
103
+ const messages = {
104
+ 'app.title': 'My Application',
105
+ }
106
+ const intl = createIntlService({
107
+ locale: 'en-US',
108
+ messages,
109
+ })
110
+ expect(intl.formatMessage('app.title')).toBe('My Application')
111
+ })
112
+ })
113
+
114
+ describe('formatNumber', () => {
115
+ it('should format a number with default options', () => {
116
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
117
+ const result = intl.formatNumber(1234.5)
118
+ expect(result).toBe('1,234.5')
119
+ })
120
+
121
+ it('should format a string number', () => {
122
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
123
+ const result = intl.formatNumber('1234.5')
124
+ expect(result).toBe('1,234.5')
125
+ })
126
+
127
+ it('should format a bigint', () => {
128
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
129
+ const result = intl.formatNumber(BigInt('1234567890'))
130
+ expect(result).toBe('1,234,567,890')
131
+ })
132
+
133
+ it('should return 0 for null or undefined', () => {
134
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
135
+ expect(intl.formatNumber(null as any)).toBe('0')
136
+ expect(intl.formatNumber(undefined as any)).toBe('0')
137
+ })
138
+
139
+ it('should accept custom format options', () => {
140
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
141
+ const result = intl.formatNumber(1234.56789, {
142
+ minimumFractionDigits: 2,
143
+ maximumFractionDigits: 2,
144
+ })
145
+ expect(result).toBe('1,234.57')
146
+ })
147
+
148
+ it('should use default maximum fraction digits of 18', () => {
149
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
150
+ // biome-ignore lint/correctness/noPrecisionLoss: because of testing
151
+ const result = intl.formatNumber(0.123456789012345678123)
152
+ expect(result).toBe('0.12345678901234568')
153
+ })
154
+ })
155
+
156
+ describe('formatDate', () => {
157
+ it('should format a Date object', () => {
158
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
159
+ const date = new Date('2024-12-02T12:00:00Z')
160
+ const result = intl.formatDate(date)
161
+ expect(result).toBe('12/2/2024')
162
+ })
163
+
164
+ it('should format a timestamp number', () => {
165
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
166
+ const timestamp = 1733144400000 // 2024-12-02
167
+ const result = intl.formatDate(timestamp)
168
+ expect(result).toBe('12/2/2024')
169
+ })
170
+
171
+ it('should format a date string', () => {
172
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
173
+ const result = intl.formatDate('2024-12-02')
174
+ expect(result).toBe('12/2/2024')
175
+ })
176
+
177
+ it('should format a date when given undefined input', () => {
178
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
179
+ const result = intl.formatDate(undefined)
180
+ // Should format the mocked date (2024-12-02)
181
+ expect(result).toBe('12/1/2024')
182
+ })
183
+ })
184
+
185
+ describe('formatTime', () => {
186
+ it('should format a Date object as time', () => {
187
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
188
+ const date = new Date('2024-12-02T14:30:00Z')
189
+ const result = intl.formatTime(date)
190
+ expect(result).toBe('2:30 PM')
191
+ })
192
+
193
+ it('should format a timestamp number as time', () => {
194
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
195
+ const timestamp = 1733151000000 // 2024-12-02T14:30:00Z
196
+ const result = intl.formatTime(timestamp)
197
+ expect(result).toBe('2:50 PM')
198
+ })
199
+
200
+ it('should format current time when given undefined input', () => {
201
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
202
+ const result = intl.formatTime(undefined)
203
+ // Should format the mocked time (2024-12-01T12:00:00Z = 12:00 PM in UTC)
204
+ expect(result).toBe('12:00 PM')
205
+ })
206
+ })
207
+
208
+ describe('formatTimestamp', () => {
209
+ it('should format a UNIX timestamp in seconds', () => {
210
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
211
+ const timestamp = 1733144400 // 2024-12-02 in seconds
212
+ const result = intl.formatTimestamp(timestamp)
213
+ expect(result).toBe('12/2/2024')
214
+ })
215
+
216
+ it('should format a UNIX timestamp as string', () => {
217
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
218
+ const result = intl.formatTimestamp('1733144400')
219
+ expect(result).toBe('12/2/2024')
220
+ })
221
+
222
+ it('should return empty string for undefined or null', () => {
223
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
224
+ expect(intl.formatTimestamp('')).toBe('')
225
+ expect(intl.formatTimestamp(undefined)).toBe('')
226
+ expect(intl.formatTimestamp(null as any)).toBe('')
227
+ })
228
+
229
+ it('should accept custom DateTimeFormat options', () => {
230
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
231
+ const timestamp = 1733144400
232
+ const result = intl.formatTimestamp(timestamp, {
233
+ year: 'numeric',
234
+ month: 'long',
235
+ day: 'numeric',
236
+ })
237
+ expect(result).toBe('December 2, 2024')
238
+ })
239
+
240
+ it('can use custom format', () => {
241
+ const intl = createIntlService({
242
+ locale: 'en-US',
243
+ messages: {},
244
+ formats: {
245
+ date: {
246
+ shortDateTime: {
247
+ weekday: 'short', // Wed
248
+ month: 'short', // Dec
249
+ day: 'numeric', // 3
250
+ year: 'numeric', // 2025
251
+ hour: '2-digit', // 16
252
+ minute: '2-digit', // 07
253
+ hour12: false,
254
+ },
255
+ },
256
+ },
257
+ })
258
+ const timestamp = 1733144400 // 2024-12-02T14:00:00Z
259
+ const result = intl.formatTimestamp(timestamp, 'shortDateTime')
260
+ expect(result).toBe('Mon, Dec 2, 2024, 13:00')
261
+ })
262
+ })
263
+
264
+ describe('setLocale', () => {
265
+ it('should change the locale', () => {
266
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
267
+ intl.setLocale('de-DE')
268
+ expect(intl.getLocale()).toBe('de-DE')
269
+ })
270
+
271
+ it('should update messages when provided', () => {
272
+ const enMessages = { greeting: 'Hello' }
273
+ const deMessages = { greeting: 'Hallo' }
274
+ const intl = createIntlService({
275
+ locale: 'en-US',
276
+ messages: enMessages,
277
+ })
278
+
279
+ intl.setLocale('de-DE', deMessages)
280
+ expect(intl.getLocale()).toBe('de-DE')
281
+ expect(intl.formatMessage('greeting')).toBe('Hallo')
282
+ })
283
+
284
+ it('should keep existing messages if not provided', () => {
285
+ const messages = { greeting: 'Hello' }
286
+ const intl = createIntlService({
287
+ locale: 'en-US',
288
+ messages,
289
+ })
290
+
291
+ intl.setLocale('de-DE')
292
+ expect(intl.getLocale()).toBe('de-DE')
293
+ })
294
+
295
+ it('should increment localeChanged signal', () => {
296
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
297
+ const initialValue = intl.localeChanged.value
298
+
299
+ intl.setLocale('de-DE')
300
+ expect(intl.localeChanged.value).toBe(initialValue + 1)
301
+
302
+ intl.setLocale('fr-FR')
303
+ expect(intl.localeChanged.value).toBe(initialValue + 2)
304
+ })
305
+ })
306
+
307
+ describe('getLocale', () => {
308
+ it('should return the current locale', () => {
309
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
310
+ expect(intl.getLocale()).toBe('en-US')
311
+ })
312
+
313
+ it('should return updated locale after setLocale', () => {
314
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
315
+ intl.setLocale('de-DE')
316
+ expect(intl.getLocale()).toBe('de-DE')
317
+ })
318
+ })
319
+
320
+ describe('localeChanged signal', () => {
321
+ it('should have an initial value of 0', () => {
322
+ const intl = createIntlService()
323
+ expect(intl.localeChanged.value).toBe(0)
324
+ })
325
+
326
+ it('should increment when locale changes', () => {
327
+ const intl = createIntlService()
328
+ expect(intl.localeChanged.value).toBe(0)
329
+
330
+ intl.setLocale('de-DE')
331
+ expect(intl.localeChanged.value).toBe(1)
332
+
333
+ intl.setLocale('fr-FR')
334
+ expect(intl.localeChanged.value).toBe(2)
335
+ })
336
+
337
+ it('should be observable', () => {
338
+ const intl = createIntlService()
339
+ let callCount = 0
340
+ const signal = intl.localeChanged
341
+
342
+ // Create a simple effect to track changes
343
+ const initialValue = signal.value
344
+ intl.setLocale('de-DE')
345
+ expect(signal.value).toBeGreaterThan(initialValue)
346
+ callCount++
347
+
348
+ expect(callCount).toBe(1)
349
+ })
350
+ })
351
+
352
+ describe('global service management', () => {
353
+ it('should set and get global intl service', () => {
354
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
355
+ setIntlService(intl)
356
+
357
+ const retrieved = getIntlService()
358
+ expect(retrieved).toBe(intl)
359
+ })
360
+
361
+ it('should return null if no service is set', () => {
362
+ clearIntlService()
363
+ expect(getIntlService()).toBeNull()
364
+ })
365
+
366
+ it('should clear the global service', () => {
367
+ const intl = createIntlService()
368
+ setIntlService(intl)
369
+ expect(getIntlService()).toBe(intl)
370
+
371
+ clearIntlService()
372
+ expect(getIntlService()).toBeNull()
373
+ })
374
+
375
+ it('should support multiple service instances', () => {
376
+ const intl1 = createIntlService({ locale: 'en-US', messages: {} })
377
+ const intl2 = createIntlService({ locale: 'de-DE', messages: {} })
378
+
379
+ expect(intl1.getLocale()).toBe('en-US')
380
+ expect(intl2.getLocale()).toBe('de-DE')
381
+
382
+ setIntlService(intl1)
383
+ expect(getIntlService()).toBe(intl1)
384
+
385
+ setIntlService(intl2)
386
+ expect(getIntlService()).toBe(intl2)
387
+ })
388
+ })
389
+
390
+ describe('default config', () => {
391
+ it('should have correct default config', () => {
392
+ expect(defaultConfig.locale).toBe('en-US')
393
+ expect(defaultConfig.messages).toEqual({})
394
+ })
395
+
396
+ it('should use default config when none provided', () => {
397
+ const intl = createIntlService()
398
+ expect(intl.getLocale()).toBe(defaultConfig.locale)
399
+ })
400
+ })
401
+
402
+ describe('locale-specific formatting', () => {
403
+ it('should format numbers differently for different locales', () => {
404
+ const enIntl = createIntlService({ locale: 'en-US', messages: {} })
405
+ const deIntl = createIntlService({ locale: 'de-DE', messages: {} })
406
+
407
+ const enResult = enIntl.formatNumber(1234.56)
408
+ const deResult = deIntl.formatNumber(1234.56)
409
+
410
+ // Numbers may format differently based on locale
411
+ expect(enResult).toBe('1,234.56')
412
+ expect(deResult).toBe('1.234,56')
413
+ })
414
+
415
+ it('should format dates differently for different locales', () => {
416
+ const enIntl = createIntlService({ locale: 'en-US', messages: {} })
417
+ const deIntl = createIntlService({ locale: 'de-DE', messages: {} })
418
+
419
+ const date = new Date('2024-12-02T12:00:00Z')
420
+ const enResult = enIntl.formatDate(date)
421
+ const deResult = deIntl.formatDate(date)
422
+
423
+ // Dates should format differently based on locale
424
+ expect(enResult).toBe('12/2/2024')
425
+ expect(deResult).toBe('2.12.2024')
426
+ })
427
+ })
428
+
429
+ describe('setFallbackTranslations', () => {
430
+ it('should return fallback translation for missing keys', () => {
431
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
432
+ const fallback = { missing_key: 'Fallback value' }
433
+ intl.setFallbackTranslations(fallback)
434
+ expect(intl.formatMessage('missing_key')).toBe('Fallback value')
435
+ })
436
+
437
+ it('should use primary messages before fallback', () => {
438
+ const messages = { greeting: 'Hello' }
439
+ const fallback = { greeting: 'Hi' }
440
+ const intl = createIntlService({
441
+ locale: 'en-US',
442
+ messages,
443
+ })
444
+ intl.setFallbackTranslations(fallback)
445
+ expect(intl.formatMessage('greeting')).toBe('Hello')
446
+ })
447
+
448
+ it('should use fallback only for missing translations', () => {
449
+ const messages = { primary: 'Primary translation' }
450
+ const fallback = { primary: 'Fallback', fallback: 'Fallback only' }
451
+ const intl = createIntlService({
452
+ locale: 'en-US',
453
+ messages,
454
+ })
455
+ intl.setFallbackTranslations(fallback)
456
+ expect(intl.formatMessage('primary')).toBe('Primary translation')
457
+ expect(intl.formatMessage('fallback')).toBe('Fallback only')
458
+ })
459
+
460
+ it('should support updating fallback translations', () => {
461
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
462
+ const fallback1 = { key: 'First fallback' }
463
+ const fallback2 = { key: 'Second fallback' }
464
+
465
+ intl.setFallbackTranslations(fallback1)
466
+ expect(intl.formatMessage('key')).toBe('First fallback')
467
+
468
+ intl.setFallbackTranslations(fallback2)
469
+ expect(intl.formatMessage('key')).toBe('Second fallback')
470
+ })
471
+
472
+ it('should return key if neither primary nor fallback translation exists', () => {
473
+ const fallback = { some_key: 'Fallback text' }
474
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
475
+ intl.setFallbackTranslations(fallback)
476
+ expect(intl.formatMessage('non_existent_key')).toBe('non_existent_key')
477
+ })
478
+
479
+ it('should accept empty fallback translations', () => {
480
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
481
+ intl.setFallbackTranslations({})
482
+ expect(intl.formatMessage('any_key')).toBe('any_key')
483
+ })
484
+
485
+ it('should work with multiple locale changes and fallback translations', () => {
486
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
487
+ const fallback = { greeting: 'Default greeting' }
488
+ intl.setFallbackTranslations(fallback)
489
+
490
+ expect(intl.formatMessage('greeting')).toBe('Default greeting')
491
+
492
+ intl.setLocale('de-DE', { greeting: 'Hallo' })
493
+ expect(intl.formatMessage('greeting')).toBe('Hallo')
494
+
495
+ // When switching locale without new messages, intl keeps the existing messages
496
+ // so the greeting from de-DE remains available
497
+ intl.setLocale('fr-FR')
498
+ expect(intl.formatMessage('greeting')).toBe('Hallo')
499
+ })
500
+ })
501
+
502
+ describe('error handling', () => {
503
+ it('should not throw when formatting missing messages', () => {
504
+ const intl = createIntlService()
505
+ expect(() => {
506
+ intl.formatMessage('missing.key')
507
+ }).not.toThrow()
508
+ })
509
+
510
+ it('should return fallback values on errors', () => {
511
+ const intl = createIntlService()
512
+ const result = intl.formatMessage('missing.key')
513
+ expect(result).toBe('missing.key')
514
+ })
515
+
516
+ it('should handle invalid numbers gracefully', () => {
517
+ const intl = createIntlService()
518
+ expect(() => {
519
+ intl.formatNumber(NaN)
520
+ }).not.toThrow()
521
+ })
522
+
523
+ it('should handle very large numbers', () => {
524
+ const intl = createIntlService()
525
+ const result = intl.formatNumber(999999999999999999n)
526
+ expect(result).toBe('999,999,999,999,999,999')
527
+ })
528
+
529
+ it('should fallback to fallback translations on formatMessage error', () => {
530
+ const intl = createIntlService({ locale: 'en-US', messages: {} })
531
+ const fallback = { error_key: 'Error fallback text' }
532
+ intl.setFallbackTranslations(fallback)
533
+ expect(intl.formatMessage('error_key')).toBe('Error fallback text')
534
+ })
535
+ })
536
+ })
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Device Detection Service
3
+ *
4
+ * Provides device type, OS, and browser detection using ua-parser-js
5
+ */
6
+
7
+ import { signal } from '@preact/signals-core'
8
+ import { UAParser } from 'ua-parser-js'
9
+
10
+ /**
11
+ * Device detection flags and utilities
12
+ */
13
+ export interface DeviceService {
14
+ /**
15
+ * Device type detection flags
16
+ */
17
+ isMobile: boolean
18
+ isTablet: boolean
19
+ isDesktop: boolean
20
+
21
+ /**
22
+ * Operating system detection flags
23
+ */
24
+ isIOS: boolean
25
+ isAndroid: boolean
26
+ isWindows: boolean
27
+ isMacOS: boolean
28
+ isLinux: boolean
29
+
30
+ /**
31
+ * Browser detection flags
32
+ */
33
+ isChrome: boolean
34
+ isSafari: boolean
35
+ isFirefox: boolean
36
+ isEdge: boolean
37
+ isOpera: boolean
38
+ isBrave: boolean
39
+
40
+ /**
41
+ * Get raw parser result for additional details
42
+ * @returns Full UAParser result object
43
+ */
44
+ getRaw: () => ReturnType<UAParser['getResult']>
45
+
46
+ /**
47
+ * Signal that tracks device detector initialization
48
+ * Useful for reactivity in web components
49
+ */
50
+ initialized: { value: number }
51
+ }
52
+
53
+ export interface NavigatorExtended extends Navigator {
54
+ brave?: unknown
55
+ }
56
+
57
+ /**
58
+ * Create a new device detector instance
59
+ *
60
+ * @param navigator - Optional custom navigator object (uses global navigator by default)
61
+ * @returns DeviceService instance with device/OS/browser detection
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * import { deviceService } from '@lukso/core/services/device';
66
+ *
67
+ * const device = deviceService();
68
+ *
69
+ * if (device.isMobile) {
70
+ * console.log('User is on mobile');
71
+ * }
72
+ * ```
73
+ */
74
+ export function deviceService(navigator?: NavigatorExtended): DeviceService {
75
+ const userAgent = navigator?.userAgent ?? ''
76
+ const parser = new UAParser(userAgent)
77
+ const result = parser.getResult()
78
+ const initialized = signal(1)
79
+
80
+ // Determine device type
81
+ const deviceType = result.device.type as 'mobile' | 'tablet' | undefined
82
+ const isMobile = deviceType === 'mobile'
83
+ const isTablet = deviceType === 'tablet'
84
+ const isDesktop = !deviceType
85
+
86
+ // Operating system detection
87
+ const osName = result.os.name || ''
88
+ const isIOS = osName.includes('iOS') || osName.includes('Mac OS')
89
+ const isAndroid = osName === 'Android'
90
+ const isWindows = osName === 'Windows'
91
+ const isMacOS = osName === 'Mac OS'
92
+ const isLinux = osName === 'Linux'
93
+
94
+ // Browser detection
95
+ const browserName = result.browser.name || ''
96
+ const isChrome = browserName === 'Chrome'
97
+ const isSafari = browserName === 'Safari'
98
+ const isFirefox = browserName === 'Firefox'
99
+ const isEdge = browserName === 'Edge'
100
+ const isOpera = browserName === 'Opera'
101
+ const isBrave = navigator?.brave !== undefined
102
+
103
+ return {
104
+ isMobile,
105
+ isTablet,
106
+ isDesktop,
107
+ isIOS,
108
+ isAndroid,
109
+ isWindows,
110
+ isMacOS,
111
+ isLinux,
112
+ isChrome,
113
+ isSafari,
114
+ isFirefox,
115
+ isEdge,
116
+ isOpera,
117
+ isBrave,
118
+ getRaw: () => result,
119
+ initialized,
120
+ }
121
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @lukso/core - Services
3
+ *
4
+ * Core services for device detection, internationalization, and more
5
+ */
6
+
7
+ export type { DeviceService, NavigatorExtended } from './device.js'
8
+ export { deviceService } from './device.js'
9
+
10
+ export type { IntlMessages, IntlService } from './intl.js'
11
+ export {
12
+ clearIntlService,
13
+ createIntlService,
14
+ defaultConfig,
15
+ getIntlService,
16
+ setIntlService,
17
+ } from './intl.js'