@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.
- package/LICENSE +201 -0
- package/README.md +159 -0
- package/dist/chunk-3WGYJTN4.js +19 -0
- package/dist/chunk-3WGYJTN4.js.map +1 -0
- package/dist/chunk-4TNWG4ME.js +106 -0
- package/dist/chunk-4TNWG4ME.js.map +1 -0
- package/dist/chunk-AMRGSLR5.cjs +1 -0
- package/dist/chunk-AMRGSLR5.cjs.map +1 -0
- package/dist/chunk-CC3LFUYY.cjs +19 -0
- package/dist/chunk-CC3LFUYY.cjs.map +1 -0
- package/dist/chunk-DFMMMF62.cjs +1 -0
- package/dist/chunk-DFMMMF62.cjs.map +1 -0
- package/dist/chunk-DKEXQFNE.js +1 -0
- package/dist/chunk-DKEXQFNE.js.map +1 -0
- package/dist/chunk-DKXHVRHM.js +84 -0
- package/dist/chunk-DKXHVRHM.js.map +1 -0
- package/dist/chunk-FR74YPGJ.cjs +87 -0
- package/dist/chunk-FR74YPGJ.cjs.map +1 -0
- package/dist/chunk-LEL6VWU4.js +1 -0
- package/dist/chunk-LEL6VWU4.js.map +1 -0
- package/dist/chunk-MBIRTPNM.cjs +84 -0
- package/dist/chunk-MBIRTPNM.cjs.map +1 -0
- package/dist/chunk-NJQVWIZL.cjs +49 -0
- package/dist/chunk-NJQVWIZL.cjs.map +1 -0
- package/dist/chunk-RM42NG7E.cjs +106 -0
- package/dist/chunk-RM42NG7E.cjs.map +1 -0
- package/dist/chunk-SV4TVR2K.js +87 -0
- package/dist/chunk-SV4TVR2K.js.map +1 -0
- package/dist/chunk-X2QNFZU7.js +49 -0
- package/dist/chunk-X2QNFZU7.js.map +1 -0
- package/dist/index.cjs +37 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/mixins/device.cjs +8 -0
- package/dist/mixins/device.cjs.map +1 -0
- package/dist/mixins/device.d.cts +34 -0
- package/dist/mixins/device.d.ts +34 -0
- package/dist/mixins/device.js +8 -0
- package/dist/mixins/device.js.map +1 -0
- package/dist/mixins/index.cjs +14 -0
- package/dist/mixins/index.cjs.map +1 -0
- package/dist/mixins/index.d.cts +3 -0
- package/dist/mixins/index.d.ts +3 -0
- package/dist/mixins/index.js +14 -0
- package/dist/mixins/index.js.map +1 -0
- package/dist/mixins/intl.cjs +8 -0
- package/dist/mixins/intl.cjs.map +1 -0
- package/dist/mixins/intl.d.cts +37 -0
- package/dist/mixins/intl.d.ts +37 -0
- package/dist/mixins/intl.js +8 -0
- package/dist/mixins/intl.js.map +1 -0
- package/dist/services/device.cjs +7 -0
- package/dist/services/device.cjs.map +1 -0
- package/dist/services/device.d.cts +71 -0
- package/dist/services/device.d.ts +71 -0
- package/dist/services/device.js +7 -0
- package/dist/services/device.js.map +1 -0
- package/dist/services/index.cjs +20 -0
- package/dist/services/index.cjs.map +1 -0
- package/dist/services/index.d.cts +4 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +20 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/intl.cjs +15 -0
- package/dist/services/intl.cjs.map +1 -0
- package/dist/services/intl.d.cts +170 -0
- package/dist/services/intl.d.ts +170 -0
- package/dist/services/intl.js +15 -0
- package/dist/services/intl.js.map +1 -0
- package/dist/utils/index.cjs +11 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +32 -0
- package/dist/utils/index.d.ts +32 -0
- package/dist/utils/index.js +11 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +114 -0
- package/src/index.ts +10 -0
- package/src/mixins/__tests__/device.spec.ts +119 -0
- package/src/mixins/__tests__/intl.spec.ts +198 -0
- package/src/mixins/device.ts +48 -0
- package/src/mixins/index.ts +8 -0
- package/src/mixins/intl.ts +112 -0
- package/src/services/__tests__/device.spec.ts +36 -0
- package/src/services/__tests__/intl.spec.ts +536 -0
- package/src/services/device.ts +121 -0
- package/src/services/index.ts +17 -0
- package/src/services/intl.ts +326 -0
- package/src/utils/__tests__/browserInfo.spec.ts +310 -0
- package/src/utils/__tests__/slug.spec.ts +26 -0
- package/src/utils/browserInfo.ts +102 -0
- package/src/utils/index.ts +4 -0
- 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'
|