@pyreon/i18n 0.11.5 → 0.11.7

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,224 +1,224 @@
1
- import { effect } from "@pyreon/reactivity"
2
- import { createI18n } from "../create-i18n"
3
- import { parseRichText, Trans } from "../trans"
4
- import type { TranslationDictionary } from "../types"
1
+ import { effect } from '@pyreon/reactivity'
2
+ import { createI18n } from '../create-i18n'
3
+ import { parseRichText, Trans } from '../trans'
4
+ import type { TranslationDictionary } from '../types'
5
5
 
6
6
  // ─── nestFlatKeys via addMessages ────────────────────────────────────────────
7
7
 
8
- describe("addMessages with nestFlatKeys", () => {
9
- it("converts deeply nested flat keys", () => {
10
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
11
- i18n.addMessages("en", { "a.b.c.d": "deep" })
12
- expect(i18n.t("a.b.c.d")).toBe("deep")
8
+ describe('addMessages with nestFlatKeys', () => {
9
+ it('converts deeply nested flat keys', () => {
10
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
11
+ i18n.addMessages('en', { 'a.b.c.d': 'deep' })
12
+ expect(i18n.t('a.b.c.d')).toBe('deep')
13
13
  })
14
14
 
15
- it("multiple flat keys build shared parent objects", () => {
16
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
17
- i18n.addMessages("en", {
18
- "form.email.label": "Email",
19
- "form.email.placeholder": "Enter email",
20
- "form.password.label": "Password",
15
+ it('multiple flat keys build shared parent objects', () => {
16
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
17
+ i18n.addMessages('en', {
18
+ 'form.email.label': 'Email',
19
+ 'form.email.placeholder': 'Enter email',
20
+ 'form.password.label': 'Password',
21
21
  })
22
- expect(i18n.t("form.email.label")).toBe("Email")
23
- expect(i18n.t("form.email.placeholder")).toBe("Enter email")
24
- expect(i18n.t("form.password.label")).toBe("Password")
22
+ expect(i18n.t('form.email.label')).toBe('Email')
23
+ expect(i18n.t('form.email.placeholder')).toBe('Enter email')
24
+ expect(i18n.t('form.password.label')).toBe('Password')
25
25
  })
26
26
 
27
- it("flat keys coexist with nested keys in same addMessages call", () => {
28
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
29
- i18n.addMessages("en", {
30
- "flat.key": "Flat Value",
31
- nested: { key: "Nested Value" },
27
+ it('flat keys coexist with nested keys in same addMessages call', () => {
28
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
29
+ i18n.addMessages('en', {
30
+ 'flat.key': 'Flat Value',
31
+ nested: { key: 'Nested Value' },
32
32
  })
33
- expect(i18n.t("flat.key")).toBe("Flat Value")
34
- expect(i18n.t("nested.key")).toBe("Nested Value")
33
+ expect(i18n.t('flat.key')).toBe('Flat Value')
34
+ expect(i18n.t('nested.key')).toBe('Nested Value')
35
35
  })
36
36
 
37
- it("flat key overwrites previous nested value at same path", () => {
38
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
39
- i18n.addMessages("en", { section: { title: "Old" } })
40
- i18n.addMessages("en", { "section.title": "New" })
41
- expect(i18n.t("section.title")).toBe("New")
37
+ it('flat key overwrites previous nested value at same path', () => {
38
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
39
+ i18n.addMessages('en', { section: { title: 'Old' } })
40
+ i18n.addMessages('en', { 'section.title': 'New' })
41
+ expect(i18n.t('section.title')).toBe('New')
42
42
  })
43
43
 
44
- it("flat keys without dots are passed through unchanged", () => {
45
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
46
- i18n.addMessages("en", { simple: "Just a string" })
47
- expect(i18n.t("simple")).toBe("Just a string")
44
+ it('flat keys without dots are passed through unchanged', () => {
45
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
46
+ i18n.addMessages('en', { simple: 'Just a string' })
47
+ expect(i18n.t('simple')).toBe('Just a string')
48
48
  })
49
49
 
50
- it("flat key with non-string value (nested object at dotted key) is treated as nested", () => {
51
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
50
+ it('flat key with non-string value (nested object at dotted key) is treated as nested', () => {
51
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
52
52
  // If a dotted key has an object value, nestFlatKeys treats it as a regular key
53
- i18n.addMessages("en", { "a.b": { c: "Nested under flat" } } as any)
53
+ i18n.addMessages('en', { 'a.b': { c: 'Nested under flat' } } as any)
54
54
  // Since "a.b" has a non-string value, it's kept as-is — lookupKey("a.b") won't resolve
55
55
  // But "a" → "b" → { c: "Nested under flat" } won't be created either
56
56
  // The object value at key "a.b" isn't a string, so nestFlatKeys passes it through
57
- expect(i18n.t("a.b")).toBe("a.b") // Key not found as string
57
+ expect(i18n.t('a.b')).toBe('a.b') // Key not found as string
58
58
  })
59
59
 
60
- it("flat keys in namespaced addMessages", () => {
61
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
60
+ it('flat keys in namespaced addMessages', () => {
61
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
62
62
  i18n.addMessages(
63
- "en",
64
- { "errors.auth": "Auth failed", "errors.network": "Network error" },
65
- "admin",
63
+ 'en',
64
+ { 'errors.auth': 'Auth failed', 'errors.network': 'Network error' },
65
+ 'admin',
66
66
  )
67
- expect(i18n.t("admin:errors.auth")).toBe("Auth failed")
68
- expect(i18n.t("admin:errors.network")).toBe("Network error")
67
+ expect(i18n.t('admin:errors.auth')).toBe('Auth failed')
68
+ expect(i18n.t('admin:errors.network')).toBe('Network error')
69
69
  })
70
70
  })
71
71
 
72
72
  // ─── core subpath imports ────────────────────────────────────────────────────
73
73
 
74
- describe("i18n core subpath", () => {
75
- it("exports createI18n and interpolate without @pyreon/core dependency", async () => {
76
- const mod = await import("../core")
74
+ describe('i18n core subpath', () => {
75
+ it('exports createI18n and interpolate without @pyreon/core dependency', async () => {
76
+ const mod = await import('../core')
77
77
  expect(mod.createI18n).toBeDefined()
78
78
  expect(mod.interpolate).toBeDefined()
79
79
  })
80
80
 
81
- it("exports resolvePluralCategory", async () => {
82
- const mod = await import("../core")
81
+ it('exports resolvePluralCategory', async () => {
82
+ const mod = await import('../core')
83
83
  expect(mod.resolvePluralCategory).toBeDefined()
84
- expect(mod.resolvePluralCategory("en", 1)).toBe("one")
85
- expect(mod.resolvePluralCategory("en", 5)).toBe("other")
84
+ expect(mod.resolvePluralCategory('en', 1)).toBe('one')
85
+ expect(mod.resolvePluralCategory('en', 5)).toBe('other')
86
86
  })
87
87
 
88
- it("core createI18n is functional without DOM", async () => {
89
- const { createI18n: coreCreateI18n } = await import("../core")
88
+ it('core createI18n is functional without DOM', async () => {
89
+ const { createI18n: coreCreateI18n } = await import('../core')
90
90
  const i18n = coreCreateI18n({
91
- locale: "en",
92
- messages: { en: { greeting: "Hello {{name}}!" } },
91
+ locale: 'en',
92
+ messages: { en: { greeting: 'Hello {{name}}!' } },
93
93
  })
94
- expect(i18n.t("greeting", { name: "Server" })).toBe("Hello Server!")
94
+ expect(i18n.t('greeting', { name: 'Server' })).toBe('Hello Server!')
95
95
  })
96
96
  })
97
97
 
98
98
  // ─── Pluralization with _zero suffix ─────────────────────────────────────────
99
99
 
100
- describe("pluralization with _zero suffix", () => {
100
+ describe('pluralization with _zero suffix', () => {
101
101
  it("uses _zero form when count is 0 and rule returns 'zero'", () => {
102
102
  const i18n = createI18n({
103
- locale: "custom",
103
+ locale: 'custom',
104
104
  pluralRules: {
105
- custom: (count: number) => (count === 0 ? "zero" : count === 1 ? "one" : "other"),
105
+ custom: (count: number) => (count === 0 ? 'zero' : count === 1 ? 'one' : 'other'),
106
106
  },
107
107
  messages: {
108
108
  custom: {
109
- items_zero: "No items",
110
- items_one: "{{count}} item",
111
- items_other: "{{count}} items",
109
+ items_zero: 'No items',
110
+ items_one: '{{count}} item',
111
+ items_other: '{{count}} items',
112
112
  },
113
113
  },
114
114
  })
115
115
 
116
- expect(i18n.t("items", { count: 0 })).toBe("No items")
117
- expect(i18n.t("items", { count: 1 })).toBe("1 item")
118
- expect(i18n.t("items", { count: 5 })).toBe("5 items")
116
+ expect(i18n.t('items', { count: 0 })).toBe('No items')
117
+ expect(i18n.t('items', { count: 1 })).toBe('1 item')
118
+ expect(i18n.t('items', { count: 5 })).toBe('5 items')
119
119
  })
120
120
 
121
- it("falls back to _other when _zero is missing and count is 0 in English", () => {
121
+ it('falls back to _other when _zero is missing and count is 0 in English', () => {
122
122
  const i18n = createI18n({
123
- locale: "en",
123
+ locale: 'en',
124
124
  messages: {
125
125
  en: {
126
- items_one: "{{count}} item",
127
- items_other: "{{count}} items",
126
+ items_one: '{{count}} item',
127
+ items_other: '{{count}} items',
128
128
  },
129
129
  },
130
130
  })
131
131
 
132
132
  // English Intl.PluralRules returns "other" for 0, so _other is used
133
- expect(i18n.t("items", { count: 0 })).toBe("0 items")
133
+ expect(i18n.t('items', { count: 0 })).toBe('0 items')
134
134
  })
135
135
 
136
- it("pluralization with all three suffixes: _zero, _one, _other", () => {
136
+ it('pluralization with all three suffixes: _zero, _one, _other', () => {
137
137
  const i18n = createI18n({
138
- locale: "pl",
138
+ locale: 'pl',
139
139
  pluralRules: {
140
140
  pl: (count: number) => {
141
- if (count === 0) return "zero"
142
- if (count === 1) return "one"
143
- return "other"
141
+ if (count === 0) return 'zero'
142
+ if (count === 1) return 'one'
143
+ return 'other'
144
144
  },
145
145
  },
146
146
  messages: {
147
147
  pl: {
148
- messages_zero: "Brak wiadomości",
149
- messages_one: "{{count}} wiadomość",
150
- messages_other: "{{count}} wiadomości",
148
+ messages_zero: 'Brak wiadomości',
149
+ messages_one: '{{count}} wiadomość',
150
+ messages_other: '{{count}} wiadomości',
151
151
  },
152
152
  },
153
153
  })
154
154
 
155
- expect(i18n.t("messages", { count: 0 })).toBe("Brak wiadomości")
156
- expect(i18n.t("messages", { count: 1 })).toBe("1 wiadomość")
157
- expect(i18n.t("messages", { count: 7 })).toBe("7 wiadomości")
155
+ expect(i18n.t('messages', { count: 0 })).toBe('Brak wiadomości')
156
+ expect(i18n.t('messages', { count: 1 })).toBe('1 wiadomość')
157
+ expect(i18n.t('messages', { count: 7 })).toBe('7 wiadomości')
158
158
  })
159
159
 
160
- it("count as string number still works for pluralization", () => {
160
+ it('count as string number still works for pluralization', () => {
161
161
  const i18n = createI18n({
162
- locale: "en",
162
+ locale: 'en',
163
163
  messages: {
164
164
  en: {
165
- items_one: "{{count}} item",
166
- items_other: "{{count}} items",
165
+ items_one: '{{count}} item',
166
+ items_other: '{{count}} items',
167
167
  },
168
168
  },
169
169
  })
170
170
 
171
171
  // count is a string but Number("1") === 1
172
- expect(i18n.t("items", { count: "1" })).toBe("1 item")
173
- expect(i18n.t("items", { count: "5" })).toBe("5 items")
172
+ expect(i18n.t('items', { count: '1' })).toBe('1 item')
173
+ expect(i18n.t('items', { count: '5' })).toBe('5 items')
174
174
  })
175
175
  })
176
176
 
177
177
  // ─── Namespace lazy loading — additional scenarios ───────────────────────────
178
178
 
179
- describe("namespace lazy loading", () => {
180
- it("loads namespace for current locale by default", async () => {
179
+ describe('namespace lazy loading', () => {
180
+ it('loads namespace for current locale by default', async () => {
181
181
  const loaderCalls: string[] = []
182
182
 
183
183
  const i18n = createI18n({
184
- locale: "fr",
184
+ locale: 'fr',
185
185
  loader: async (locale, namespace) => {
186
186
  loaderCalls.push(`${locale}:${namespace}`)
187
187
  return { title: `${locale} ${namespace} title` }
188
188
  },
189
189
  })
190
190
 
191
- await i18n.loadNamespace("dashboard")
192
- expect(loaderCalls).toEqual(["fr:dashboard"])
193
- expect(i18n.t("dashboard:title")).toBe("fr dashboard title")
191
+ await i18n.loadNamespace('dashboard')
192
+ expect(loaderCalls).toEqual(['fr:dashboard'])
193
+ expect(i18n.t('dashboard:title')).toBe('fr dashboard title')
194
194
  })
195
195
 
196
- it("loading namespace then switching locale does not lose previous locale data", async () => {
196
+ it('loading namespace then switching locale does not lose previous locale data', async () => {
197
197
  const translations: Record<string, Record<string, TranslationDictionary>> = {
198
- en: { auth: { login: "Log in" } },
199
- fr: { auth: { login: "Connexion" } },
198
+ en: { auth: { login: 'Log in' } },
199
+ fr: { auth: { login: 'Connexion' } },
200
200
  }
201
201
 
202
202
  const i18n = createI18n({
203
- locale: "en",
203
+ locale: 'en',
204
204
  loader: async (locale, ns) => translations[locale]?.[ns],
205
205
  })
206
206
 
207
- await i18n.loadNamespace("auth")
208
- expect(i18n.t("auth:login")).toBe("Log in")
207
+ await i18n.loadNamespace('auth')
208
+ expect(i18n.t('auth:login')).toBe('Log in')
209
209
 
210
- i18n.locale.set("fr")
211
- await i18n.loadNamespace("auth")
212
- expect(i18n.t("auth:login")).toBe("Connexion")
210
+ i18n.locale.set('fr')
211
+ await i18n.loadNamespace('auth')
212
+ expect(i18n.t('auth:login')).toBe('Connexion')
213
213
 
214
214
  // Switch back — en data should still be there
215
- i18n.locale.set("en")
216
- expect(i18n.t("auth:login")).toBe("Log in")
215
+ i18n.locale.set('en')
216
+ expect(i18n.t('auth:login')).toBe('Log in')
217
217
  })
218
218
 
219
- it("loaded namespaces update reactively", async () => {
219
+ it('loaded namespaces update reactively', async () => {
220
220
  const i18n = createI18n({
221
- locale: "en",
221
+ locale: 'en',
222
222
  loader: async (_locale, ns) => ({ key: `${ns}-value` }),
223
223
  })
224
224
 
@@ -229,10 +229,10 @@ describe("namespace lazy loading", () => {
229
229
 
230
230
  expect(snapshots).toEqual([0])
231
231
 
232
- await i18n.loadNamespace("auth")
232
+ await i18n.loadNamespace('auth')
233
233
  expect(snapshots).toEqual([0, 1])
234
234
 
235
- await i18n.loadNamespace("admin")
235
+ await i18n.loadNamespace('admin')
236
236
  expect(snapshots).toEqual([0, 1, 2])
237
237
 
238
238
  cleanup.dispose()
@@ -241,150 +241,150 @@ describe("namespace lazy loading", () => {
241
241
 
242
242
  // ─── Locale switching — reactivity ───────────────────────────────────────────
243
243
 
244
- describe("locale switching reactivity", () => {
245
- it("locale.set triggers reactive t() updates across multiple calls", () => {
244
+ describe('locale switching reactivity', () => {
245
+ it('locale.set triggers reactive t() updates across multiple calls', () => {
246
246
  const i18n = createI18n({
247
- locale: "en",
247
+ locale: 'en',
248
248
  messages: {
249
- en: { greeting: "Hello", farewell: "Goodbye" },
250
- es: { greeting: "Hola", farewell: "Adiós" },
251
- ja: { greeting: "こんにちは", farewell: "さようなら" },
249
+ en: { greeting: 'Hello', farewell: 'Goodbye' },
250
+ es: { greeting: 'Hola', farewell: 'Adiós' },
251
+ ja: { greeting: 'こんにちは', farewell: 'さようなら' },
252
252
  },
253
253
  })
254
254
 
255
255
  const results: string[] = []
256
256
  const cleanup = effect(() => {
257
- results.push(i18n.t("greeting"))
257
+ results.push(i18n.t('greeting'))
258
258
  })
259
259
 
260
- expect(results).toEqual(["Hello"])
260
+ expect(results).toEqual(['Hello'])
261
261
 
262
- i18n.locale.set("es")
263
- expect(results).toEqual(["Hello", "Hola"])
262
+ i18n.locale.set('es')
263
+ expect(results).toEqual(['Hello', 'Hola'])
264
264
 
265
- i18n.locale.set("ja")
266
- expect(results).toEqual(["Hello", "Hola", "こんにちは"])
265
+ i18n.locale.set('ja')
266
+ expect(results).toEqual(['Hello', 'Hola', 'こんにちは'])
267
267
 
268
268
  cleanup.dispose()
269
269
  })
270
270
 
271
- it("locale() returns current locale reactively", () => {
272
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
271
+ it('locale() returns current locale reactively', () => {
272
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
273
273
 
274
274
  const locales: string[] = []
275
275
  const cleanup = effect(() => {
276
276
  locales.push(i18n.locale())
277
277
  })
278
278
 
279
- expect(locales).toEqual(["en"])
279
+ expect(locales).toEqual(['en'])
280
280
 
281
- i18n.locale.set("fr")
282
- expect(locales).toEqual(["en", "fr"])
281
+ i18n.locale.set('fr')
282
+ expect(locales).toEqual(['en', 'fr'])
283
283
 
284
- i18n.locale.set("de")
285
- expect(locales).toEqual(["en", "fr", "de"])
284
+ i18n.locale.set('de')
285
+ expect(locales).toEqual(['en', 'fr', 'de'])
286
286
 
287
287
  cleanup.dispose()
288
288
  })
289
289
 
290
- it("switching to locale with missing key falls back to fallbackLocale", () => {
290
+ it('switching to locale with missing key falls back to fallbackLocale', () => {
291
291
  const i18n = createI18n({
292
- locale: "en",
293
- fallbackLocale: "en",
292
+ locale: 'en',
293
+ fallbackLocale: 'en',
294
294
  messages: {
295
- en: { common: "English common", onlyEn: "Only in English" },
296
- fr: { common: "French common" },
295
+ en: { common: 'English common', onlyEn: 'Only in English' },
296
+ fr: { common: 'French common' },
297
297
  },
298
298
  })
299
299
 
300
- i18n.locale.set("fr")
301
- expect(i18n.t("common")).toBe("French common")
302
- expect(i18n.t("onlyEn")).toBe("Only in English") // falls back to en
300
+ i18n.locale.set('fr')
301
+ expect(i18n.t('common')).toBe('French common')
302
+ expect(i18n.t('onlyEn')).toBe('Only in English') // falls back to en
303
303
  })
304
304
  })
305
305
 
306
306
  // ─── parseRichText — additional edge cases ───────────────────────────────────
307
307
 
308
- describe("parseRichText — additional", () => {
309
- it("handles self-closing-like tags (treated as unmatched)", () => {
308
+ describe('parseRichText — additional', () => {
309
+ it('handles self-closing-like tags (treated as unmatched)', () => {
310
310
  // parseRichText only handles <tag>content</tag> — <br/> etc. are left as-is
311
- expect(parseRichText("Line1<br/>Line2")).toEqual(["Line1<br/>Line2"])
311
+ expect(parseRichText('Line1<br/>Line2')).toEqual(['Line1<br/>Line2'])
312
312
  })
313
313
 
314
- it("nested tags match inner tag only (regex uses [^<]* for content)", () => {
314
+ it('nested tags match inner tag only (regex uses [^<]* for content)', () => {
315
315
  // The regex [^<]* means content between tags cannot contain <, so the outer
316
316
  // tag is not matched and only the inner <em> tag is parsed
317
- const result = parseRichText("<bold>some <em>nested</em> text</bold>")
318
- expect(result).toEqual(["<bold>some ", { tag: "em", children: "nested" }, " text</bold>"])
317
+ const result = parseRichText('<bold>some <em>nested</em> text</bold>')
318
+ expect(result).toEqual(['<bold>some ', { tag: 'em', children: 'nested' }, ' text</bold>'])
319
319
  })
320
320
 
321
- it("tag names with only word characters are matched (\\w+)", () => {
321
+ it('tag names with only word characters are matched (\\w+)', () => {
322
322
  // \\w matches [a-zA-Z0-9_], so underscores and digits work
323
- const result = parseRichText("Click <cta_1>here</cta_1>!")
324
- expect(result).toEqual(["Click ", { tag: "cta_1", children: "here" }, "!"])
323
+ const result = parseRichText('Click <cta_1>here</cta_1>!')
324
+ expect(result).toEqual(['Click ', { tag: 'cta_1', children: 'here' }, '!'])
325
325
  })
326
326
 
327
- it("tag names with hyphens are not matched (left as plain text)", () => {
327
+ it('tag names with hyphens are not matched (left as plain text)', () => {
328
328
  // Hyphens are not word characters, so <cta-1> is not a valid tag
329
- const result = parseRichText("Click <cta-1>here</cta-1>!")
330
- expect(result).toEqual(["Click <cta-1>here</cta-1>!"])
329
+ const result = parseRichText('Click <cta-1>here</cta-1>!')
330
+ expect(result).toEqual(['Click <cta-1>here</cta-1>!'])
331
331
  })
332
332
  })
333
333
 
334
334
  // ─── Trans component — additional scenarios ──────────────────────────────────
335
335
 
336
- describe("Trans — additional", () => {
337
- it("handles translation with multiple component tags", () => {
336
+ describe('Trans — additional', () => {
337
+ it('handles translation with multiple component tags', () => {
338
338
  const i18n = createI18n({
339
- locale: "en",
339
+ locale: 'en',
340
340
  messages: {
341
- en: { tos: "Read <terms>terms</terms> and <privacy>privacy</privacy>" },
341
+ en: { tos: 'Read <terms>terms</terms> and <privacy>privacy</privacy>' },
342
342
  },
343
343
  })
344
344
 
345
345
  const result = Trans({
346
346
  t: i18n.t,
347
- i18nKey: "tos",
347
+ i18nKey: 'tos',
348
348
  components: {
349
- terms: (children: string) => ({ type: "a", props: { href: "/terms" }, children }),
350
- privacy: (children: string) => ({ type: "a", props: { href: "/privacy" }, children }),
349
+ terms: (children: string) => ({ type: 'a', props: { href: '/terms' }, children }),
350
+ privacy: (children: string) => ({ type: 'a', props: { href: '/privacy' }, children }),
351
351
  },
352
352
  })
353
353
 
354
354
  const vnode = result as any
355
355
  expect(vnode.children.length).toBe(4) // "Read ", terms link, " and ", privacy link
356
- expect(vnode.children[0]).toBe("Read ")
357
- expect(vnode.children[1]).toEqual({ type: "a", props: { href: "/terms" }, children: "terms" })
358
- expect(vnode.children[2]).toBe(" and ")
356
+ expect(vnode.children[0]).toBe('Read ')
357
+ expect(vnode.children[1]).toEqual({ type: 'a', props: { href: '/terms' }, children: 'terms' })
358
+ expect(vnode.children[2]).toBe(' and ')
359
359
  expect(vnode.children[3]).toEqual({
360
- type: "a",
361
- props: { href: "/privacy" },
362
- children: "privacy",
360
+ type: 'a',
361
+ props: { href: '/privacy' },
362
+ children: 'privacy',
363
363
  })
364
364
  })
365
365
 
366
- it("returns plain text when translation has no tags and no components", () => {
367
- const t = (key: string) => (key === "plain" ? "Just plain text" : key)
368
- const result = Trans({ t, i18nKey: "plain" })
369
- expect(result).toBe("Just plain text")
366
+ it('returns plain text when translation has no tags and no components', () => {
367
+ const t = (key: string) => (key === 'plain' ? 'Just plain text' : key)
368
+ const result = Trans({ t, i18nKey: 'plain' })
369
+ expect(result).toBe('Just plain text')
370
370
  })
371
371
  })
372
372
 
373
373
  // ─── Deep merge edge cases ───────────────────────────────────────────────────
374
374
 
375
- describe("addMessages deep merge edge cases", () => {
376
- it("prototype pollution keys are rejected", () => {
377
- const i18n = createI18n({ locale: "en", messages: { en: { safe: "yes" } } })
375
+ describe('addMessages deep merge edge cases', () => {
376
+ it('prototype pollution keys are rejected', () => {
377
+ const i18n = createI18n({ locale: 'en', messages: { en: { safe: 'yes' } } })
378
378
  // deepMerge skips __proto__, constructor, prototype
379
- i18n.addMessages("en", { __proto__: { polluted: "yes" } } as any)
380
- expect(i18n.t("safe")).toBe("yes")
379
+ i18n.addMessages('en', { __proto__: { polluted: 'yes' } } as any)
380
+ expect(i18n.t('safe')).toBe('yes')
381
381
  // The polluted key should not be accessible
382
382
  expect(({} as any).polluted).toBeUndefined()
383
383
  })
384
384
 
385
- it("addMessages with undefined values are skipped in nestFlatKeys", () => {
386
- const i18n = createI18n({ locale: "en", messages: { en: {} } })
387
- i18n.addMessages("en", { key: undefined as any, valid: "yes" })
388
- expect(i18n.t("valid")).toBe("yes")
385
+ it('addMessages with undefined values are skipped in nestFlatKeys', () => {
386
+ const i18n = createI18n({ locale: 'en', messages: { en: {} } })
387
+ i18n.addMessages('en', { key: undefined as any, valid: 'yes' })
388
+ expect(i18n.t('valid')).toBe('yes')
389
389
  })
390
390
  })