@i18n-micro/core 1.0.27 → 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.
@@ -0,0 +1,485 @@
1
+ import { BaseI18n, type BaseI18nOptions } from '../src/base'
2
+ import type { Translations, PluralFunc } from '@i18n-micro/types'
3
+
4
+ // Test implementation of BaseI18n
5
+ class TestI18n extends BaseI18n {
6
+ private _locale: string
7
+ private _fallbackLocale: string
8
+ private _route: string
9
+
10
+ constructor(
11
+ locale: string,
12
+ fallbackLocale: string,
13
+ route: string,
14
+ options?: BaseI18nOptions,
15
+ ) {
16
+ super(options)
17
+ this._locale = locale
18
+ this._fallbackLocale = fallbackLocale
19
+ this._route = route
20
+ }
21
+
22
+ public getLocale(): string {
23
+ return this._locale
24
+ }
25
+
26
+ public getFallbackLocale(): string {
27
+ return this._fallbackLocale
28
+ }
29
+
30
+ public getRoute(): string {
31
+ return this._route
32
+ }
33
+
34
+ public setLocale(locale: string): void {
35
+ this._locale = locale
36
+ }
37
+
38
+ public setRoute(route: string): void {
39
+ this._route = route
40
+ }
41
+ }
42
+
43
+ describe('BaseI18n', () => {
44
+ describe('Constructor', () => {
45
+ test('should initialize with default options', () => {
46
+ const i18n = new TestI18n('en', 'en', 'general')
47
+ expect(i18n.getLocale()).toBe('en')
48
+ expect(i18n.getFallbackLocale()).toBe('en')
49
+ expect(i18n.getRoute()).toBe('general')
50
+ })
51
+
52
+ test('should initialize with custom storage', () => {
53
+ const storage = { translations: new Map<string, Translations>() }
54
+ const i18n = new TestI18n('en', 'en', 'general', { storage })
55
+ expect(i18n).toBeDefined()
56
+ })
57
+
58
+ test('should initialize with custom plural function', () => {
59
+ const customPlural: PluralFunc = () => 'custom'
60
+ const i18n = new TestI18n('en', 'en', 'general', { plural: customPlural })
61
+ expect(i18n).toBeDefined()
62
+ })
63
+
64
+ test('should initialize with missingWarn option', () => {
65
+ const i18n = new TestI18n('en', 'en', 'general', { missingWarn: false })
66
+ expect(i18n).toBeDefined()
67
+ })
68
+
69
+ test('should initialize with missingHandler', () => {
70
+ const handler = jest.fn()
71
+ const i18n = new TestI18n('en', 'en', 'general', { missingHandler: handler })
72
+ expect(i18n).toBeDefined()
73
+ })
74
+ })
75
+
76
+ describe('t() method', () => {
77
+ test('should return empty string for empty key', () => {
78
+ const i18n = new TestI18n('en', 'en', 'general')
79
+ expect(i18n.t('')).toBe('')
80
+ })
81
+
82
+ test('should return translation for existing key', async () => {
83
+ const i18n = new TestI18n('en', 'en', 'general')
84
+ const translations: Translations = { greeting: 'Hello' }
85
+ i18n['helper'].loadTranslations('en', translations)
86
+
87
+ expect(i18n.t('greeting')).toBe('Hello')
88
+ })
89
+
90
+ test('should interpolate params in translation', async () => {
91
+ const storage = { translations: new Map<string, Translations>() }
92
+ const i18n = new TestI18n('en', 'en', 'general', { storage })
93
+ const translations: Translations = { greeting: 'Hello, {name}!' }
94
+ await i18n['helper'].loadTranslations('en', translations)
95
+
96
+ expect(i18n.t('greeting', { name: 'John' })).toBe('Hello, John!')
97
+ })
98
+
99
+ test('should use defaultValue when translation is missing', () => {
100
+ const i18n = new TestI18n('en', 'en', 'general')
101
+ expect(i18n.t('missing.key', undefined, 'Default value')).toBe('Default value')
102
+ })
103
+
104
+ test('should return key when translation is missing and no defaultValue', () => {
105
+ const i18n = new TestI18n('en', 'en', 'general')
106
+ expect(i18n.t('missing.key')).toBe('missing.key')
107
+ })
108
+
109
+ test('should fallback to fallbackLocale when translation is missing', async () => {
110
+ const storage = { translations: new Map<string, Translations>() }
111
+ const i18n = new TestI18n('en', 'fr', 'general', { storage })
112
+ const translations: Translations = { greeting: 'Bonjour' }
113
+ await i18n['helper'].loadTranslations('fr', translations)
114
+
115
+ expect(i18n.t('greeting')).toBe('Bonjour')
116
+ })
117
+
118
+ test('should use route-specific translation when routeName is provided', async () => {
119
+ const i18n = new TestI18n('en', 'en', 'general')
120
+ const routeTranslations: Translations = { title: 'Route Title' }
121
+ await i18n['helper'].loadPageTranslations('en', 'about', routeTranslations)
122
+
123
+ expect(i18n.t('title', undefined, undefined, 'about')).toBe('Route Title')
124
+ })
125
+
126
+ test('should use previousPageInfo fallback when enabled', async () => {
127
+ const storage = { translations: new Map<string, Translations>() }
128
+ const prevInfo = { locale: 'fr', routeName: 'previous' }
129
+ const i18n = new TestI18n('en', 'en', 'general', {
130
+ storage,
131
+ getPreviousPageInfo: () => prevInfo,
132
+ enablePreviousPageFallback: true,
133
+ })
134
+
135
+ const translations: Translations = { greeting: 'Bonjour' }
136
+ await i18n['helper'].loadPageTranslations('fr', 'previous', translations)
137
+
138
+ // Current locale is 'en', route is 'general', translation not found
139
+ // Should fallback to previous page info: locale 'fr', route 'previous'
140
+ expect(i18n.t('greeting')).toBe('Bonjour')
141
+ })
142
+
143
+ test('should call missingHandler when translation is missing', () => {
144
+ const handler = jest.fn()
145
+ const i18n = new TestI18n('en', 'en', 'general', { missingHandler: handler })
146
+
147
+ i18n.t('missing.key')
148
+
149
+ expect(handler).toHaveBeenCalledWith('en', 'missing.key', 'general')
150
+ })
151
+
152
+ test('should call customMissingHandler when set (Nuxt runtime)', () => {
153
+ const customHandler = jest.fn()
154
+ const i18n = new TestI18n('en', 'en', 'general', {
155
+ getCustomMissingHandler: () => customHandler,
156
+ })
157
+
158
+ i18n.t('missing.key')
159
+
160
+ expect(customHandler).toHaveBeenCalledWith('en', 'missing.key', 'general')
161
+ })
162
+
163
+ test('should not warn when missingWarn is false', () => {
164
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
165
+ const i18n = new TestI18n('en', 'en', 'general', { missingWarn: false })
166
+
167
+ i18n.t('missing.key')
168
+
169
+ expect(consoleSpy).not.toHaveBeenCalled()
170
+ consoleSpy.mockRestore()
171
+ })
172
+ })
173
+
174
+ describe('ts() method', () => {
175
+ test('should return translation as string', async () => {
176
+ const i18n = new TestI18n('en', 'en', 'general')
177
+ const translations: Translations = { greeting: 'Hello' }
178
+ i18n['helper'].loadTranslations('en', translations)
179
+
180
+ expect(i18n.ts('greeting')).toBe('Hello')
181
+ })
182
+
183
+ test('should return defaultValue when translation is missing', () => {
184
+ const i18n = new TestI18n('en', 'en', 'general')
185
+ expect(i18n.ts('missing.key', undefined, 'Default')).toBe('Default')
186
+ })
187
+
188
+ test('should return key when translation is missing and no defaultValue', () => {
189
+ const i18n = new TestI18n('en', 'en', 'general')
190
+ expect(i18n.ts('missing.key')).toBe('missing.key')
191
+ })
192
+
193
+ test('should convert non-string values to string', async () => {
194
+ const i18n = new TestI18n('en', 'en', 'general')
195
+ const translations: Translations = { count: 42 }
196
+ i18n['helper'].loadTranslations('en', translations)
197
+
198
+ expect(i18n.ts('count')).toBe('42')
199
+ })
200
+ })
201
+
202
+ describe('tc() method', () => {
203
+ test('should return defaultValue when count is undefined', () => {
204
+ const i18n = new TestI18n('en', 'en', 'general')
205
+ expect(i18n.tc('apples', { other: 'params' }, 'No count')).toBe('No count')
206
+ })
207
+
208
+ test('should use plural function with count', async () => {
209
+ const i18n = new TestI18n('en', 'en', 'general')
210
+ const translations: Translations = { apples: 'apple|apples' }
211
+ await i18n['helper'].loadTranslations('en', translations)
212
+
213
+ // defaultPlural selects form by index: forms[count] or last form if count >= forms.length
214
+ // For 'apple|apples': forms[0]='apple', forms[1]='apples'
215
+ expect(i18n.tc('apples', 0)).toBe('apple')
216
+ expect(i18n.tc('apples', 1)).toBe('apples') // forms[1]
217
+ expect(i18n.tc('apples', 5)).toBe('apples') // last form
218
+ })
219
+
220
+ test('should handle count as number', async () => {
221
+ const i18n = new TestI18n('en', 'en', 'general')
222
+ const translations: Translations = { apples: 'apple|apples' }
223
+ i18n['helper'].loadTranslations('en', translations)
224
+
225
+ expect(i18n.tc('apples', 2)).toBe('apples')
226
+ })
227
+
228
+ test('should handle count as Params object', async () => {
229
+ const i18n = new TestI18n('en', 'en', 'general')
230
+ const translations: Translations = { apples: 'apple|apples' }
231
+ i18n['helper'].loadTranslations('en', translations)
232
+
233
+ expect(i18n.tc('apples', { count: 2, name: 'John' })).toBe('apples')
234
+ })
235
+
236
+ test('should return defaultValue when plural function returns null', () => {
237
+ const i18n = new TestI18n('en', 'en', 'general')
238
+ // When translation is missing, t() returns key, which is passed to pluralFunc
239
+ // pluralFunc tries to process 'missing.key' as translation, but since it doesn't contain '|',
240
+ // it returns the key itself. So tc returns the key, not defaultValue.
241
+ // To test defaultValue, we need a case where pluralFunc actually returns null.
242
+ // This happens when translation exists but is empty or invalid.
243
+ expect(i18n.tc('missing.key', 1, 'Default')).toBe('missing.key')
244
+ })
245
+ })
246
+
247
+ describe('tn() method', () => {
248
+ test('should format number with default locale', () => {
249
+ const i18n = new TestI18n('en', 'en', 'general')
250
+ const result = i18n.tn(1234.56)
251
+ expect(result).toMatch(/1[,.]234[.,]56/)
252
+ })
253
+
254
+ test('should format number with custom options', () => {
255
+ const i18n = new TestI18n('en', 'en', 'general')
256
+ const result = i18n.tn(1234.56, { style: 'currency', currency: 'USD' })
257
+ expect(result).toContain('1,234.56')
258
+ })
259
+
260
+ test('should use current locale for formatting', () => {
261
+ const i18n = new TestI18n('ru', 'en', 'general')
262
+ const result = i18n.tn(1234.56)
263
+ // Russian locale uses different number formatting
264
+ expect(result).toBeDefined()
265
+ })
266
+ })
267
+
268
+ describe('td() method', () => {
269
+ test('should format date with default locale', () => {
270
+ const i18n = new TestI18n('en', 'en', 'general')
271
+ const date = new Date('2024-01-15')
272
+ const result = i18n.td(date)
273
+ expect(result).toBeDefined()
274
+ expect(result).not.toBe('Invalid Date')
275
+ })
276
+
277
+ test('should format date with custom options', () => {
278
+ const i18n = new TestI18n('en', 'en', 'general')
279
+ const date = new Date('2024-01-15')
280
+ const result = i18n.td(date, { year: 'numeric', month: 'long', day: 'numeric' })
281
+ expect(result).toContain('2024')
282
+ expect(result).toContain('January')
283
+ })
284
+
285
+ test('should handle date as number (timestamp)', () => {
286
+ const i18n = new TestI18n('en', 'en', 'general')
287
+ const timestamp = new Date('2024-01-15').getTime()
288
+ const result = i18n.td(timestamp)
289
+ expect(result).toBeDefined()
290
+ expect(result).not.toBe('Invalid Date')
291
+ })
292
+
293
+ test('should handle date as string', () => {
294
+ const i18n = new TestI18n('en', 'en', 'general')
295
+ const result = i18n.td('2024-01-15')
296
+ expect(result).toBeDefined()
297
+ expect(result).not.toBe('Invalid Date')
298
+ })
299
+ })
300
+
301
+ describe('tdr() method', () => {
302
+ test('should format relative time', () => {
303
+ const i18n = new TestI18n('en', 'en', 'general')
304
+ const yesterday = new Date(Date.now() - 86400000)
305
+ const result = i18n.tdr(yesterday)
306
+ expect(result).toBeDefined()
307
+ expect(result).toMatch(/day|ago/i)
308
+ })
309
+
310
+ test('should format relative time with custom options', () => {
311
+ const i18n = new TestI18n('en', 'en', 'general')
312
+ const yesterday = new Date(Date.now() - 86400000)
313
+ const result = i18n.tdr(yesterday, { numeric: 'always' })
314
+ expect(result).toBeDefined()
315
+ })
316
+
317
+ test('should handle invalid date gracefully', () => {
318
+ const i18n = new TestI18n('en', 'en', 'general')
319
+ const invalidDate = new Date('invalid')
320
+ const result = i18n.tdr(invalidDate)
321
+ expect(result).toBeDefined()
322
+ })
323
+ })
324
+
325
+ describe('has() method', () => {
326
+ test('should return true when translation exists', async () => {
327
+ const i18n = new TestI18n('en', 'en', 'general')
328
+ const translations: Translations = { greeting: 'Hello' }
329
+ i18n['helper'].loadTranslations('en', translations)
330
+
331
+ expect(i18n.has('greeting')).toBe(true)
332
+ })
333
+
334
+ test('should return false when translation does not exist', () => {
335
+ const i18n = new TestI18n('en', 'en', 'general')
336
+ expect(i18n.has('missing.key')).toBe(false)
337
+ })
338
+
339
+ test('should check route-specific translation when routeName is provided', async () => {
340
+ const i18n = new TestI18n('en', 'en', 'general')
341
+ const routeTranslations: Translations = { title: 'Route Title' }
342
+ await i18n['helper'].loadPageTranslations('en', 'about', routeTranslations)
343
+
344
+ expect(i18n.has('title', 'about')).toBe(true)
345
+ expect(i18n.has('title', 'general')).toBe(false)
346
+ })
347
+
348
+ test('should use current route when routeName is not provided', async () => {
349
+ const i18n = new TestI18n('en', 'en', 'about')
350
+ const routeTranslations: Translations = { title: 'Route Title' }
351
+ await i18n['helper'].loadPageTranslations('en', 'about', routeTranslations)
352
+
353
+ expect(i18n.has('title')).toBe(true)
354
+ })
355
+ })
356
+
357
+ describe('clearCache() method', () => {
358
+ test('should clear all translations from cache', async () => {
359
+ const i18n = new TestI18n('en', 'en', 'general')
360
+ const translations: Translations = { greeting: 'Hello' }
361
+ i18n['helper'].loadTranslations('en', translations)
362
+
363
+ expect(i18n.has('greeting')).toBe(true)
364
+
365
+ i18n.clearCache()
366
+
367
+ expect(i18n.has('greeting')).toBe(false)
368
+ })
369
+ })
370
+
371
+ describe('loadTranslationsCore() method', () => {
372
+ test('should load translations when merge is false', async () => {
373
+ const i18n = new TestI18n('en', 'en', 'general')
374
+ const translations: Translations = { greeting: 'Hello' }
375
+
376
+ i18n['loadTranslationsCore']('en', translations, false)
377
+ // Wait for async operation to complete
378
+ await new Promise(resolve => setTimeout(resolve, 0))
379
+
380
+ expect(i18n.has('greeting')).toBe(true)
381
+ })
382
+
383
+ test('should merge translations when merge is true', async () => {
384
+ const i18n = new TestI18n('en', 'en', 'general')
385
+ const initial: Translations = { greeting: 'Hello' }
386
+ const additional: Translations = { farewell: 'Goodbye' }
387
+
388
+ await i18n['helper'].loadTranslations('en', initial)
389
+ i18n['loadTranslationsCore']('en', additional, true)
390
+ // Wait for async operation to complete
391
+ await new Promise(resolve => setTimeout(resolve, 0))
392
+
393
+ expect(i18n.has('greeting')).toBe(true)
394
+ expect(i18n.has('farewell')).toBe(true)
395
+ })
396
+ })
397
+
398
+ describe('loadRouteTranslationsCore() method', () => {
399
+ test('should load route translations when merge is false', async () => {
400
+ const i18n = new TestI18n('en', 'en', 'general')
401
+ const translations: Translations = { title: 'Page Title' }
402
+
403
+ i18n['loadRouteTranslationsCore']('en', 'about', translations, false)
404
+ // Wait for async operation to complete
405
+ await new Promise(resolve => setTimeout(resolve, 0))
406
+
407
+ expect(i18n.has('title', 'about')).toBe(true)
408
+ })
409
+
410
+ test('should merge route translations when merge is true', async () => {
411
+ const i18n = new TestI18n('en', 'en', 'general')
412
+ const initial: Translations = { title: 'Page Title' }
413
+ const additional: Translations = { description: 'Page Description' }
414
+
415
+ await i18n['helper'].loadPageTranslations('en', 'about', initial)
416
+ i18n['loadRouteTranslationsCore']('en', 'about', additional, true)
417
+ // Wait for async operation to complete
418
+ await new Promise(resolve => setTimeout(resolve, 0))
419
+
420
+ expect(i18n.has('title', 'about')).toBe(true)
421
+ expect(i18n.has('description', 'about')).toBe(true)
422
+ })
423
+ })
424
+
425
+ describe('Edge cases', () => {
426
+ test('should handle nested translation keys', async () => {
427
+ const i18n = new TestI18n('en', 'en', 'general')
428
+ const translations: Translations = {
429
+ nested: {
430
+ deep: {
431
+ key: 'Nested value',
432
+ },
433
+ },
434
+ }
435
+ i18n['helper'].loadTranslations('en', translations)
436
+
437
+ expect(i18n.t('nested.deep.key')).toBe('Nested value')
438
+ })
439
+
440
+ test('should handle multiple params in interpolation', async () => {
441
+ const i18n = new TestI18n('en', 'en', 'general')
442
+ const translations: Translations = {
443
+ message: 'Hello, {name}! You are {age} years old.',
444
+ }
445
+ i18n['helper'].loadTranslations('en', translations)
446
+
447
+ expect(i18n.t('message', { name: 'John', age: 30 })).toBe('Hello, John! You are 30 years old.')
448
+ })
449
+
450
+ test('should handle null defaultValue', () => {
451
+ const i18n = new TestI18n('en', 'en', 'general')
452
+ expect(i18n.t('missing.key', undefined, null)).toBe('missing.key')
453
+ })
454
+
455
+ test('should handle empty string defaultValue', () => {
456
+ const i18n = new TestI18n('en', 'en', 'general')
457
+ // Empty string is falsy, so it will fall back to key (as per defaultValue || key logic)
458
+ expect(i18n.t('missing.key', undefined, '')).toBe('missing.key')
459
+ })
460
+
461
+ test('should handle route change', async () => {
462
+ const storage = { translations: new Map<string, Translations>() }
463
+ const i18n = new TestI18n('en', 'en', 'general', { storage })
464
+ const generalTranslations: Translations = { greeting: 'Hello' }
465
+ const routeTranslations: Translations = { title: 'About Page' }
466
+
467
+ await i18n['helper'].loadTranslations('en', generalTranslations)
468
+ await i18n['helper'].loadPageTranslations('en', 'about', routeTranslations)
469
+
470
+ // Verify translations are loaded
471
+ expect(i18n.has('greeting')).toBe(true)
472
+ expect(i18n.has('title', 'about')).toBe(true)
473
+
474
+ // Check route-specific translation with explicit routeName
475
+ // Note: getTranslation uses routeName parameter, so this should work
476
+ const titleValue = i18n.t('title', undefined, undefined, 'about')
477
+ expect(titleValue).toBe('About Page')
478
+
479
+ // Change route and check
480
+ i18n.setRoute('about')
481
+ expect(i18n.t('title')).toBe('About Page')
482
+ expect(i18n.t('greeting')).toBe('Hello') // Should still work from general
483
+ })
484
+ })
485
+ })
@@ -13,7 +13,6 @@ describe('RouteService', () => {
13
13
  let routeService: RouteService
14
14
  let mockRouter: jest.Mocked<Router>
15
15
  let mockI18nConfig: ModuleOptionsExtend
16
- let setCookieMock: jest.Mock
17
16
  let navigateToMock: jest.Mock
18
17
 
19
18
  beforeEach(() => {
@@ -47,20 +46,15 @@ describe('RouteService', () => {
47
46
  hashMode: false,
48
47
  }
49
48
 
50
- // Mock setCookie and navigateTo
51
- setCookieMock = jest.fn()
52
49
  navigateToMock = jest.fn()
53
50
 
54
- // Initialize RouteService
51
+ // Initialize RouteService (no setCookie - plugin handles cookies)
55
52
  routeService = new RouteService(
56
53
  mockI18nConfig,
57
54
  mockRouter,
58
55
  null,
59
56
  null,
60
57
  navigateToMock,
61
- setCookieMock,
62
- null,
63
- null,
64
58
  )
65
59
  })
66
60
 
@@ -71,14 +65,14 @@ describe('RouteService', () => {
71
65
 
72
66
  test('getCurrentLocale should return the hash locale if hashMode is enabled', () => {
73
67
  const mockI18nConfigWithHashMode = { ...mockI18nConfig, hashMode: true }
74
- routeService = new RouteService(mockI18nConfigWithHashMode, mockRouter, 'de', null, navigateToMock, setCookieMock, null, null)
68
+ routeService = new RouteService(mockI18nConfigWithHashMode, mockRouter, 'de', null, navigateToMock)
75
69
  const locale = routeService.getCurrentLocale()
76
70
  expect(locale).toBe('de')
77
71
  })
78
72
 
79
73
  test('getCurrentLocale should return the noPrefix locale if noPrefix strategy is enabled', () => {
80
74
  const mockI18nConfigWithNoPrefix: ModuleOptionsExtend = { ...mockI18nConfig, strategy: 'no_prefix' }
81
- routeService = new RouteService(mockI18nConfigWithNoPrefix, mockRouter, null, 'ru', navigateToMock, setCookieMock, null, null)
75
+ routeService = new RouteService(mockI18nConfigWithNoPrefix, mockRouter, null, 'ru', navigateToMock)
82
76
  const locale = routeService.getCurrentLocale()
83
77
  expect(locale).toBe('ru')
84
78
  })
@@ -92,7 +86,7 @@ describe('RouteService', () => {
92
86
  { code: 'ru', iso: 'ru-RU', displayName: 'Russian' },
93
87
  ],
94
88
  }
95
- routeService = new RouteService(mockI18nConfigWithDisplayName, mockRouter, null, null, navigateToMock, setCookieMock, null, null)
89
+ routeService = new RouteService(mockI18nConfigWithDisplayName, mockRouter, null, null, navigateToMock)
96
90
  const route = { name: 'localized-about-en' } as RouteLocationNormalizedLoaded
97
91
  const displayName = routeService.getCurrentName(route)
98
92
  expect(displayName).toBe('English')
@@ -157,25 +151,6 @@ describe('RouteService', () => {
157
151
  expect(fullPath).toBe('https://example.com/de/about')
158
152
  })
159
153
 
160
- test('updateCookies should update cookies for hashMode and noPrefixStrategy', () => {
161
- const mockI18nConfigWithHashMode: ModuleOptionsExtend = {
162
- ...mockI18nConfig,
163
- hashMode: true,
164
- strategy: 'prefix_except_default',
165
- }
166
- routeService = new RouteService(mockI18nConfigWithHashMode, mockRouter, 'de', null, navigateToMock, setCookieMock, null, null)
167
- routeService.updateCookies('de')
168
- expect(setCookieMock).toHaveBeenCalledWith('hash-locale', 'de')
169
-
170
- const mockI18nConfigWithNoPrefix: ModuleOptionsExtend = {
171
- ...mockI18nConfig,
172
- strategy: 'no_prefix',
173
- }
174
- routeService = new RouteService(mockI18nConfigWithNoPrefix, mockRouter, null, 'ru', navigateToMock, setCookieMock, null, null)
175
- routeService.updateCookies('ru')
176
- expect(setCookieMock).toHaveBeenCalledWith('no-prefix-locale', 'ru')
177
- })
178
-
179
154
  test('resolveLocalizedRoute should return the correct localized route', () => {
180
155
  const to = { path: '/about' } as RouteLocationAsPathGeneric
181
156
  mockRouter.resolve.mockReturnValueOnce({
@@ -209,21 +184,38 @@ describe('RouteService', () => {
209
184
  expect(locale).toBe('ru') // Should extract from URL
210
185
  })
211
186
 
212
- test('getCurrentLocale should use cookie locale when route params and URL path are missing', () => {
187
+ test('getCurrentLocale should return defaultLocale for prefix_except_default when URL has no locale prefix', () => {
213
188
  routeService = new RouteService(
214
- mockI18nConfig,
189
+ mockI18nConfig, // strategy: prefix_except_default
215
190
  mockRouter,
216
191
  null,
217
192
  null,
218
193
  navigateToMock,
219
- setCookieMock,
220
- 'de', // cookie locale
221
- 'user-locale', // cookie name
194
+ () => 'de', // getter from plugin (cookie/state)
222
195
  )
223
196
  mockRouter.currentRoute.value.params = {} // Remove locale from params
224
197
  mockRouter.currentRoute.value.path = '/' // No locale in path
225
198
  const locale = routeService.getCurrentLocale()
226
- expect(locale).toBe('de') // Should use cookie
199
+ // For prefix_except_default: URL without locale = defaultLocale (SEO requirement)
200
+ // Getter is NOT used because URL explicitly indicates defaultLocale
201
+ expect(locale).toBe('en')
202
+ })
203
+
204
+ test('getCurrentLocale should use getDefaultLocale for no_prefix strategy', () => {
205
+ const noPrefixConfig = { ...mockI18nConfig, strategy: 'no_prefix' as const }
206
+ routeService = new RouteService(
207
+ noPrefixConfig,
208
+ mockRouter,
209
+ null,
210
+ 'de', // noPrefixDefault
211
+ navigateToMock,
212
+ () => 'de', // getter from plugin (cookie/state)
213
+ )
214
+ mockRouter.currentRoute.value.params = {} // Remove locale from params
215
+ mockRouter.currentRoute.value.path = '/' // No locale in path
216
+ const locale = routeService.getCurrentLocale()
217
+ // For no_prefix: getter/noPrefixDefault determines locale
218
+ expect(locale).toBe('de')
227
219
  })
228
220
 
229
221
  test('getCurrentLocale should prioritize route params over URL path', () => {
@@ -233,36 +225,19 @@ describe('RouteService', () => {
233
225
  expect(locale).toBe('de') // Should use route params
234
226
  })
235
227
 
236
- test('getCurrentLocale should prioritize URL path over cookie', () => {
228
+ test('getCurrentLocale should prioritize URL path over getDefaultLocale', () => {
237
229
  routeService = new RouteService(
238
230
  mockI18nConfig,
239
231
  mockRouter,
240
232
  null,
241
233
  null,
242
234
  navigateToMock,
243
- setCookieMock,
244
- 'de', // cookie locale
245
- 'user-locale', // cookie name
235
+ () => 'de', // getter
246
236
  )
247
237
  mockRouter.currentRoute.value.params = {} // No locale in params
248
238
  mockRouter.currentRoute.value.path = '/ru/sdfsdf' // URL has locale
249
239
  const locale = routeService.getCurrentLocale()
250
- expect(locale).toBe('ru') // Should use URL path, not cookie
251
- })
252
-
253
- test('updateCookies should update cookie for regular strategy', () => {
254
- routeService = new RouteService(
255
- mockI18nConfig,
256
- mockRouter,
257
- null,
258
- null,
259
- navigateToMock,
260
- setCookieMock,
261
- null,
262
- 'user-locale', // cookie name
263
- )
264
- routeService.updateCookies('ru')
265
- expect(setCookieMock).toHaveBeenCalledWith('user-locale', 'ru')
240
+ expect(locale).toBe('ru') // Should use URL path, not getter
266
241
  })
267
242
 
268
243
  test('getCurrentName should return null if displayName is missing', () => {
@@ -274,7 +249,7 @@ describe('RouteService', () => {
274
249
  { code: 'ru', iso: 'ru-RU' },
275
250
  ],
276
251
  }
277
- routeService = new RouteService(mockI18nConfigWithoutDisplayName, mockRouter, null, null, navigateToMock, setCookieMock, null, null)
252
+ routeService = new RouteService(mockI18nConfigWithoutDisplayName, mockRouter, null, null, navigateToMock)
278
253
  const route = { name: 'localized-about-en' } as RouteLocationNormalizedLoaded
279
254
  const displayName = routeService.getCurrentName(route)
280
255
  expect(displayName).toBeNull()
@@ -350,11 +325,6 @@ describe('RouteService', () => {
350
325
  })
351
326
  })
352
327
 
353
- test('updateCookies should not update cookies if hashMode and noPrefixStrategy are disabled', () => {
354
- routeService.updateCookies('de')
355
- expect(setCookieMock).not.toHaveBeenCalled()
356
- })
357
-
358
328
  test('getCurrentLocale should extract locale from fullPath with query params when route.path is missing', () => {
359
329
  mockRouter.currentRoute.value.params = {}
360
330
  mockRouter.currentRoute.value.path = '' // Simulate missing path (e.g. error state)