@pyreon/i18n 0.11.5 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,120 +1,120 @@
1
- import { effect } from "@pyreon/reactivity"
2
- import { mount } from "@pyreon/runtime-dom"
3
- import { I18nProvider, useI18n } from "../context"
4
- import { createI18n } from "../create-i18n"
5
- import { interpolate } from "../interpolation"
6
- import { resolvePluralCategory } from "../pluralization"
7
- import { parseRichText, Trans } from "../trans"
8
- import type { TranslationDictionary } from "../types"
1
+ import { effect } from '@pyreon/reactivity'
2
+ import { mount } from '@pyreon/runtime-dom'
3
+ import { I18nProvider, useI18n } from '../context'
4
+ import { createI18n } from '../create-i18n'
5
+ import { interpolate } from '../interpolation'
6
+ import { resolvePluralCategory } from '../pluralization'
7
+ import { parseRichText, Trans } from '../trans'
8
+ import type { TranslationDictionary } from '../types'
9
9
 
10
10
  // ─── interpolate ─────────────────────────────────────────────────────────────
11
11
 
12
- describe("interpolate", () => {
13
- it("replaces placeholders with values", () => {
14
- expect(interpolate("Hello {{name}}!", { name: "Alice" })).toBe("Hello Alice!")
12
+ describe('interpolate', () => {
13
+ it('replaces placeholders with values', () => {
14
+ expect(interpolate('Hello {{name}}!', { name: 'Alice' })).toBe('Hello Alice!')
15
15
  })
16
16
 
17
- it("handles multiple placeholders", () => {
18
- expect(interpolate("{{greeting}}, {{name}}!", { greeting: "Hi", name: "Bob" })).toBe("Hi, Bob!")
17
+ it('handles multiple placeholders', () => {
18
+ expect(interpolate('{{greeting}}, {{name}}!', { greeting: 'Hi', name: 'Bob' })).toBe('Hi, Bob!')
19
19
  })
20
20
 
21
- it("handles whitespace inside braces", () => {
22
- expect(interpolate("Hello {{ name }}!", { name: "Alice" })).toBe("Hello Alice!")
21
+ it('handles whitespace inside braces', () => {
22
+ expect(interpolate('Hello {{ name }}!', { name: 'Alice' })).toBe('Hello Alice!')
23
23
  })
24
24
 
25
- it("leaves unmatched placeholders as-is", () => {
26
- expect(interpolate("Hello {{name}}!", {})).toBe("Hello {{name}}!")
25
+ it('leaves unmatched placeholders as-is', () => {
26
+ expect(interpolate('Hello {{name}}!', {})).toBe('Hello {{name}}!')
27
27
  })
28
28
 
29
- it("returns template unchanged when no values provided", () => {
30
- expect(interpolate("Hello {{name}}!")).toBe("Hello {{name}}!")
29
+ it('returns template unchanged when no values provided', () => {
30
+ expect(interpolate('Hello {{name}}!')).toBe('Hello {{name}}!')
31
31
  })
32
32
 
33
- it("returns template unchanged when no placeholders present", () => {
34
- expect(interpolate("Hello world!", { name: "Alice" })).toBe("Hello world!")
33
+ it('returns template unchanged when no placeholders present', () => {
34
+ expect(interpolate('Hello world!', { name: 'Alice' })).toBe('Hello world!')
35
35
  })
36
36
 
37
- it("handles number values", () => {
38
- expect(interpolate("Count: {{count}}", { count: 42 })).toBe("Count: 42")
37
+ it('handles number values', () => {
38
+ expect(interpolate('Count: {{count}}', { count: 42 })).toBe('Count: 42')
39
39
  })
40
40
  })
41
41
 
42
42
  // ─── resolvePluralCategory ───────────────────────────────────────────────────
43
43
 
44
- describe("resolvePluralCategory", () => {
44
+ describe('resolvePluralCategory', () => {
45
45
  it('returns "one" for count 1 in English', () => {
46
- expect(resolvePluralCategory("en", 1)).toBe("one")
46
+ expect(resolvePluralCategory('en', 1)).toBe('one')
47
47
  })
48
48
 
49
49
  it('returns "other" for count 0 in English', () => {
50
- expect(resolvePluralCategory("en", 0)).toBe("other")
50
+ expect(resolvePluralCategory('en', 0)).toBe('other')
51
51
  })
52
52
 
53
53
  it('returns "other" for count > 1 in English', () => {
54
- expect(resolvePluralCategory("en", 5)).toBe("other")
54
+ expect(resolvePluralCategory('en', 5)).toBe('other')
55
55
  })
56
56
 
57
- it("uses custom plural rules when provided", () => {
57
+ it('uses custom plural rules when provided', () => {
58
58
  const rules = {
59
- custom: (count: number) => (count === 0 ? "zero" : count === 1 ? "one" : "other"),
59
+ custom: (count: number) => (count === 0 ? 'zero' : count === 1 ? 'one' : 'other'),
60
60
  }
61
- expect(resolvePluralCategory("custom", 0, rules)).toBe("zero")
62
- expect(resolvePluralCategory("custom", 1, rules)).toBe("one")
63
- expect(resolvePluralCategory("custom", 5, rules)).toBe("other")
61
+ expect(resolvePluralCategory('custom', 0, rules)).toBe('zero')
62
+ expect(resolvePluralCategory('custom', 1, rules)).toBe('one')
63
+ expect(resolvePluralCategory('custom', 5, rules)).toBe('other')
64
64
  })
65
65
  })
66
66
 
67
67
  // ─── createI18n ──────────────────────────────────────────────────────────────
68
68
 
69
- describe("createI18n", () => {
69
+ describe('createI18n', () => {
70
70
  const en = {
71
- greeting: "Hello {{name}}!",
72
- farewell: "Goodbye",
71
+ greeting: 'Hello {{name}}!',
72
+ farewell: 'Goodbye',
73
73
  nested: {
74
74
  deep: {
75
- key: "Deep value",
75
+ key: 'Deep value',
76
76
  },
77
77
  },
78
78
  }
79
79
 
80
80
  const de = {
81
- greeting: "Hallo {{name}}!",
82
- farewell: "Auf Wiedersehen",
81
+ greeting: 'Hallo {{name}}!',
82
+ farewell: 'Auf Wiedersehen',
83
83
  nested: {
84
84
  deep: {
85
- key: "Tiefer Wert",
85
+ key: 'Tiefer Wert',
86
86
  },
87
87
  },
88
88
  }
89
89
 
90
- it("translates a simple key", () => {
91
- const i18n = createI18n({ locale: "en", messages: { en } })
92
- expect(i18n.t("greeting", { name: "Alice" })).toBe("Hello Alice!")
90
+ it('translates a simple key', () => {
91
+ const i18n = createI18n({ locale: 'en', messages: { en } })
92
+ expect(i18n.t('greeting', { name: 'Alice' })).toBe('Hello Alice!')
93
93
  })
94
94
 
95
- it("translates nested keys with dot notation", () => {
96
- const i18n = createI18n({ locale: "en", messages: { en } })
97
- expect(i18n.t("nested.deep.key")).toBe("Deep value")
95
+ it('translates nested keys with dot notation', () => {
96
+ const i18n = createI18n({ locale: 'en', messages: { en } })
97
+ expect(i18n.t('nested.deep.key')).toBe('Deep value')
98
98
  })
99
99
 
100
- it("returns the key itself when missing", () => {
101
- const i18n = createI18n({ locale: "en", messages: { en } })
102
- expect(i18n.t("nonexistent.key")).toBe("nonexistent.key")
100
+ it('returns the key itself when missing', () => {
101
+ const i18n = createI18n({ locale: 'en', messages: { en } })
102
+ expect(i18n.t('nonexistent.key')).toBe('nonexistent.key')
103
103
  })
104
104
 
105
- it("falls back to fallbackLocale when key is missing", () => {
105
+ it('falls back to fallbackLocale when key is missing', () => {
106
106
  const i18n = createI18n({
107
- locale: "de",
108
- fallbackLocale: "en",
109
- messages: { en: { ...en, onlyEn: "English only" }, de },
107
+ locale: 'de',
108
+ fallbackLocale: 'en',
109
+ messages: { en: { ...en, onlyEn: 'English only' }, de },
110
110
  })
111
- expect(i18n.t("onlyEn")).toBe("English only")
111
+ expect(i18n.t('onlyEn')).toBe('English only')
112
112
  })
113
113
 
114
- it("calls onMissingKey when translation is not found", () => {
114
+ it('calls onMissingKey when translation is not found', () => {
115
115
  const missing: string[] = []
116
116
  const i18n = createI18n({
117
- locale: "en",
117
+ locale: 'en',
118
118
  messages: { en },
119
119
  onMissingKey: (locale, key) => {
120
120
  missing.push(`${locale}:${key}`)
@@ -122,13 +122,13 @@ describe("createI18n", () => {
122
122
  },
123
123
  })
124
124
 
125
- expect(i18n.t("unknown")).toBe("[MISSING: unknown]")
126
- expect(missing).toEqual(["en:unknown"])
125
+ expect(i18n.t('unknown')).toBe('[MISSING: unknown]')
126
+ expect(missing).toEqual(['en:unknown'])
127
127
  })
128
128
 
129
- it("falls back to key when onMissingKey returns void", () => {
129
+ it('falls back to key when onMissingKey returns void', () => {
130
130
  const i18n = createI18n({
131
- locale: "en",
131
+ locale: 'en',
132
132
  messages: { en },
133
133
  onMissingKey: () => {
134
134
  // intentionally returns undefined
@@ -136,196 +136,196 @@ describe("createI18n", () => {
136
136
  },
137
137
  })
138
138
 
139
- expect(i18n.t("unknown")).toBe("unknown")
139
+ expect(i18n.t('unknown')).toBe('unknown')
140
140
  })
141
141
 
142
- it("uses fallback locale for plural forms", () => {
142
+ it('uses fallback locale for plural forms', () => {
143
143
  const i18n = createI18n({
144
- locale: "de",
145
- fallbackLocale: "en",
144
+ locale: 'de',
145
+ fallbackLocale: 'en',
146
146
  messages: {
147
147
  en: {
148
- items_one: "{{count}} item",
149
- items_other: "{{count}} items",
148
+ items_one: '{{count}} item',
149
+ items_other: '{{count}} items',
150
150
  },
151
151
  de: {},
152
152
  },
153
153
  })
154
154
 
155
155
  // German has no items translation, falls back to English plural forms
156
- expect(i18n.t("items", { count: 1 })).toBe("1 item")
157
- expect(i18n.t("items", { count: 5 })).toBe("5 items")
156
+ expect(i18n.t('items', { count: 1 })).toBe('1 item')
157
+ expect(i18n.t('items', { count: 5 })).toBe('5 items')
158
158
  })
159
159
 
160
- it("reactively updates when locale changes", () => {
160
+ it('reactively updates when locale changes', () => {
161
161
  const i18n = createI18n({
162
- locale: "en",
162
+ locale: 'en',
163
163
  messages: { en, de },
164
164
  })
165
165
 
166
166
  const results: string[] = []
167
167
  const cleanup = effect(() => {
168
- results.push(i18n.t("farewell"))
168
+ results.push(i18n.t('farewell'))
169
169
  })
170
170
 
171
- expect(results).toEqual(["Goodbye"])
171
+ expect(results).toEqual(['Goodbye'])
172
172
 
173
- i18n.locale.set("de")
174
- expect(results).toEqual(["Goodbye", "Auf Wiedersehen"])
173
+ i18n.locale.set('de')
174
+ expect(results).toEqual(['Goodbye', 'Auf Wiedersehen'])
175
175
 
176
176
  cleanup.dispose()
177
177
  })
178
178
 
179
- it("reactively updates when messages are added", () => {
179
+ it('reactively updates when messages are added', () => {
180
180
  const i18n = createI18n({
181
- locale: "en",
181
+ locale: 'en',
182
182
  messages: { en },
183
183
  })
184
184
 
185
185
  const results: string[] = []
186
186
  const cleanup = effect(() => {
187
- results.push(i18n.t("newKey"))
187
+ results.push(i18n.t('newKey'))
188
188
  })
189
189
 
190
- expect(results).toEqual(["newKey"]) // Missing key returns key itself
190
+ expect(results).toEqual(['newKey']) // Missing key returns key itself
191
191
 
192
- i18n.addMessages("en", { newKey: "New value!" })
193
- expect(results).toEqual(["newKey", "New value!"])
192
+ i18n.addMessages('en', { newKey: 'New value!' })
193
+ expect(results).toEqual(['newKey', 'New value!'])
194
194
 
195
195
  cleanup.dispose()
196
196
  })
197
197
 
198
- it("reports available locales", () => {
198
+ it('reports available locales', () => {
199
199
  const i18n = createI18n({
200
- locale: "en",
200
+ locale: 'en',
201
201
  messages: { en, de },
202
202
  })
203
- expect(i18n.availableLocales()).toEqual(["en", "de"])
203
+ expect(i18n.availableLocales()).toEqual(['en', 'de'])
204
204
  })
205
205
 
206
- it("exists() checks key presence", () => {
207
- const i18n = createI18n({ locale: "en", messages: { en } })
208
- expect(i18n.exists("greeting")).toBe(true)
209
- expect(i18n.exists("nested.deep.key")).toBe(true)
210
- expect(i18n.exists("nonexistent")).toBe(false)
206
+ it('exists() checks key presence', () => {
207
+ const i18n = createI18n({ locale: 'en', messages: { en } })
208
+ expect(i18n.exists('greeting')).toBe(true)
209
+ expect(i18n.exists('nested.deep.key')).toBe(true)
210
+ expect(i18n.exists('nonexistent')).toBe(false)
211
211
  })
212
212
 
213
- it("exists() checks namespaced keys", () => {
213
+ it('exists() checks namespaced keys', () => {
214
214
  const i18n = createI18n({
215
- locale: "en",
215
+ locale: 'en',
216
216
  messages: { en: {} },
217
217
  })
218
- i18n.addMessages("en", { title: "Admin Panel" }, "admin")
219
- expect(i18n.exists("admin:title")).toBe(true)
220
- expect(i18n.exists("admin:nonexistent")).toBe(false)
218
+ i18n.addMessages('en', { title: 'Admin Panel' }, 'admin')
219
+ expect(i18n.exists('admin:title')).toBe(true)
220
+ expect(i18n.exists('admin:nonexistent')).toBe(false)
221
221
  })
222
222
 
223
- it("exists() checks fallback locale", () => {
223
+ it('exists() checks fallback locale', () => {
224
224
  const i18n = createI18n({
225
- locale: "de",
226
- fallbackLocale: "en",
227
- messages: { en: { ...en, onlyEn: "yes" }, de },
225
+ locale: 'de',
226
+ fallbackLocale: 'en',
227
+ messages: { en: { ...en, onlyEn: 'yes' }, de },
228
228
  })
229
- expect(i18n.exists("onlyEn")).toBe(true)
229
+ expect(i18n.exists('onlyEn')).toBe(true)
230
230
  })
231
231
  })
232
232
 
233
233
  // ─── Pluralization ───────────────────────────────────────────────────────────
234
234
 
235
- describe("createI18n pluralization", () => {
236
- it("selects the correct plural form", () => {
235
+ describe('createI18n pluralization', () => {
236
+ it('selects the correct plural form', () => {
237
237
  const i18n = createI18n({
238
- locale: "en",
238
+ locale: 'en',
239
239
  messages: {
240
240
  en: {
241
- items_one: "{{count}} item",
242
- items_other: "{{count}} items",
241
+ items_one: '{{count}} item',
242
+ items_other: '{{count}} items',
243
243
  },
244
244
  },
245
245
  })
246
246
 
247
- expect(i18n.t("items", { count: 1 })).toBe("1 item")
248
- expect(i18n.t("items", { count: 5 })).toBe("5 items")
249
- expect(i18n.t("items", { count: 0 })).toBe("0 items")
247
+ expect(i18n.t('items', { count: 1 })).toBe('1 item')
248
+ expect(i18n.t('items', { count: 5 })).toBe('5 items')
249
+ expect(i18n.t('items', { count: 0 })).toBe('0 items')
250
250
  })
251
251
 
252
- it("falls back to base key when plural form is missing", () => {
252
+ it('falls back to base key when plural form is missing', () => {
253
253
  const i18n = createI18n({
254
- locale: "en",
254
+ locale: 'en',
255
255
  messages: {
256
256
  en: {
257
- items: "some items", // No _one or _other suffixes
257
+ items: 'some items', // No _one or _other suffixes
258
258
  },
259
259
  },
260
260
  })
261
261
 
262
- expect(i18n.t("items", { count: 1 })).toBe("some items")
262
+ expect(i18n.t('items', { count: 1 })).toBe('some items')
263
263
  })
264
264
 
265
- it("falls back to basic rules when Intl.PluralRules fails", () => {
265
+ it('falls back to basic rules when Intl.PluralRules fails', () => {
266
266
  // Use an invalid locale to trigger the catch branch
267
- const category1 = resolvePluralCategory("invalid-xxx-yyy", 1)
268
- const category5 = resolvePluralCategory("invalid-xxx-yyy", 5)
267
+ const category1 = resolvePluralCategory('invalid-xxx-yyy', 1)
268
+ const category5 = resolvePluralCategory('invalid-xxx-yyy', 5)
269
269
  // Intl.PluralRules may either handle it or throw — either way we get a result
270
- expect(typeof category1).toBe("string")
271
- expect(typeof category5).toBe("string")
270
+ expect(typeof category1).toBe('string')
271
+ expect(typeof category5).toBe('string')
272
272
  })
273
273
 
274
- it("uses custom plural rules", () => {
274
+ it('uses custom plural rules', () => {
275
275
  const i18n = createI18n({
276
- locale: "ar",
276
+ locale: 'ar',
277
277
  pluralRules: {
278
278
  ar: (count: number) => {
279
- if (count === 0) return "zero"
280
- if (count === 1) return "one"
281
- if (count === 2) return "two"
282
- if (count >= 3 && count <= 10) return "few"
283
- return "other"
279
+ if (count === 0) return 'zero'
280
+ if (count === 1) return 'one'
281
+ if (count === 2) return 'two'
282
+ if (count >= 3 && count <= 10) return 'few'
283
+ return 'other'
284
284
  },
285
285
  },
286
286
  messages: {
287
287
  ar: {
288
- items_zero: "لا عناصر",
289
- items_one: "عنصر واحد",
290
- items_two: "عنصران",
291
- items_few: "{{count}} عناصر",
292
- items_other: "{{count}} عنصراً",
288
+ items_zero: 'لا عناصر',
289
+ items_one: 'عنصر واحد',
290
+ items_two: 'عنصران',
291
+ items_few: '{{count}} عناصر',
292
+ items_other: '{{count}} عنصراً',
293
293
  },
294
294
  },
295
295
  })
296
296
 
297
- expect(i18n.t("items", { count: 0 })).toBe("لا عناصر")
298
- expect(i18n.t("items", { count: 1 })).toBe("عنصر واحد")
299
- expect(i18n.t("items", { count: 2 })).toBe("عنصران")
300
- expect(i18n.t("items", { count: 5 })).toBe("5 عناصر")
301
- expect(i18n.t("items", { count: 100 })).toBe("100 عنصراً")
297
+ expect(i18n.t('items', { count: 0 })).toBe('لا عناصر')
298
+ expect(i18n.t('items', { count: 1 })).toBe('عنصر واحد')
299
+ expect(i18n.t('items', { count: 2 })).toBe('عنصران')
300
+ expect(i18n.t('items', { count: 5 })).toBe('5 عناصر')
301
+ expect(i18n.t('items', { count: 100 })).toBe('100 عنصراً')
302
302
  })
303
303
  })
304
304
 
305
305
  // ─── Namespaces ──────────────────────────────────────────────────────────────
306
306
 
307
- describe("createI18n namespaces", () => {
308
- it("supports namespace:key syntax", () => {
307
+ describe('createI18n namespaces', () => {
308
+ it('supports namespace:key syntax', () => {
309
309
  const i18n = createI18n({
310
- locale: "en",
311
- messages: { en: { greeting: "Hi" } },
310
+ locale: 'en',
311
+ messages: { en: { greeting: 'Hi' } },
312
312
  })
313
313
  // "greeting" is in the default "common" namespace
314
- expect(i18n.t("greeting")).toBe("Hi")
314
+ expect(i18n.t('greeting')).toBe('Hi')
315
315
  })
316
316
 
317
- it("loads namespaces asynchronously", async () => {
317
+ it('loads namespaces asynchronously', async () => {
318
318
  const loaderCalls: string[] = []
319
319
 
320
320
  const i18n = createI18n({
321
- locale: "en",
321
+ locale: 'en',
322
322
  loader: async (locale, namespace) => {
323
323
  loaderCalls.push(`${locale}:${namespace}`)
324
- if (namespace === "auth") {
324
+ if (namespace === 'auth') {
325
325
  return {
326
- login: "Log in",
326
+ login: 'Log in',
327
327
  errors: {
328
- invalid: "Invalid credentials",
328
+ invalid: 'Invalid credentials',
329
329
  },
330
330
  }
331
331
  }
@@ -333,20 +333,20 @@ describe("createI18n namespaces", () => {
333
333
  },
334
334
  })
335
335
 
336
- expect(i18n.t("auth:login")).toBe("auth:login") // Not loaded yet
336
+ expect(i18n.t('auth:login')).toBe('auth:login') // Not loaded yet
337
337
 
338
- await i18n.loadNamespace("auth")
338
+ await i18n.loadNamespace('auth')
339
339
 
340
- expect(loaderCalls).toEqual(["en:auth"])
341
- expect(i18n.t("auth:login")).toBe("Log in")
342
- expect(i18n.t("auth:errors.invalid")).toBe("Invalid credentials")
340
+ expect(loaderCalls).toEqual(['en:auth'])
341
+ expect(i18n.t('auth:login')).toBe('Log in')
342
+ expect(i18n.t('auth:errors.invalid')).toBe('Invalid credentials')
343
343
  })
344
344
 
345
- it("tracks loading state", async () => {
345
+ it('tracks loading state', async () => {
346
346
  let resolveLoader: ((dict: TranslationDictionary) => void) | undefined
347
347
 
348
348
  const i18n = createI18n({
349
- locale: "en",
349
+ locale: 'en',
350
350
  loader: async () => {
351
351
  return new Promise<TranslationDictionary>((resolve) => {
352
352
  resolveLoader = resolve
@@ -356,231 +356,231 @@ describe("createI18n namespaces", () => {
356
356
 
357
357
  expect(i18n.isLoading()).toBe(false)
358
358
 
359
- const loadPromise = i18n.loadNamespace("test")
359
+ const loadPromise = i18n.loadNamespace('test')
360
360
  expect(i18n.isLoading()).toBe(true)
361
361
 
362
- resolveLoader!({ hello: "world" })
362
+ resolveLoader!({ hello: 'world' })
363
363
  await loadPromise
364
364
 
365
365
  expect(i18n.isLoading()).toBe(false)
366
366
  })
367
367
 
368
- it("does not re-fetch already loaded namespaces", async () => {
368
+ it('does not re-fetch already loaded namespaces', async () => {
369
369
  let callCount = 0
370
370
 
371
371
  const i18n = createI18n({
372
- locale: "en",
372
+ locale: 'en',
373
373
  loader: async () => {
374
374
  callCount++
375
- return { key: "value" }
375
+ return { key: 'value' }
376
376
  },
377
377
  })
378
378
 
379
- await i18n.loadNamespace("test")
380
- await i18n.loadNamespace("test")
379
+ await i18n.loadNamespace('test')
380
+ await i18n.loadNamespace('test')
381
381
 
382
382
  expect(callCount).toBe(1)
383
383
  })
384
384
 
385
- it("deduplicates concurrent loads for the same namespace", async () => {
385
+ it('deduplicates concurrent loads for the same namespace', async () => {
386
386
  let callCount = 0
387
387
 
388
388
  const i18n = createI18n({
389
- locale: "en",
389
+ locale: 'en',
390
390
  loader: async () => {
391
391
  callCount++
392
- return { key: "value" }
392
+ return { key: 'value' }
393
393
  },
394
394
  })
395
395
 
396
396
  // Fire two loads concurrently — should only call loader once
397
- await Promise.all([i18n.loadNamespace("test"), i18n.loadNamespace("test")])
397
+ await Promise.all([i18n.loadNamespace('test'), i18n.loadNamespace('test')])
398
398
 
399
399
  expect(callCount).toBe(1)
400
400
  })
401
401
 
402
- it("loadedNamespaces reflects current locale namespaces", async () => {
402
+ it('loadedNamespaces reflects current locale namespaces', async () => {
403
403
  const i18n = createI18n({
404
- locale: "en",
405
- loader: async (_locale, ns) => ({ [`${ns}Key`]: "value" }),
404
+ locale: 'en',
405
+ loader: async (_locale, ns) => ({ [`${ns}Key`]: 'value' }),
406
406
  })
407
407
 
408
408
  expect(i18n.loadedNamespaces().size).toBe(0)
409
409
 
410
- await i18n.loadNamespace("auth")
411
- expect(i18n.loadedNamespaces().has("auth")).toBe(true)
410
+ await i18n.loadNamespace('auth')
411
+ expect(i18n.loadedNamespaces().has('auth')).toBe(true)
412
412
 
413
- await i18n.loadNamespace("admin")
414
- expect(i18n.loadedNamespaces().has("auth")).toBe(true)
415
- expect(i18n.loadedNamespaces().has("admin")).toBe(true)
413
+ await i18n.loadNamespace('admin')
414
+ expect(i18n.loadedNamespaces().has('auth')).toBe(true)
415
+ expect(i18n.loadedNamespaces().has('admin')).toBe(true)
416
416
  })
417
417
 
418
- it("loadNamespace is a no-op without a loader", async () => {
418
+ it('loadNamespace is a no-op without a loader', async () => {
419
419
  const i18n = createI18n({
420
- locale: "en",
421
- messages: { en: { key: "value" } },
420
+ locale: 'en',
421
+ messages: { en: { key: 'value' } },
422
422
  })
423
423
 
424
- await i18n.loadNamespace("test") // Should not throw
424
+ await i18n.loadNamespace('test') // Should not throw
425
425
  expect(i18n.isLoading()).toBe(false)
426
426
  })
427
427
 
428
- it("handles loader returning undefined", async () => {
428
+ it('handles loader returning undefined', async () => {
429
429
  const i18n = createI18n({
430
- locale: "en",
430
+ locale: 'en',
431
431
  loader: async () => undefined,
432
432
  })
433
433
 
434
- await i18n.loadNamespace("missing")
435
- expect(i18n.loadedNamespaces().has("missing")).toBe(false)
434
+ await i18n.loadNamespace('missing')
435
+ expect(i18n.loadedNamespaces().has('missing')).toBe(false)
436
436
  })
437
437
 
438
- it("handles loader errors gracefully", async () => {
438
+ it('handles loader errors gracefully', async () => {
439
439
  const i18n = createI18n({
440
- locale: "en",
440
+ locale: 'en',
441
441
  loader: async () => {
442
- throw new Error("Network error")
442
+ throw new Error('Network error')
443
443
  },
444
444
  })
445
445
 
446
- await expect(i18n.loadNamespace("test")).rejects.toThrow("Network error")
446
+ await expect(i18n.loadNamespace('test')).rejects.toThrow('Network error')
447
447
  expect(i18n.isLoading()).toBe(false) // Loading state cleaned up
448
448
  })
449
449
 
450
- it("addMessages does not corrupt store when source is mutated", () => {
451
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
450
+ it('addMessages does not corrupt store when source is mutated', () => {
451
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
452
452
 
453
- const source = { greeting: "Hello" }
454
- i18n.addMessages("en", source)
453
+ const source = { greeting: 'Hello' }
454
+ i18n.addMessages('en', source)
455
455
 
456
456
  // Mutating the source should not affect the store
457
- source.greeting = "MUTATED"
458
- expect(i18n.t("greeting")).toBe("Hello")
457
+ source.greeting = 'MUTATED'
458
+ expect(i18n.t('greeting')).toBe('Hello')
459
459
  })
460
460
 
461
- it("loads namespace for a specific locale", async () => {
461
+ it('loads namespace for a specific locale', async () => {
462
462
  const loaderCalls: string[] = []
463
463
 
464
464
  const i18n = createI18n({
465
- locale: "en",
465
+ locale: 'en',
466
466
  loader: async (locale, namespace) => {
467
467
  loaderCalls.push(`${locale}:${namespace}`)
468
468
  return { key: `${locale} value` }
469
469
  },
470
470
  })
471
471
 
472
- await i18n.loadNamespace("common", "de")
473
- expect(loaderCalls).toEqual(["de:common"])
472
+ await i18n.loadNamespace('common', 'de')
473
+ expect(loaderCalls).toEqual(['de:common'])
474
474
  })
475
475
 
476
- it("addMessages merges into existing namespace", () => {
476
+ it('addMessages merges into existing namespace', () => {
477
477
  const i18n = createI18n({
478
- locale: "en",
478
+ locale: 'en',
479
479
  messages: {
480
- en: { existing: "yes" },
480
+ en: { existing: 'yes' },
481
481
  },
482
482
  })
483
483
 
484
- i18n.addMessages("en", { added: "also yes" })
485
- expect(i18n.t("existing")).toBe("yes")
486
- expect(i18n.t("added")).toBe("also yes")
484
+ i18n.addMessages('en', { added: 'also yes' })
485
+ expect(i18n.t('existing')).toBe('yes')
486
+ expect(i18n.t('added')).toBe('also yes')
487
487
  })
488
488
 
489
- it("addMessages deep-merges nested dictionaries", () => {
489
+ it('addMessages deep-merges nested dictionaries', () => {
490
490
  const i18n = createI18n({
491
- locale: "en",
491
+ locale: 'en',
492
492
  messages: {
493
493
  en: {
494
494
  errors: {
495
- auth: "Auth error",
495
+ auth: 'Auth error',
496
496
  },
497
497
  },
498
498
  },
499
499
  })
500
500
 
501
- i18n.addMessages("en", {
501
+ i18n.addMessages('en', {
502
502
  errors: {
503
- network: "Network error",
503
+ network: 'Network error',
504
504
  },
505
505
  })
506
506
 
507
- expect(i18n.t("errors.auth")).toBe("Auth error")
508
- expect(i18n.t("errors.network")).toBe("Network error")
507
+ expect(i18n.t('errors.auth')).toBe('Auth error')
508
+ expect(i18n.t('errors.network')).toBe('Network error')
509
509
  })
510
510
 
511
- it("addMessages to a specific namespace", () => {
511
+ it('addMessages to a specific namespace', () => {
512
512
  const i18n = createI18n({
513
- locale: "en",
513
+ locale: 'en',
514
514
  messages: { en: {} },
515
515
  })
516
516
 
517
- i18n.addMessages("en", { title: "Dashboard" }, "admin")
518
- expect(i18n.t("admin:title")).toBe("Dashboard")
517
+ i18n.addMessages('en', { title: 'Dashboard' }, 'admin')
518
+ expect(i18n.t('admin:title')).toBe('Dashboard')
519
519
  })
520
520
 
521
- it("addMessages creates locale if not exists", () => {
522
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
521
+ it('addMessages creates locale if not exists', () => {
522
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
523
523
 
524
- i18n.addMessages("fr", { greeting: "Bonjour" })
525
- i18n.locale.set("fr")
526
- expect(i18n.t("greeting")).toBe("Bonjour")
527
- expect(i18n.availableLocales()).toContain("fr")
524
+ i18n.addMessages('fr', { greeting: 'Bonjour' })
525
+ i18n.locale.set('fr')
526
+ expect(i18n.t('greeting')).toBe('Bonjour')
527
+ expect(i18n.availableLocales()).toContain('fr')
528
528
  })
529
529
  })
530
530
 
531
531
  // ─── Locale switching with namespaces ────────────────────────────────────────
532
532
 
533
- describe("createI18n locale switching", () => {
534
- it("reloads translations when switching locale with loader", async () => {
533
+ describe('createI18n locale switching', () => {
534
+ it('reloads translations when switching locale with loader', async () => {
535
535
  const translations: Record<string, Record<string, TranslationDictionary>> = {
536
- en: { common: { hello: "Hello" } },
537
- de: { common: { hello: "Hallo" } },
536
+ en: { common: { hello: 'Hello' } },
537
+ de: { common: { hello: 'Hallo' } },
538
538
  }
539
539
 
540
540
  const i18n = createI18n({
541
- locale: "en",
541
+ locale: 'en',
542
542
  loader: async (locale, ns) => translations[locale]?.[ns],
543
543
  })
544
544
 
545
- await i18n.loadNamespace("common")
546
- expect(i18n.t("hello")).toBe("Hello")
545
+ await i18n.loadNamespace('common')
546
+ expect(i18n.t('hello')).toBe('Hello')
547
547
 
548
- i18n.locale.set("de")
549
- await i18n.loadNamespace("common")
550
- expect(i18n.t("hello")).toBe("Hallo")
548
+ i18n.locale.set('de')
549
+ await i18n.loadNamespace('common')
550
+ expect(i18n.t('hello')).toBe('Hallo')
551
551
  })
552
552
 
553
- it("handles mixed static + async messages", async () => {
553
+ it('handles mixed static + async messages', async () => {
554
554
  const i18n = createI18n({
555
- locale: "en",
555
+ locale: 'en',
556
556
  messages: {
557
- en: { staticKey: "From static" },
557
+ en: { staticKey: 'From static' },
558
558
  },
559
559
  loader: async (_locale, ns) => {
560
- if (ns === "dynamic") return { dynamicKey: "From loader" }
560
+ if (ns === 'dynamic') return { dynamicKey: 'From loader' }
561
561
  return undefined
562
562
  },
563
563
  })
564
564
 
565
- expect(i18n.t("staticKey")).toBe("From static")
566
- expect(i18n.t("dynamic:dynamicKey")).toBe("dynamic:dynamicKey")
565
+ expect(i18n.t('staticKey')).toBe('From static')
566
+ expect(i18n.t('dynamic:dynamicKey')).toBe('dynamic:dynamicKey')
567
567
 
568
- await i18n.loadNamespace("dynamic")
569
- expect(i18n.t("dynamic:dynamicKey")).toBe("From loader")
568
+ await i18n.loadNamespace('dynamic')
569
+ expect(i18n.t('dynamic:dynamicKey')).toBe('From loader')
570
570
  })
571
571
  })
572
572
 
573
573
  // ─── I18nProvider / useI18n context ──────────────────────────────────────────
574
574
 
575
- describe("I18nProvider / useI18n", () => {
576
- it("provides i18n instance to child components via useI18n", () => {
575
+ describe('I18nProvider / useI18n', () => {
576
+ it('provides i18n instance to child components via useI18n', () => {
577
577
  const i18n = createI18n({
578
- locale: "en",
579
- messages: { en: { hello: "Hello World" } },
578
+ locale: 'en',
579
+ messages: { en: { hello: 'Hello World' } },
580
580
  })
581
581
 
582
582
  let received: ReturnType<typeof useI18n> | undefined
583
- const el = document.createElement("div")
583
+ const el = document.createElement('div')
584
584
  document.body.appendChild(el)
585
585
  const Child = () => {
586
586
  received = useI18n()
@@ -594,19 +594,19 @@ describe("I18nProvider / useI18n", () => {
594
594
  )
595
595
 
596
596
  expect(received).toBe(i18n)
597
- expect(received!.t("hello")).toBe("Hello World")
597
+ expect(received!.t('hello')).toBe('Hello World')
598
598
  unmount()
599
599
  el.remove()
600
600
  })
601
601
 
602
- it("renders children passed as a function", () => {
602
+ it('renders children passed as a function', () => {
603
603
  const i18n = createI18n({
604
- locale: "en",
605
- messages: { en: { key: "Value" } },
604
+ locale: 'en',
605
+ messages: { en: { key: 'Value' } },
606
606
  })
607
607
 
608
608
  let received: ReturnType<typeof useI18n> | undefined
609
- const el = document.createElement("div")
609
+ const el = document.createElement('div')
610
610
  document.body.appendChild(el)
611
611
  const Child = () => {
612
612
  received = useI18n()
@@ -615,14 +615,14 @@ describe("I18nProvider / useI18n", () => {
615
615
  const unmount = mount(<I18nProvider instance={i18n}>{() => <Child />}</I18nProvider>, el)
616
616
 
617
617
  expect(received).toBeDefined()
618
- expect(received!.t("key")).toBe("Value")
618
+ expect(received!.t('key')).toBe('Value')
619
619
  unmount()
620
620
  el.remove()
621
621
  })
622
622
 
623
- it("useI18n throws when called outside I18nProvider", () => {
623
+ it('useI18n throws when called outside I18nProvider', () => {
624
624
  let error: Error | undefined
625
- const el = document.createElement("div")
625
+ const el = document.createElement('div')
626
626
  document.body.appendChild(el)
627
627
 
628
628
  const Child = () => {
@@ -636,19 +636,19 @@ describe("I18nProvider / useI18n", () => {
636
636
  const unmount = mount(<Child />, el)
637
637
 
638
638
  expect(error).toBeDefined()
639
- expect(error!.message).toContain("useI18n() must be used within an <I18nProvider>")
639
+ expect(error!.message).toContain('useI18n() must be used within an <I18nProvider>')
640
640
  unmount()
641
641
  el.remove()
642
642
  })
643
643
 
644
- it("renders direct VNode children (not a function)", () => {
644
+ it('renders direct VNode children (not a function)', () => {
645
645
  const i18n = createI18n({
646
- locale: "en",
647
- messages: { en: { test: "Test value" } },
646
+ locale: 'en',
647
+ messages: { en: { test: 'Test value' } },
648
648
  })
649
649
 
650
650
  let received: ReturnType<typeof useI18n> | undefined
651
- const el = document.createElement("div")
651
+ const el = document.createElement('div')
652
652
  document.body.appendChild(el)
653
653
  const Child = () => {
654
654
  received = useI18n()
@@ -662,7 +662,7 @@ describe("I18nProvider / useI18n", () => {
662
662
  )
663
663
 
664
664
  expect(received).toBeDefined()
665
- expect(received!.t("test")).toBe("Test value")
665
+ expect(received!.t('test')).toBe('Test value')
666
666
  unmount()
667
667
  el.remove()
668
668
  })
@@ -670,15 +670,15 @@ describe("I18nProvider / useI18n", () => {
670
670
 
671
671
  // ─── Pluralization — Intl.PluralRules unavailable fallback ──────────────────
672
672
 
673
- describe("resolvePluralCategory fallback when Intl is unavailable", () => {
674
- it("falls back to basic one/other when Intl is undefined", () => {
673
+ describe('resolvePluralCategory fallback when Intl is unavailable', () => {
674
+ it('falls back to basic one/other when Intl is undefined', () => {
675
675
  const originalIntl = globalThis.Intl
676
676
  // @ts-expect-error — temporarily remove Intl
677
677
  globalThis.Intl = undefined
678
678
 
679
- expect(resolvePluralCategory("en", 1)).toBe("one")
680
- expect(resolvePluralCategory("en", 0)).toBe("other")
681
- expect(resolvePluralCategory("en", 5)).toBe("other")
679
+ expect(resolvePluralCategory('en', 1)).toBe('one')
680
+ expect(resolvePluralCategory('en', 0)).toBe('other')
681
+ expect(resolvePluralCategory('en', 5)).toBe('other')
682
682
 
683
683
  globalThis.Intl = originalIntl
684
684
  })
@@ -686,93 +686,93 @@ describe("resolvePluralCategory fallback when Intl is unavailable", () => {
686
686
 
687
687
  // ─── parseRichText ──────────────────────────────────────────────────────────
688
688
 
689
- describe("parseRichText", () => {
690
- it("returns a single-element array for plain text", () => {
691
- expect(parseRichText("Hello world")).toEqual(["Hello world"])
689
+ describe('parseRichText', () => {
690
+ it('returns a single-element array for plain text', () => {
691
+ expect(parseRichText('Hello world')).toEqual(['Hello world'])
692
692
  })
693
693
 
694
- it("returns empty array for empty string", () => {
695
- expect(parseRichText("")).toEqual([])
694
+ it('returns empty array for empty string', () => {
695
+ expect(parseRichText('')).toEqual([])
696
696
  })
697
697
 
698
- it("parses a single tag", () => {
699
- expect(parseRichText("Hello <bold>world</bold>!")).toEqual([
700
- "Hello ",
701
- { tag: "bold", children: "world" },
702
- "!",
698
+ it('parses a single tag', () => {
699
+ expect(parseRichText('Hello <bold>world</bold>!')).toEqual([
700
+ 'Hello ',
701
+ { tag: 'bold', children: 'world' },
702
+ '!',
703
703
  ])
704
704
  })
705
705
 
706
- it("parses multiple tags", () => {
707
- expect(parseRichText("Read <terms>terms</terms> and <privacy>policy</privacy>")).toEqual([
708
- "Read ",
709
- { tag: "terms", children: "terms" },
710
- " and ",
711
- { tag: "privacy", children: "policy" },
706
+ it('parses multiple tags', () => {
707
+ expect(parseRichText('Read <terms>terms</terms> and <privacy>policy</privacy>')).toEqual([
708
+ 'Read ',
709
+ { tag: 'terms', children: 'terms' },
710
+ ' and ',
711
+ { tag: 'privacy', children: 'policy' },
712
712
  ])
713
713
  })
714
714
 
715
- it("handles tags at the start and end", () => {
716
- expect(parseRichText("<a>start</a> middle <b>end</b>")).toEqual([
717
- { tag: "a", children: "start" },
718
- " middle ",
719
- { tag: "b", children: "end" },
715
+ it('handles tags at the start and end', () => {
716
+ expect(parseRichText('<a>start</a> middle <b>end</b>')).toEqual([
717
+ { tag: 'a', children: 'start' },
718
+ ' middle ',
719
+ { tag: 'b', children: 'end' },
720
720
  ])
721
721
  })
722
722
 
723
- it("handles adjacent tags with no gap", () => {
724
- expect(parseRichText("<a>one</a><b>two</b>")).toEqual([
725
- { tag: "a", children: "one" },
726
- { tag: "b", children: "two" },
723
+ it('handles adjacent tags with no gap', () => {
724
+ expect(parseRichText('<a>one</a><b>two</b>')).toEqual([
725
+ { tag: 'a', children: 'one' },
726
+ { tag: 'b', children: 'two' },
727
727
  ])
728
728
  })
729
729
 
730
- it("leaves unmatched tags as plain text", () => {
731
- expect(parseRichText("Hello <open>no close")).toEqual(["Hello <open>no close"])
730
+ it('leaves unmatched tags as plain text', () => {
731
+ expect(parseRichText('Hello <open>no close')).toEqual(['Hello <open>no close'])
732
732
  })
733
733
  })
734
734
 
735
735
  // ─── Trans ──────────────────────────────────────────────────────────────────
736
736
 
737
- describe("Trans", () => {
738
- it("returns plain translated text when no components provided", () => {
739
- const t = (key: string) => (key === "hello" ? "Hello World" : key)
740
- const result = Trans({ t, i18nKey: "hello" })
741
- expect(result).toBe("Hello World")
737
+ describe('Trans', () => {
738
+ it('returns plain translated text when no components provided', () => {
739
+ const t = (key: string) => (key === 'hello' ? 'Hello World' : key)
740
+ const result = Trans({ t, i18nKey: 'hello' })
741
+ expect(result).toBe('Hello World')
742
742
  })
743
743
 
744
- it("returns plain translated text with values but no components", () => {
744
+ it('returns plain translated text with values but no components', () => {
745
745
  const i18n = createI18n({
746
- locale: "en",
747
- messages: { en: { greeting: "Hi {{name}}!" } },
746
+ locale: 'en',
747
+ messages: { en: { greeting: 'Hi {{name}}!' } },
748
748
  })
749
749
  const result = Trans({
750
750
  t: i18n.t,
751
- i18nKey: "greeting",
752
- values: { name: "Alice" },
751
+ i18nKey: 'greeting',
752
+ values: { name: 'Alice' },
753
753
  })
754
- expect(result).toBe("Hi Alice!")
754
+ expect(result).toBe('Hi Alice!')
755
755
  })
756
756
 
757
- it("returns plain string when components map is provided but text has no tags", () => {
758
- const t = () => "No tags here"
757
+ it('returns plain string when components map is provided but text has no tags', () => {
758
+ const t = () => 'No tags here'
759
759
  const result = Trans({
760
760
  t,
761
- i18nKey: "plain",
762
- components: { bold: (ch: string) => ({ type: "strong", children: ch }) },
761
+ i18nKey: 'plain',
762
+ components: { bold: (ch: string) => ({ type: 'strong', children: ch }) },
763
763
  })
764
- expect(result).toBe("No tags here")
764
+ expect(result).toBe('No tags here')
765
765
  })
766
766
 
767
- it("invokes component functions for matched tags", () => {
768
- const t = () => "Click <link>here</link> please"
767
+ it('invokes component functions for matched tags', () => {
768
+ const t = () => 'Click <link>here</link> please'
769
769
  const result = Trans({
770
770
  t,
771
- i18nKey: "action",
771
+ i18nKey: 'action',
772
772
  components: {
773
773
  link: (children: string) => ({
774
- type: "a",
775
- props: { href: "/go" },
774
+ type: 'a',
775
+ props: { href: '/go' },
776
776
  children,
777
777
  }),
778
778
  },
@@ -780,87 +780,87 @@ describe("Trans", () => {
780
780
 
781
781
  // Result is a Fragment VNode; check its children
782
782
  expect(result).toBeTruthy()
783
- expect(typeof result).toBe("object")
783
+ expect(typeof result).toBe('object')
784
784
  // The Fragment wraps: ["Click ", { type: 'a', ... }, " please"]
785
785
  const vnode = result as any
786
786
  expect(vnode.children.length).toBe(3)
787
- expect(vnode.children[0]).toBe("Click ")
787
+ expect(vnode.children[0]).toBe('Click ')
788
788
  expect(vnode.children[1]).toEqual({
789
- type: "a",
790
- props: { href: "/go" },
791
- children: "here",
789
+ type: 'a',
790
+ props: { href: '/go' },
791
+ children: 'here',
792
792
  })
793
- expect(vnode.children[2]).toBe(" please")
793
+ expect(vnode.children[2]).toBe(' please')
794
794
  })
795
795
 
796
- it("renders unmatched tags as plain text children (no raw HTML)", () => {
797
- const t = () => "Hello <unknown>world</unknown>"
796
+ it('renders unmatched tags as plain text children (no raw HTML)', () => {
797
+ const t = () => 'Hello <unknown>world</unknown>'
798
798
  const result = Trans({
799
799
  t,
800
- i18nKey: "test",
800
+ i18nKey: 'test',
801
801
  components: {}, // No matching component
802
802
  })
803
803
 
804
804
  const vnode = result as any
805
805
  expect(vnode.children.length).toBe(2)
806
- expect(vnode.children[0]).toBe("Hello ")
806
+ expect(vnode.children[0]).toBe('Hello ')
807
807
  // Unmatched tags render children as plain text, stripping markup for safety
808
- expect(vnode.children[1]).toBe("world")
808
+ expect(vnode.children[1]).toBe('world')
809
809
  })
810
810
 
811
- it("works with values and components together", () => {
811
+ it('works with values and components together', () => {
812
812
  const i18n = createI18n({
813
- locale: "en",
813
+ locale: 'en',
814
814
  messages: {
815
- en: { items: "You have <bold>{{count}}</bold> items" },
815
+ en: { items: 'You have <bold>{{count}}</bold> items' },
816
816
  },
817
817
  })
818
818
 
819
819
  const result = Trans({
820
820
  t: i18n.t,
821
- i18nKey: "items",
821
+ i18nKey: 'items',
822
822
  values: { count: 42 },
823
823
  components: {
824
- bold: (children: string) => ({ type: "strong", children }),
824
+ bold: (children: string) => ({ type: 'strong', children }),
825
825
  },
826
826
  })
827
827
 
828
828
  const vnode = result as any
829
829
  expect(vnode.children.length).toBe(3)
830
- expect(vnode.children[0]).toBe("You have ")
831
- expect(vnode.children[1]).toEqual({ type: "strong", children: "42" })
832
- expect(vnode.children[2]).toBe(" items")
830
+ expect(vnode.children[0]).toBe('You have ')
831
+ expect(vnode.children[1]).toEqual({ type: 'strong', children: '42' })
832
+ expect(vnode.children[2]).toBe(' items')
833
833
  })
834
834
  })
835
835
 
836
836
  // ─── addMessages flat keys ──────────────────────────────────────────────────
837
837
 
838
- describe("addMessages flat dot-notation keys", () => {
839
- it("flat key makes t() resolve via dot notation", () => {
840
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
841
- i18n.addMessages("en", { "section.title": "Report" })
842
- expect(i18n.t("section.title")).toBe("Report")
838
+ describe('addMessages flat dot-notation keys', () => {
839
+ it('flat key makes t() resolve via dot notation', () => {
840
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
841
+ i18n.addMessages('en', { 'section.title': 'Report' })
842
+ expect(i18n.t('section.title')).toBe('Report')
843
843
  })
844
844
 
845
- it("nested keys still work", () => {
846
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
847
- i18n.addMessages("en", { section: { title: "Report" } })
848
- expect(i18n.t("section.title")).toBe("Report")
845
+ it('nested keys still work', () => {
846
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
847
+ i18n.addMessages('en', { section: { title: 'Report' } })
848
+ expect(i18n.t('section.title')).toBe('Report')
849
849
  })
850
850
 
851
- it("mixed flat and nested keys", () => {
852
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
853
- i18n.addMessages("en", { "a.b": "flat", c: { d: "nested" } })
854
- expect(i18n.t("a.b")).toBe("flat")
855
- expect(i18n.t("c.d")).toBe("nested")
851
+ it('mixed flat and nested keys', () => {
852
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
853
+ i18n.addMessages('en', { 'a.b': 'flat', c: { d: 'nested' } })
854
+ expect(i18n.t('a.b')).toBe('flat')
855
+ expect(i18n.t('c.d')).toBe('nested')
856
856
  })
857
857
  })
858
858
 
859
859
  // ─── core subpath ───────────────────────────────────────────────────────────
860
860
 
861
- describe("i18n core subpath", () => {
862
- it("exports createI18n and interpolate without @pyreon/core dependency", async () => {
863
- const mod = await import("../core")
861
+ describe('i18n core subpath', () => {
862
+ it('exports createI18n and interpolate without @pyreon/core dependency', async () => {
863
+ const mod = await import('../core')
864
864
  expect(mod.createI18n).toBeDefined()
865
865
  expect(mod.interpolate).toBeDefined()
866
866
  })