@npm-questionpro/wick-ui-i18n 0.8.0 → 0.10.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.
@@ -1,13 +1,29 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, vi } from "vitest";
2
2
  import wickuiI18nPlugin from "./index.js";
3
3
 
4
- // Helper to simulate Vite running the transform hook
5
4
  function transform(code, options = {}) {
6
5
  const plugin = wickuiI18nPlugin(options);
7
6
  const result = plugin.transform(code, "TestFile.jsx");
8
7
  return result ? result.code : code;
9
8
  }
10
9
 
10
+ /**
11
+ * Run the full plugin lifecycle on `code` and return the emitted dictionary.
12
+ * Needed for wt() tests since those record keys without transforming code.
13
+ */
14
+ function getDictionary(code, options = {}) {
15
+ const plugin = wickuiI18nPlugin(options);
16
+ plugin.buildStart();
17
+ plugin.transform(code, "TestFile.jsx");
18
+ let dict = {};
19
+ plugin.generateBundle.call({
20
+ emitFile: ({ source }) => {
21
+ dict = JSON.parse(source);
22
+ },
23
+ });
24
+ return dict;
25
+ }
26
+
11
27
  describe("Wick UI i18n Vite Plugin", () => {
12
28
  it("1. Translates basic Wu* components and injects import", () => {
13
29
  const code = `<WuButton>Submit</WuButton>`;
@@ -50,6 +66,23 @@ describe("Wick UI i18n Vite Plugin", () => {
50
66
  );
51
67
  });
52
68
 
69
+ it("6b. data-i18n-key propagates through nested elements", () => {
70
+ const code = `<WuButton data-i18n-key="btn_login"><span>Log In</span></WuButton>`;
71
+ const result = transform(code);
72
+ expect(result).toContain(
73
+ `<WuTranslate __i18nKey="btn_login"></WuTranslate>`,
74
+ );
75
+ expect(result).not.toContain(`__i18nKey="Log In"`);
76
+ });
77
+
78
+ it("6c. data-i18n-key on ternary parent is ignored — each branch uses its own text as key", () => {
79
+ const code = `<WuButton data-i18n-key="btn">{flag ? "Yes" : "No"}</WuButton>`;
80
+ const result = transform(code);
81
+ expect(result).toContain(`<WuTranslate __i18nKey="Yes"></WuTranslate>`);
82
+ expect(result).toContain(`<WuTranslate __i18nKey="No"></WuTranslate>`);
83
+ expect(result).not.toContain(`__i18nKey="btn"`);
84
+ });
85
+
53
86
  it("7. Translates JSX Expression Containers (Strings in braces)", () => {
54
87
  const code = `<WuButton>{"Hello World"}</WuButton>`;
55
88
  const result = transform(code);
@@ -58,14 +91,13 @@ describe("Wick UI i18n Vite Plugin", () => {
58
91
  );
59
92
  });
60
93
 
61
- // it("8. Supports custom components via options", () => {
62
- // const code = `<CustomCard>Welcome</CustomCard>`;
63
- // const result = transform(code, { components: ["CustomCard"] });
64
- // expect(result).toContain(
65
- // `<WuTranslate __i18nKey="Welcome">Welcome</WuTranslate>`,
66
- // );
67
- // });
68
- //
94
+ it("8. Does not translate JSX attribute string values", () => {
95
+ const code = `<WuButton variant={'secondary'}>Submit</WuButton>`;
96
+ const result = transform(code);
97
+ expect(result).not.toContain(`__i18nKey="secondary"`);
98
+ expect(result).toContain(`<WuTranslate __i18nKey="Submit"></WuTranslate>`);
99
+ });
100
+
69
101
  it("9. Supports data-i18n-wrapper on non-target tags", () => {
70
102
  const code = `<span data-i18n-wrapper>Wrapped Text</span>`;
71
103
  const triggerCode = `import { WuPlaceholder } from 'lib';\n` + code;
@@ -74,4 +106,490 @@ describe("Wick UI i18n Vite Plugin", () => {
74
106
  `<WuTranslate __i18nKey="Wrapped Text"></WuTranslate>`,
75
107
  );
76
108
  });
109
+
110
+ it("10. Skips files matching excludeFiles", () => {
111
+ const code = `<WuButton>Submit</WuButton>`;
112
+ const plugin = wickuiI18nPlugin({ excludeFiles: ["**/ignored/**"] });
113
+ const hit = plugin.transform(code, "src/components/Form.jsx");
114
+ const miss = plugin.transform(code, "src/ignored/Form.jsx");
115
+ expect(hit?.code).toContain("WuTranslate");
116
+ expect(miss).toBeNull();
117
+ });
118
+
119
+ it("11. Strips newline from JSX text node", () => {
120
+ const code = `<WuButton>Hello\nWorld</WuButton>`;
121
+ const result = transform(code);
122
+ expect(result).toContain(
123
+ `<WuTranslate __i18nKey="Hello World"></WuTranslate>`,
124
+ );
125
+ });
126
+
127
+ it("11b. Collapses multiple newlines/spaces into a single space", () => {
128
+ const code = `<WuButton>Hello\n\n World</WuButton>`;
129
+ const result = transform(code);
130
+ expect(result).toContain(
131
+ `<WuTranslate __i18nKey="Hello World"></WuTranslate>`,
132
+ );
133
+ });
134
+
135
+ it("12. Translates both static branches of a ternary", () => {
136
+ const code = `<WuButton>{isActive ? "Deactivate" : "Activate"}</WuButton>`;
137
+ const result = transform(code);
138
+ expect(result).toContain(
139
+ `<WuTranslate __i18nKey="Deactivate"></WuTranslate>`,
140
+ );
141
+ expect(result).toContain(
142
+ `<WuTranslate __i18nKey="Activate"></WuTranslate>`,
143
+ );
144
+ expect(result).toContain(`isActive ?`);
145
+ expect(result).toContain(`import { WuTranslate }`);
146
+ });
147
+
148
+ it("12b. Translates only the static branch when one side is dynamic", () => {
149
+ const code = `<WuButton>{isActive ? "Deactivate" : dynamicLabel}</WuButton>`;
150
+ const result = transform(code);
151
+ expect(result).toContain(
152
+ `<WuTranslate __i18nKey="Deactivate"></WuTranslate>`,
153
+ );
154
+ expect(result).toContain(`dynamicLabel`);
155
+ expect(result).not.toContain(`__i18nKey="dynamicLabel"`);
156
+ });
157
+
158
+ it("12c. Skips ternary when both branches are dynamic", () => {
159
+ const code = `<WuButton>{isActive ? labelA : labelB}</WuButton>`;
160
+ const result = transform(code);
161
+ expect(result).not.toContain(`WuTranslate`);
162
+ expect(result).toContain(`labelA`);
163
+ expect(result).toContain(`labelB`);
164
+ });
165
+
166
+ it("12d. Translates ternary with template literal branches", () => {
167
+ const code = "<WuButton>{flag ? `Yes` : `No`}</WuButton>";
168
+ const result = transform(code);
169
+ expect(result).toContain(`<WuTranslate __i18nKey="Yes"></WuTranslate>`);
170
+ expect(result).toContain(`<WuTranslate __i18nKey="No"></WuTranslate>`);
171
+ });
172
+
173
+ it("12e. Skips ternary inside ignored component", () => {
174
+ const code = `<WuIcon>{flag ? "A" : "B"}</WuIcon>`;
175
+ const result = transform(code);
176
+ expect(result).not.toContain(`WuTranslate`);
177
+ });
178
+
179
+ it("12f. Respects data-skip on parent of ternary", () => {
180
+ const code = `<WuButton data-skip>{flag ? "A" : "B"}</WuButton>`;
181
+ const result = transform(code);
182
+ expect(result).not.toContain(`WuTranslate`);
183
+ });
184
+
185
+ it("12g. Nested ternary: translates first branch, skips inner ternary", () => {
186
+ // {a ? "A" : b ? "B" : "C"} — alternate is ConditionalExpression, not a static string
187
+ const code = `<WuButton>{a ? "A" : b ? "B" : "C"}</WuButton>`;
188
+ const result = transform(code);
189
+ expect(result).toContain(`<WuTranslate __i18nKey="A"></WuTranslate>`);
190
+ // inner ternary branches are not translated (known v1 limitation)
191
+ expect(result).toContain(`b ? "B" : "C"`);
192
+ });
193
+
194
+ it("12h. Ternary outside any Wu/wrapper component is not translated", () => {
195
+ const code = `<div>{flag ? "A" : "B"}</div>`;
196
+ const result = transform(code);
197
+ expect(result).not.toContain(`WuTranslate`);
198
+ });
199
+
200
+ it("13. Translates dynamic string literals", () => {
201
+ const code = `<WuButton>Submit {name}</WuButton>`;
202
+ const result = transform(code);
203
+ expect(result).toContain(`import { WuTranslate }`);
204
+ expect(result).toContain(`<WuTranslate __i18nKey="Submit"></WuTranslate>`);
205
+ });
206
+ });
207
+
208
+ // ─── HTML entities ─────────────────────────────────────────────────────────
209
+ //
210
+ // Any text segment containing an HTML entity must be left completely untouched.
211
+ // Entities are presentational / structural characters (&nbsp;, &amp;, &copy;…)
212
+ // that are not translatable and would confuse translation APIs.
213
+ // ─────────────────────────────────────────────────────────────────────────────
214
+
215
+ describe("HTML entities", () => {
216
+ it("E1. standalone named entity is not wrapped", () => {
217
+ const code = `<WuButton>&amp;</WuButton>`;
218
+ const result = transform(code);
219
+ expect(result).not.toContain("WuTranslate");
220
+ expect(result).toContain("&amp;");
221
+ });
222
+
223
+ it("E2. text mixed with entity — non-entity parts wrapped, entity preserved", () => {
224
+ const code = `<WuButton>Hello &amp; World</WuButton>`;
225
+ const result = transform(code);
226
+ expect(result).toContain(`<WuTranslate __i18nKey="Hello"></WuTranslate>`);
227
+ expect(result).toContain(`<WuTranslate __i18nKey="World"></WuTranslate>`);
228
+ expect(result).toContain("&amp;");
229
+ // must not treat the whole string (decoded or raw) as one key
230
+ expect(result).not.toContain(`__i18nKey="Hello & World"`);
231
+ expect(result).not.toContain(`__i18nKey="Hello &amp; World"`);
232
+ });
233
+
234
+ it("E3. non-breaking space entity is not wrapped", () => {
235
+ const code = `<WuButton>&nbsp;</WuButton>`;
236
+ const result = transform(code);
237
+ expect(result).not.toContain("WuTranslate");
238
+ });
239
+
240
+ it("E4. numeric decimal entity is not wrapped", () => {
241
+ // &#169; = ©
242
+ const code = `<WuButton>&#169;</WuButton>`;
243
+ const result = transform(code);
244
+ expect(result).not.toContain("WuTranslate");
245
+ });
246
+
247
+ it("E5. numeric hex entity is not wrapped", () => {
248
+ // &#x00A9; = ©
249
+ const code = `<WuButton>&#x00A9;</WuButton>`;
250
+ const result = transform(code);
251
+ expect(result).not.toContain("WuTranslate");
252
+ });
253
+
254
+ it("E6. entity inside static string expression is not wrapped", () => {
255
+ const code = `<WuButton>{"Hello &amp; World"}</WuButton>`;
256
+ const result = transform(code);
257
+ expect(result).not.toContain("WuTranslate");
258
+ });
259
+
260
+ it("E7. entity branch in ternary not wrapped, clean branch still wrapped", () => {
261
+ const code = `<WuButton>{flag ? "Yes &amp;" : "No"}</WuButton>`;
262
+ const result = transform(code);
263
+ expect(result).not.toContain(`__i18nKey="Yes &amp;"`);
264
+ expect(result).toContain(`<WuTranslate __i18nKey="No"></WuTranslate>`);
265
+ });
266
+
267
+ it("E8. entity JSXText sibling not wrapped, clean siblings still wrapped", () => {
268
+ // <WuButton>Hello {x}&amp;{y} World</WuButton>
269
+ // JSXText nodes: "Hello ", "&amp;", " World"
270
+ // Babel decodes &amp; → "&" in .value, so check both forms are absent
271
+ const code = `<WuButton>Hello {x}&amp;{y} World</WuButton>`;
272
+ const result = transform(code);
273
+ expect(result).toContain(`<WuTranslate __i18nKey="Hello"></WuTranslate>`);
274
+ expect(result).not.toContain(`__i18nKey="&amp;"`);
275
+ expect(result).not.toContain(`__i18nKey="&"`);
276
+ expect(result).toContain(`<WuTranslate __i18nKey="World"></WuTranslate>`);
277
+ });
278
+
279
+ it("E9. multiple entities — text between them wrapped, both entities kept", () => {
280
+ // &lt;Tag&gt; → &lt;<WuTranslate key="Tag" />&gt;
281
+ const code = `<WuButton>&lt;Tag&gt;</WuButton>`;
282
+ const result = transform(code);
283
+ expect(result).toContain(`<WuTranslate __i18nKey="Tag"></WuTranslate>`);
284
+ expect(result).toContain("&lt;");
285
+ expect(result).toContain("&gt;");
286
+ });
287
+
288
+ it("E10. entity at boundaries — text in middle still wrapped", () => {
289
+ // &nbsp;Hello&nbsp; → &nbsp;<WuTranslate key="Hello" />&nbsp;
290
+ const code = `<WuButton>&nbsp;Hello&nbsp;</WuButton>`;
291
+ const result = transform(code);
292
+ expect(result).toContain(`<WuTranslate __i18nKey="Hello"></WuTranslate>`);
293
+ expect(result).toContain("&nbsp;");
294
+ });
295
+
296
+ it("E11. only entities, no text — nothing wrapped", () => {
297
+ const code = `<WuButton>&lt;&gt;</WuButton>`;
298
+ const result = transform(code);
299
+ expect(result).not.toContain("WuTranslate");
300
+ });
301
+
302
+ // fix 1: whitespace normalisation inside entity-split segments
303
+ it("E12. internal newlines in entity-split segment are normalised", () => {
304
+ const code = "<WuButton>Hello\nWorld &amp; Goodbye\nFriend</WuButton>";
305
+ const result = transform(code);
306
+ expect(result).toContain(`__i18nKey="Hello World"`);
307
+ expect(result).toContain(`__i18nKey="Goodbye Friend"`);
308
+ expect(result).not.toContain("Hello\\nWorld");
309
+ });
310
+
311
+ it("E13. multiple spaces in entity-split segment are collapsed", () => {
312
+ const code = "<WuButton>Hello World &amp; Foo</WuButton>";
313
+ const result = transform(code);
314
+ expect(result).toContain(`__i18nKey="Hello World"`);
315
+ expect(result).toContain(`__i18nKey="Foo"`);
316
+ expect(result).not.toContain(`__i18nKey="Hello World"`);
317
+ });
318
+
319
+ // fix 2: data-i18n-key bypass
320
+ it("E14. data-i18n-key with entity — warns and splits using text as keys", () => {
321
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
322
+ const code = `<WuButton data-i18n-key="btn">Hello &amp; World</WuButton>`;
323
+ const result = transform(code);
324
+ expect(warnSpy).toHaveBeenCalledWith(
325
+ expect.stringContaining('data-i18n-key="btn"'),
326
+ );
327
+ expect(result).toContain(`__i18nKey="Hello"`);
328
+ expect(result).toContain(`__i18nKey="World"`);
329
+ expect(result).not.toContain(`__i18nKey="btn"`);
330
+ warnSpy.mockRestore();
331
+ });
332
+ });
333
+
334
+ describe("Mixed static + dynamic interpolation", () => {
335
+ describe("A: JSX mixed children", () => {
336
+ it("A1. text before dynamic → wraps leading text, keeps expr", () => {
337
+ // <WuButton>hello {name}</WuButton>
338
+ // children: JSXText("hello ") + JSXExpressionContainer(name)
339
+ const code = `<WuButton>hello {name}</WuButton>`;
340
+ const result = transform(code);
341
+ expect(result).toContain(`<WuTranslate __i18nKey="hello"></WuTranslate>`);
342
+ expect(result).toContain(`{name}`);
343
+ expect(result).not.toContain(`__i18nKey="name"`);
344
+ });
345
+
346
+ it("A2. dynamic before text → keeps expr, wraps trailing text", () => {
347
+ // <WuButton>{name} how are you</WuButton>
348
+ const code = `<WuButton>{name} how are you</WuButton>`;
349
+ const result = transform(code);
350
+ expect(result).toContain(
351
+ `<WuTranslate __i18nKey="how are you"></WuTranslate>`,
352
+ );
353
+ expect(result).toContain(`{name}`);
354
+ });
355
+
356
+ it("A3. text + dynamic + text → wraps both text segments independently", () => {
357
+ // <WuButton>hello {name} how are you</WuButton>
358
+ const code = `<WuButton>hello {name} how are you</WuButton>`;
359
+ const result = transform(code);
360
+ expect(result).toContain(`<WuTranslate __i18nKey="hello"></WuTranslate>`);
361
+ expect(result).toContain(
362
+ `<WuTranslate __i18nKey="how are you"></WuTranslate>`,
363
+ );
364
+ expect(result).toContain(`{name}`);
365
+ });
366
+
367
+ it("A4. multiple dynamics with text between → wraps middle text", () => {
368
+ // <WuButton>{a} and {b}</WuButton>
369
+ const code = `<WuButton>{a} and {b}</WuButton>`;
370
+ const result = transform(code);
371
+ expect(result).toContain(`<WuTranslate __i18nKey="and"></WuTranslate>`);
372
+ expect(result).toContain(`{a}`);
373
+ expect(result).toContain(`{b}`);
374
+ });
375
+
376
+ it("A5. text + two dynamics + text → wraps outer text, keeps both exprs", () => {
377
+ // <WuButton>hello {a} and {b} end</WuButton>
378
+ const code = `<WuButton>hello {a} and {b} end</WuButton>`;
379
+ const result = transform(code);
380
+ expect(result).toContain(`<WuTranslate __i18nKey="hello"></WuTranslate>`);
381
+ expect(result).toContain(`<WuTranslate __i18nKey="and"></WuTranslate>`);
382
+ expect(result).toContain(`<WuTranslate __i18nKey="end"></WuTranslate>`);
383
+ expect(result).toContain(`{a}`);
384
+ expect(result).toContain(`{b}`);
385
+ });
386
+
387
+ it("A6. whitespace-only text between two dynamics → not wrapped", () => {
388
+ // <WuButton>{a} {b}</WuButton> — JSXText between them is only spaces
389
+ const code = `<WuButton>{a} {b}</WuButton>`;
390
+ const result = transform(code);
391
+ // whitespace-only JSXText must not produce a key
392
+ expect(result).not.toContain(`WuTranslate`);
393
+ });
394
+ });
395
+
396
+ // ── B: Template literal with expressions ──────────────────────────────────
397
+ // Currently the plugin skips any TemplateLiteral whose .expressions.length > 0.
398
+ // The expected output reconstructs the container as a React fragment:
399
+ // {`hello ${name}`} → <><WuTranslate __i18nKey="hello" />{name}</>
400
+ //
401
+ // All tests in this group are expected to FAIL until the feature lands.
402
+
403
+ describe("B: Template literal with expressions", () => {
404
+ it("B1. text before dynamic → wraps leading quasi, keeps expr", () => {
405
+ // {`hello ${name}`} → <><WuTranslate __i18nKey="hello" />{name}</>
406
+ const code = "<WuButton>{`hello ${name}`}</WuButton>";
407
+ const result = transform(code);
408
+ expect(result).toContain(`<WuTranslate __i18nKey="hello"></WuTranslate>`);
409
+ expect(result).toContain(`{name}`);
410
+ expect(result).not.toContain('__i18nKey="name"');
411
+ });
412
+
413
+ it("B2. dynamic before text → keeps expr, wraps trailing quasi", () => {
414
+ // {`${name} how are you`} → <>{name}<WuTranslate __i18nKey="how are you" /></>
415
+ const code = "<WuButton>{`${name} how are you`}</WuButton>";
416
+ const result = transform(code);
417
+ expect(result).toContain(
418
+ `<WuTranslate __i18nKey="how are you"></WuTranslate>`,
419
+ );
420
+ expect(result).toContain(`{name}`);
421
+ });
422
+
423
+ it("B3. text + dynamic + text → wraps both quasis, keeps expr in between", () => {
424
+ // {`hello ${name} how are you`}
425
+ // → <><WuTranslate __i18nKey="hello" />{name}<WuTranslate __i18nKey="how are you" /></>
426
+ const code = "<WuButton>{`hello ${name} how are you`}</WuButton>";
427
+ const result = transform(code);
428
+ expect(result).toContain(`<WuTranslate __i18nKey="hello"></WuTranslate>`);
429
+ expect(result).toContain(
430
+ `<WuTranslate __i18nKey="how are you"></WuTranslate>`,
431
+ );
432
+ expect(result).toContain(`{name}`);
433
+ });
434
+
435
+ it("B4. two dynamics with static text between → wraps middle quasi only", () => {
436
+ // {`${a} and ${b}`} → <>{a}<WuTranslate __i18nKey="and" />{b}</>
437
+ const code = "<WuButton>{`${a} and ${b}`}</WuButton>";
438
+ const result = transform(code);
439
+ expect(result).toContain(`<WuTranslate __i18nKey="and"></WuTranslate>`);
440
+ expect(result).toContain(`{a}`);
441
+ expect(result).toContain(`{b}`);
442
+ });
443
+
444
+ it("B5. text + two dynamics + text → wraps outer quasis and middle quasi", () => {
445
+ // {`hello ${a} and ${b} end`}
446
+ // → <><WuTranslate key="hello" />{a}<WuTranslate key="and" />{b}<WuTranslate key="end" /></>
447
+ const code = "<WuButton>{`hello ${a} and ${b} end`}</WuButton>";
448
+ const result = transform(code);
449
+ expect(result).toContain(`<WuTranslate __i18nKey="hello"></WuTranslate>`);
450
+ expect(result).toContain(`<WuTranslate __i18nKey="and"></WuTranslate>`);
451
+ expect(result).toContain(`<WuTranslate __i18nKey="end"></WuTranslate>`);
452
+ expect(result).toContain(`{a}`);
453
+ expect(result).toContain(`{b}`);
454
+ });
455
+
456
+ it("B6. empty quasis between two dynamics → no key emitted for empty segment", () => {
457
+ // {`${a}${b}`} → <>{a}{b}</>
458
+ // quasis: ["", "", ""] — all empty, nothing to wrap
459
+ const code = "<WuButton>{`${a}${b}`}</WuButton>";
460
+ const result = transform(code);
461
+ // no translation key for empty/whitespace quasis
462
+ expect(result).not.toContain(`__i18nKey`);
463
+ });
464
+
465
+ it("B7. whitespace-only quasi between dynamics → not wrapped", () => {
466
+ // {`${a} ${b}`} → <>{a} {b}</>
467
+ const code = "<WuButton>{`${a} ${b}`}</WuButton>";
468
+ const result = transform(code);
469
+ expect(result).not.toContain(`WuTranslate`);
470
+ });
471
+
472
+ it("B8. ignored component — template literal skipped entirely", () => {
473
+ const code = "<WuIcon>{`hello ${name}`}</WuIcon>";
474
+ const result = transform(code);
475
+ expect(result).not.toContain(`WuTranslate`);
476
+ });
477
+
478
+ it("B9. data-skip — template literal skipped entirely", () => {
479
+ const code = "<WuButton data-skip>{`hello ${name}`}</WuButton>";
480
+ const result = transform(code);
481
+ expect(result).not.toContain(`WuTranslate`);
482
+ });
483
+
484
+ it("B10. output is a React fragment wrapping the interleaved nodes", () => {
485
+ // the entire {`...`} container is replaced with <> ... </>
486
+ const code = "<WuButton>{`hello ${name}`}</WuButton>";
487
+ const result = transform(code);
488
+ expect(result).toMatch(/<>.*WuTranslate.*<\/>/s);
489
+ });
490
+ });
491
+ });
492
+
493
+ // ─── wt() call expressions ───────────────────────────────────────────────────────
494
+ //
495
+ // wt("static string") is a runtime lookup helper exposed from useTranslate().
496
+ // The plugin’s job is to record static arguments into wick-ui-i18n.json so
497
+ // they reach the translation API. No code transformation is performed.
498
+ // ─────────────────────────────────────────────────────────────────────────────
499
+
500
+ describe("wt() call expressions", () => {
501
+ it("W1. string literal arg is recorded in dictionary", () => {
502
+ const dict = getDictionary(`const text = wt("hello");`);
503
+ expect(dict).toHaveProperty("hello", "hello");
504
+ });
505
+
506
+ it("W2. static template literal arg is recorded", () => {
507
+ const dict = getDictionary("const text = wt(`hello`);");
508
+ expect(dict).toHaveProperty("hello", "hello");
509
+ });
510
+
511
+ it("W3. dynamic variable arg is ignored", () => {
512
+ const dict = getDictionary(`const text = wt(variable);`);
513
+ expect(dict).not.toHaveProperty("variable");
514
+ });
515
+
516
+ it("W4. template literal with expressions — static quasis extracted", () => {
517
+ // wt(`hello ${name}`) → `${wt("hello")} ${name}`
518
+ const dict = getDictionary("const text = wt(`hello ${name}`);");
519
+ expect(dict).toHaveProperty("hello", "hello");
520
+ });
521
+
522
+ it("W11. template literal — call is transformed into nested wt() calls", () => {
523
+ const result = transform("const text = wt(`hello ${name}`);");
524
+ expect(result).toContain('wt("hello")');
525
+ expect(result).not.toContain("wt(`hello ${name}`)");
526
+ });
527
+
528
+ it("W12. text before expr → leading quasi wrapped, expr kept", () => {
529
+ const result = transform("const t = wt(`hello ${name}`);");
530
+ expect(result).toContain('wt("hello")');
531
+ expect(result).toContain("${name}");
532
+ });
533
+
534
+ it("W13. text after expr → trailing quasi wrapped, expr kept", () => {
535
+ const result = transform("const t = wt(`${name} how are you`);");
536
+ expect(result).toContain('wt("how are you")');
537
+ expect(result).toContain("${name}");
538
+ });
539
+
540
+ it("W14. text + expr + text → both quasis wrapped, expr in between", () => {
541
+ const result = transform("const t = wt(`hello ${name} how are you`);");
542
+ expect(result).toContain('wt("hello")');
543
+ expect(result).toContain('wt("how are you")');
544
+ expect(result).toContain("${name}");
545
+ });
546
+
547
+ it("W15. only expressions, no static text → no transform", () => {
548
+ const result = transform("const t = wt(`${a}${b}`);");
549
+ expect(result).not.toContain("wt(\"");
550
+ // original call preserved as-is
551
+ expect(result).toContain("wt(`${a}${b}`)");
552
+ });
553
+
554
+ it("W16. whitespace-only quasi between exprs → no transform", () => {
555
+ const result = transform("const t = wt(`${a} ${b}`);");
556
+ expect(result).not.toContain("wt(\"");
557
+ });
558
+
559
+ it("W5. multiple wt() calls in same file all recorded", () => {
560
+ const dict = getDictionary(`const a = wt("hello"); const b = wt("world");`);
561
+ expect(dict).toHaveProperty("hello", "hello");
562
+ expect(dict).toHaveProperty("world", "world");
563
+ });
564
+
565
+ it("W6. whitespace in arg is normalised", () => {
566
+ const dict = getDictionary(`const text = wt("hello world");`);
567
+ expect(dict).toHaveProperty("hello world", "hello world");
568
+ });
569
+
570
+ it("W7. wt() call inside JSX expression is recorded", () => {
571
+ const dict = getDictionary(`<WuButton>{wt("hello")}</WuButton>`);
572
+ expect(dict).toHaveProperty("hello", "hello");
573
+ });
574
+
575
+ it("W8. code is not transformed — wt() call left as-is", () => {
576
+ const result = transform(`const text = wt("hello");`);
577
+ expect(result).not.toContain("WuTranslate");
578
+ expect(result).toContain(`wt("hello")`);
579
+ });
580
+
581
+ it("W9. wt() and JSX translations coexist in same file", () => {
582
+ const code = `<WuButton>{wt("save")}</WuButton>`;
583
+ const dict = getDictionary(code);
584
+ // wt() arg recorded
585
+ expect(dict).toHaveProperty("save", "save");
586
+ // code still not transformed (no WuTranslate wrapping the wt call)
587
+ const result = transform(code);
588
+ expect(result).toContain(`wt("save")`);
589
+ });
590
+
591
+ it("W10. empty string arg is ignored", () => {
592
+ const dict = getDictionary(`const text = wt("");`);
593
+ expect(Object.keys(dict)).toHaveLength(0);
594
+ });
77
595
  });