@lokascript/i18n 1.0.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.
Files changed (96) hide show
  1. package/README.md +286 -0
  2. package/dist/browser.cjs +7669 -0
  3. package/dist/browser.cjs.map +1 -0
  4. package/dist/browser.d.cts +50 -0
  5. package/dist/browser.d.ts +50 -0
  6. package/dist/browser.js +7592 -0
  7. package/dist/browser.js.map +1 -0
  8. package/dist/hyperfixi-i18n.min.js +2 -0
  9. package/dist/hyperfixi-i18n.min.js.map +1 -0
  10. package/dist/hyperfixi-i18n.mjs +8558 -0
  11. package/dist/hyperfixi-i18n.mjs.map +1 -0
  12. package/dist/index.cjs +14205 -0
  13. package/dist/index.cjs.map +1 -0
  14. package/dist/index.d.cts +947 -0
  15. package/dist/index.d.ts +947 -0
  16. package/dist/index.js +14095 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/transformer-Ckask-yw.d.cts +1041 -0
  19. package/dist/transformer-Ckask-yw.d.ts +1041 -0
  20. package/package.json +84 -0
  21. package/src/browser.ts +122 -0
  22. package/src/compatibility/browser-tests/grammar-demo.spec.ts +169 -0
  23. package/src/constants.ts +366 -0
  24. package/src/dictionaries/ar.ts +233 -0
  25. package/src/dictionaries/bn.ts +156 -0
  26. package/src/dictionaries/de.ts +233 -0
  27. package/src/dictionaries/derive.ts +515 -0
  28. package/src/dictionaries/en.ts +237 -0
  29. package/src/dictionaries/es.ts +233 -0
  30. package/src/dictionaries/fr.ts +233 -0
  31. package/src/dictionaries/hi.ts +270 -0
  32. package/src/dictionaries/id.ts +233 -0
  33. package/src/dictionaries/index.ts +238 -0
  34. package/src/dictionaries/it.ts +233 -0
  35. package/src/dictionaries/ja.ts +233 -0
  36. package/src/dictionaries/ko.ts +233 -0
  37. package/src/dictionaries/ms.ts +276 -0
  38. package/src/dictionaries/pl.ts +239 -0
  39. package/src/dictionaries/pt.ts +237 -0
  40. package/src/dictionaries/qu.ts +233 -0
  41. package/src/dictionaries/ru.ts +270 -0
  42. package/src/dictionaries/sw.ts +233 -0
  43. package/src/dictionaries/th.ts +156 -0
  44. package/src/dictionaries/tl.ts +276 -0
  45. package/src/dictionaries/tr.ts +233 -0
  46. package/src/dictionaries/uk.ts +270 -0
  47. package/src/dictionaries/vi.ts +210 -0
  48. package/src/dictionaries/zh.ts +233 -0
  49. package/src/enhanced-i18n.test.ts +454 -0
  50. package/src/enhanced-i18n.ts +713 -0
  51. package/src/examples/new-languages.ts +326 -0
  52. package/src/formatting.test.ts +213 -0
  53. package/src/formatting.ts +416 -0
  54. package/src/grammar/direct-mappings.ts +353 -0
  55. package/src/grammar/grammar.test.ts +1053 -0
  56. package/src/grammar/index.ts +59 -0
  57. package/src/grammar/profiles/index.ts +860 -0
  58. package/src/grammar/transformer.ts +1318 -0
  59. package/src/grammar/types.ts +630 -0
  60. package/src/index.ts +202 -0
  61. package/src/new-languages.test.ts +389 -0
  62. package/src/parser/analyze-conflicts.test.ts +229 -0
  63. package/src/parser/ar.ts +40 -0
  64. package/src/parser/create-provider.ts +309 -0
  65. package/src/parser/de.ts +36 -0
  66. package/src/parser/es.ts +31 -0
  67. package/src/parser/fr.ts +31 -0
  68. package/src/parser/id.ts +34 -0
  69. package/src/parser/index.ts +50 -0
  70. package/src/parser/ja.ts +36 -0
  71. package/src/parser/ko.ts +37 -0
  72. package/src/parser/locale-manager.test.ts +198 -0
  73. package/src/parser/locale-manager.ts +197 -0
  74. package/src/parser/parser-integration.test.ts +439 -0
  75. package/src/parser/pt.ts +37 -0
  76. package/src/parser/qu.ts +37 -0
  77. package/src/parser/sw.ts +37 -0
  78. package/src/parser/tr.ts +38 -0
  79. package/src/parser/types.ts +113 -0
  80. package/src/parser/zh.ts +38 -0
  81. package/src/plugins/vite.ts +224 -0
  82. package/src/plugins/webpack.ts +124 -0
  83. package/src/pluralization.test.ts +197 -0
  84. package/src/pluralization.ts +393 -0
  85. package/src/runtime.ts +441 -0
  86. package/src/ssr-integration.ts +225 -0
  87. package/src/test-setup.ts +195 -0
  88. package/src/translation-validation.test.ts +171 -0
  89. package/src/translator.test.ts +252 -0
  90. package/src/translator.ts +297 -0
  91. package/src/types.ts +209 -0
  92. package/src/utils/locale.ts +190 -0
  93. package/src/utils/tokenizer-adapter.ts +469 -0
  94. package/src/utils/tokenizer.ts +19 -0
  95. package/src/validators/index.ts +174 -0
  96. package/src/validators/schema.ts +129 -0
@@ -0,0 +1,1053 @@
1
+ /**
2
+ * Grammar Transformer Tests
3
+ *
4
+ * Tests for the generalized grammar transformation system
5
+ * that handles multilingual hyperscript with proper word order
6
+ * and grammatical markers.
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest';
10
+ import {
11
+ parseStatement,
12
+ toLocale,
13
+ toEnglish,
14
+ translate,
15
+ GrammarTransformer,
16
+ examples,
17
+ } from './transformer';
18
+ import {
19
+ getProfile,
20
+ getSupportedLocales,
21
+ profiles,
22
+ englishProfile,
23
+ japaneseProfile,
24
+ chineseProfile,
25
+ arabicProfile,
26
+ } from './profiles';
27
+ import {
28
+ reorderRoles,
29
+ insertMarkers,
30
+ joinTokens,
31
+ UNIVERSAL_PATTERNS,
32
+ LANGUAGE_FAMILY_DEFAULTS,
33
+ } from './types';
34
+ import type { ParsedElement, SemanticRole } from './types';
35
+
36
+ // =============================================================================
37
+ // Profile Tests
38
+ // =============================================================================
39
+
40
+ describe('Language Profiles', () => {
41
+ it('should have profiles for all supported locales', () => {
42
+ const locales = getSupportedLocales();
43
+ expect(locales).toContain('en');
44
+ expect(locales).toContain('ja');
45
+ expect(locales).toContain('ko');
46
+ expect(locales).toContain('zh');
47
+ expect(locales).toContain('ar');
48
+ expect(locales).toContain('tr');
49
+ expect(locales).toContain('es');
50
+ expect(locales).toContain('de');
51
+ expect(locales).toContain('fr');
52
+ expect(locales).toContain('pt');
53
+ expect(locales).toContain('id');
54
+ expect(locales).toContain('qu');
55
+ expect(locales).toContain('sw');
56
+ expect(locales).toContain('bn');
57
+ // New languages added
58
+ expect(locales).toContain('it');
59
+ expect(locales).toContain('ru');
60
+ expect(locales).toContain('uk');
61
+ expect(locales).toContain('vi');
62
+ expect(locales).toContain('hi');
63
+ expect(locales).toContain('tl');
64
+ expect(locales).toContain('th');
65
+ expect(locales).toContain('pl');
66
+ expect(locales.length).toBe(22);
67
+ });
68
+
69
+ it('should return undefined for unknown locales', () => {
70
+ expect(getProfile('xx')).toBeUndefined();
71
+ expect(getProfile('xyz')).toBeUndefined();
72
+ });
73
+
74
+ describe('English Profile', () => {
75
+ it('should have SVO word order', () => {
76
+ expect(englishProfile.wordOrder).toBe('SVO');
77
+ });
78
+
79
+ it('should use prepositions', () => {
80
+ expect(englishProfile.adpositionType).toBe('preposition');
81
+ });
82
+
83
+ it('should have required markers', () => {
84
+ const onMarker = englishProfile.markers.find(m => m.form === 'on');
85
+ expect(onMarker).toBeDefined();
86
+ expect(onMarker?.role).toBe('event');
87
+ expect(onMarker?.required).toBe(true);
88
+ });
89
+ });
90
+
91
+ describe('Japanese Profile', () => {
92
+ it('should have SOV word order', () => {
93
+ expect(japaneseProfile.wordOrder).toBe('SOV');
94
+ });
95
+
96
+ it('should use postpositions', () => {
97
+ expect(japaneseProfile.adpositionType).toBe('postposition');
98
+ });
99
+
100
+ it('should have particle markers', () => {
101
+ const woMarker = japaneseProfile.markers.find(m => m.form === 'を');
102
+ expect(woMarker).toBeDefined();
103
+ expect(woMarker?.role).toBe('patient');
104
+ expect(woMarker?.position).toBe('postposition');
105
+ });
106
+
107
+ it('should place patient before action in canonical order', () => {
108
+ const patientIndex = japaneseProfile.canonicalOrder.indexOf('patient');
109
+ const actionIndex = japaneseProfile.canonicalOrder.indexOf('action');
110
+ expect(patientIndex).toBeLessThan(actionIndex);
111
+ });
112
+ });
113
+
114
+ describe('Arabic Profile', () => {
115
+ it('should have VSO word order', () => {
116
+ expect(arabicProfile.wordOrder).toBe('VSO');
117
+ });
118
+
119
+ it('should be RTL', () => {
120
+ expect(arabicProfile.direction).toBe('rtl');
121
+ });
122
+
123
+ it('should place action first in canonical order', () => {
124
+ expect(arabicProfile.canonicalOrder[0]).toBe('action');
125
+ });
126
+ });
127
+
128
+ describe('Chinese Profile', () => {
129
+ it('should have isolating morphology', () => {
130
+ expect(chineseProfile.morphology).toBe('isolating');
131
+ });
132
+
133
+ it('should have circumfix markers for events', () => {
134
+ const eventMarkers = chineseProfile.markers.filter(m => m.role === 'event');
135
+ const hasPreposition = eventMarkers.some(m => m.position === 'preposition');
136
+ const hasPostposition = eventMarkers.some(m => m.position === 'postposition');
137
+ expect(hasPreposition).toBe(true);
138
+ expect(hasPostposition).toBe(true);
139
+ });
140
+ });
141
+ });
142
+
143
+ // =============================================================================
144
+ // Statement Parsing Tests
145
+ // =============================================================================
146
+
147
+ describe('Statement Parser', () => {
148
+ describe('parseStatement', () => {
149
+ it('should parse event handlers', () => {
150
+ const parsed = parseStatement('on click increment #count');
151
+ expect(parsed).not.toBeNull();
152
+ expect(parsed?.type).toBe('event-handler');
153
+ expect(parsed?.roles.get('event')?.value).toBe('click');
154
+ expect(parsed?.roles.get('action')?.value).toBe('increment');
155
+ expect(parsed?.roles.get('patient')?.value).toBe('#count');
156
+ });
157
+
158
+ it('should identify CSS selectors as patient', () => {
159
+ const parsed = parseStatement('on click toggle .active');
160
+ expect(parsed?.roles.get('patient')?.isSelector).toBe(true);
161
+ });
162
+
163
+ it('should parse commands', () => {
164
+ const parsed = parseStatement('put my value into #output');
165
+ expect(parsed).not.toBeNull();
166
+ expect(parsed?.type).toBe('command');
167
+ expect(parsed?.roles.get('action')?.value).toBe('put');
168
+ });
169
+
170
+ it('should parse conditionals', () => {
171
+ const parsed = parseStatement('if count > 5 then log done');
172
+ expect(parsed).not.toBeNull();
173
+ expect(parsed?.type).toBe('conditional');
174
+ });
175
+
176
+ it('should return null for empty input', () => {
177
+ const parsed = parseStatement('');
178
+ expect(parsed).toBeNull();
179
+ });
180
+
181
+ it('should preserve original input', () => {
182
+ const input = 'on click increment #count';
183
+ const parsed = parseStatement(input);
184
+ expect(parsed?.original).toBe(input);
185
+ });
186
+ });
187
+
188
+ describe('Event Handler Parsing', () => {
189
+ it('should handle various event types', () => {
190
+ const events = ['click', 'input', 'keydown', 'mouseenter', 'submit'];
191
+ for (const event of events) {
192
+ const parsed = parseStatement(`on ${event} log done`);
193
+ expect(parsed?.roles.get('event')?.value).toBe(event);
194
+ }
195
+ });
196
+
197
+ it('should handle complex selectors', () => {
198
+ const parsed = parseStatement('on click toggle .menu-item.active');
199
+ expect(parsed?.roles.get('patient')?.value).toBe('.menu-item.active');
200
+ });
201
+ });
202
+
203
+ describe('Command Parsing', () => {
204
+ it('should identify destination with "to" keyword', () => {
205
+ const parsed = parseStatement('add .highlight to #element');
206
+ expect(parsed?.roles.get('destination')?.value).toBe('#element');
207
+ });
208
+
209
+ it('should identify destination with "into" keyword', () => {
210
+ const parsed = parseStatement('put value into #output');
211
+ expect(parsed?.roles.get('destination')?.value).toBe('#output');
212
+ });
213
+
214
+ it('should identify source with "from" keyword', () => {
215
+ const parsed = parseStatement('get data from #input');
216
+ expect(parsed?.roles.get('source')?.value).toBe('#input');
217
+ });
218
+ });
219
+ });
220
+
221
+ // =============================================================================
222
+ // Role Transformation Tests
223
+ // =============================================================================
224
+
225
+ describe('Role Transformation', () => {
226
+ describe('reorderRoles', () => {
227
+ it('should reorder roles according to target order', () => {
228
+ const roles = new Map<SemanticRole, ParsedElement>([
229
+ ['action', { role: 'action', value: 'increment' }],
230
+ ['patient', { role: 'patient', value: '#count' }],
231
+ ['event', { role: 'event', value: 'click' }],
232
+ ]);
233
+
234
+ // Japanese order: patient, event, action
235
+ const reordered = reorderRoles(roles, ['patient', 'event', 'action']);
236
+
237
+ expect(reordered[0].role).toBe('patient');
238
+ expect(reordered[1].role).toBe('event');
239
+ expect(reordered[2].role).toBe('action');
240
+ });
241
+
242
+ it('should skip roles not present in input', () => {
243
+ const roles = new Map<SemanticRole, ParsedElement>([
244
+ ['action', { role: 'action', value: 'toggle' }],
245
+ ['patient', { role: 'patient', value: '.active' }],
246
+ ]);
247
+
248
+ const reordered = reorderRoles(roles, ['patient', 'destination', 'action']);
249
+
250
+ expect(reordered.length).toBe(2);
251
+ expect(reordered[0].role).toBe('patient');
252
+ expect(reordered[1].role).toBe('action');
253
+ });
254
+ });
255
+
256
+ describe('insertMarkers', () => {
257
+ it('should insert preposition markers before elements', () => {
258
+ const elements: ParsedElement[] = [
259
+ { role: 'destination', value: '#output', translated: '#output' },
260
+ ];
261
+ const markers = [
262
+ {
263
+ form: 'to',
264
+ role: 'destination' as SemanticRole,
265
+ position: 'preposition' as const,
266
+ required: false,
267
+ },
268
+ ];
269
+
270
+ const result = insertMarkers(elements, markers, 'preposition');
271
+ expect(result).toEqual(['to', '#output']);
272
+ });
273
+
274
+ it('should insert postposition markers after elements', () => {
275
+ const elements: ParsedElement[] = [
276
+ { role: 'patient', value: '#count', translated: '#count' },
277
+ ];
278
+ const markers = [
279
+ {
280
+ form: 'を',
281
+ role: 'patient' as SemanticRole,
282
+ position: 'postposition' as const,
283
+ required: true,
284
+ },
285
+ ];
286
+
287
+ const result = insertMarkers(elements, markers, 'postposition');
288
+ expect(result).toEqual(['#count', 'を']);
289
+ });
290
+
291
+ it('should use translated values when available', () => {
292
+ const elements: ParsedElement[] = [
293
+ { role: 'action', value: 'increment', translated: '増加' },
294
+ ];
295
+
296
+ const result = insertMarkers(elements, [], 'none');
297
+ expect(result).toEqual(['増加']);
298
+ });
299
+ });
300
+
301
+ describe('joinTokens', () => {
302
+ it('should join regular tokens with spaces', () => {
303
+ const result = joinTokens(['hello', 'world']);
304
+ expect(result).toBe('hello world');
305
+ });
306
+
307
+ it('should handle empty array', () => {
308
+ const result = joinTokens([]);
309
+ expect(result).toBe('');
310
+ });
311
+
312
+ it('should handle single token', () => {
313
+ const result = joinTokens(['hello']);
314
+ expect(result).toBe('hello');
315
+ });
316
+
317
+ it('should attach suffix markers without space (Quechua -ta)', () => {
318
+ // #count + -ta → #countta
319
+ const result = joinTokens(['#count', '-ta']);
320
+ expect(result).toBe('#countta');
321
+ });
322
+
323
+ it('should attach prefix markers without space (Arabic بـ-)', () => {
324
+ // بـ- + الماوس → بـالماوس
325
+ const result = joinTokens(['بـ-', 'الماوس']);
326
+ expect(result).toBe('بـالماوس');
327
+ });
328
+
329
+ it('should handle multiple suffix markers (Turkish case suffixes)', () => {
330
+ // value + -i + another → valuei another
331
+ const result = joinTokens(['value', '-i', 'another']);
332
+ expect(result).toBe('valuei another');
333
+ });
334
+
335
+ it('should handle Japanese particles with normal spacing', () => {
336
+ // Japanese particles don't use hyphen notation, so they get spaces
337
+ const result = joinTokens(['#count', 'を', 'クリック', 'で', '増加']);
338
+ expect(result).toBe('#count を クリック で 増加');
339
+ });
340
+
341
+ it('should handle Quechua agglutinative chain', () => {
342
+ // #count + -ta + click + -pi + increment
343
+ const result = joinTokens(['#count', '-ta', 'click', '-pi', 'increment']);
344
+ expect(result).toBe('#countta clickpi increment');
345
+ });
346
+
347
+ it('should handle mixed prefix and regular tokens', () => {
348
+ const result = joinTokens(['كـ-', 'JSON', 'format']);
349
+ expect(result).toBe('كـJSON format');
350
+ });
351
+ });
352
+ });
353
+
354
+ // =============================================================================
355
+ // Grammar Transformer Tests
356
+ // =============================================================================
357
+
358
+ describe('GrammarTransformer', () => {
359
+ describe('Constructor', () => {
360
+ it('should create transformer with valid locales', () => {
361
+ expect(() => new GrammarTransformer('en', 'ja')).not.toThrow();
362
+ expect(() => new GrammarTransformer('en', 'zh')).not.toThrow();
363
+ expect(() => new GrammarTransformer('en', 'ar')).not.toThrow();
364
+ });
365
+
366
+ it('should throw for invalid source locale', () => {
367
+ expect(() => new GrammarTransformer('xx', 'ja')).toThrow('Unknown source locale');
368
+ });
369
+
370
+ it('should throw for invalid target locale', () => {
371
+ expect(() => new GrammarTransformer('en', 'xx')).toThrow('Unknown target locale');
372
+ });
373
+ });
374
+
375
+ describe('Japanese Transformation (SOV)', () => {
376
+ const transformer = new GrammarTransformer('en', 'ja');
377
+
378
+ it('should transform event handler to SOV order', () => {
379
+ const result = transformer.transform('on click increment #count');
380
+ // Should have patient (with を), event (with で), action pattern
381
+ expect(result).toContain('#count');
382
+ expect(result).toContain('を');
383
+ });
384
+
385
+ it('should preserve CSS selectors', () => {
386
+ const result = transformer.transform('on click toggle .active');
387
+ expect(result).toContain('.active');
388
+ });
389
+
390
+ it('should preserve ID selectors', () => {
391
+ const result = transformer.transform('on input put value into #output');
392
+ expect(result).toContain('#output');
393
+ });
394
+ });
395
+
396
+ describe('Arabic Transformation (VSO)', () => {
397
+ const transformer = new GrammarTransformer('en', 'ar');
398
+
399
+ it('should transform to VSO order with action first', () => {
400
+ const result = transformer.transform('on click increment #count');
401
+ // Arabic VSO: action comes first
402
+ expect(result).toBeTruthy();
403
+ // Verify action (زِد/increment) appears before patient (#count)
404
+ const actionIndex = result.indexOf('زِد');
405
+ const patientIndex = result.indexOf('#count');
406
+ expect(actionIndex).toBeLessThan(patientIndex);
407
+ });
408
+
409
+ it('should preserve selectors in transformation', () => {
410
+ const result = transformer.transform('on click toggle .active');
411
+ expect(result).toContain('.active');
412
+ });
413
+ });
414
+
415
+ describe('Chinese Transformation (Topic-Prominent)', () => {
416
+ const transformer = new GrammarTransformer('en', 'zh');
417
+
418
+ it('should use 当 marker for events', () => {
419
+ const result = transformer.transform('on click increment #count');
420
+ // Chinese uses 当...时 pattern but custom transform may omit 时
421
+ expect(result).toContain('当');
422
+ });
423
+
424
+ it('should include translated action', () => {
425
+ const result = transformer.transform('on click increment #count');
426
+ // Should contain 增加 (increment in Chinese)
427
+ expect(result).toContain('增加');
428
+ });
429
+
430
+ it('should preserve patient selector', () => {
431
+ const result = transformer.transform('on click toggle .menu');
432
+ expect(result).toContain('.menu');
433
+ });
434
+ });
435
+ });
436
+
437
+ // =============================================================================
438
+ // Convenience Function Tests
439
+ // =============================================================================
440
+
441
+ describe('Convenience Functions', () => {
442
+ describe('toLocale', () => {
443
+ it('should transform English to Japanese', () => {
444
+ const result = toLocale('on click toggle .active', 'ja');
445
+ expect(result).toContain('.active');
446
+ expect(result).toContain('を');
447
+ });
448
+
449
+ it('should transform English to Chinese', () => {
450
+ const result = toLocale('on click increment #count', 'zh');
451
+ expect(result).toContain('当');
452
+ });
453
+ });
454
+
455
+ describe('toEnglish', () => {
456
+ it('should return unchanged when parsing fails', () => {
457
+ // This tests fallback behavior
458
+ const result = toEnglish('invalid input', 'ja');
459
+ expect(result).toBeTruthy();
460
+ });
461
+ });
462
+
463
+ describe('translate', () => {
464
+ it('should return unchanged for same locale', () => {
465
+ const input = 'on click toggle .active';
466
+ expect(translate(input, 'en', 'en')).toBe(input);
467
+ });
468
+
469
+ it('should translate English to target locale', () => {
470
+ const result = translate('on click increment #count', 'en', 'ja');
471
+ expect(result).toContain('#count');
472
+ });
473
+
474
+ it('should translate to English from source locale', () => {
475
+ const result = translate('test input', 'ja', 'en');
476
+ expect(result).toBeTruthy();
477
+ });
478
+
479
+ it('should translate via English pivot', () => {
480
+ const result = translate('on click log done', 'ja', 'zh');
481
+ expect(result).toBeTruthy();
482
+ });
483
+ });
484
+ });
485
+
486
+ // =============================================================================
487
+ // Universal Pattern Tests
488
+ // =============================================================================
489
+
490
+ describe('Universal Patterns', () => {
491
+ it('should define event-increment pattern', () => {
492
+ const pattern = UNIVERSAL_PATTERNS.eventIncrement;
493
+ expect(pattern.name).toBe('event-increment');
494
+ expect(pattern.roles).toContain('event');
495
+ expect(pattern.roles).toContain('action');
496
+ expect(pattern.roles).toContain('patient');
497
+ });
498
+
499
+ it('should define put-into pattern', () => {
500
+ const pattern = UNIVERSAL_PATTERNS.putInto;
501
+ expect(pattern.name).toBe('put-into');
502
+ expect(pattern.roles).toContain('action');
503
+ expect(pattern.roles).toContain('patient');
504
+ expect(pattern.roles).toContain('destination');
505
+ });
506
+
507
+ it('should define wait-duration pattern', () => {
508
+ const pattern = UNIVERSAL_PATTERNS.waitDuration;
509
+ expect(pattern.roles).toContain('action');
510
+ expect(pattern.roles).toContain('quantity');
511
+ });
512
+ });
513
+
514
+ // =============================================================================
515
+ // Language Family Defaults Tests
516
+ // =============================================================================
517
+
518
+ describe('Language Family Defaults', () => {
519
+ it('should have Germanic defaults', () => {
520
+ const germanic = LANGUAGE_FAMILY_DEFAULTS.germanic;
521
+ expect(germanic.wordOrder).toBe('SVO');
522
+ expect(germanic.adpositionType).toBe('preposition');
523
+ });
524
+
525
+ it('should have Japonic defaults', () => {
526
+ const japonic = LANGUAGE_FAMILY_DEFAULTS.japonic;
527
+ expect(japonic.wordOrder).toBe('SOV');
528
+ expect(japonic.adpositionType).toBe('postposition');
529
+ });
530
+
531
+ it('should have Semitic defaults', () => {
532
+ const semitic = LANGUAGE_FAMILY_DEFAULTS.semitic;
533
+ expect(semitic.wordOrder).toBe('VSO');
534
+ expect(semitic.direction).toBe('rtl');
535
+ });
536
+
537
+ it('should have Sinitic defaults', () => {
538
+ const sinitic = LANGUAGE_FAMILY_DEFAULTS.sinitic;
539
+ expect(sinitic.morphology).toBe('isolating');
540
+ });
541
+ });
542
+
543
+ // =============================================================================
544
+ // Examples Tests
545
+ // =============================================================================
546
+
547
+ describe('Grammar Examples', () => {
548
+ it('should have English examples', () => {
549
+ expect(examples.english.eventHandler).toBe('on click increment #count');
550
+ expect(examples.english.putInto).toBe('put my value into #output');
551
+ expect(examples.english.toggle).toBe('toggle .active');
552
+ });
553
+
554
+ it('should have Japanese examples', () => {
555
+ expect(examples.japanese.eventHandler).toContain('#count');
556
+ expect(examples.japanese.eventHandler).toContain('を');
557
+ });
558
+
559
+ it('should have Chinese examples', () => {
560
+ expect(examples.chinese.eventHandler).toContain('当');
561
+ expect(examples.chinese.eventHandler).toContain('时');
562
+ });
563
+
564
+ it('should have Arabic examples', () => {
565
+ expect(examples.arabic.eventHandler).toContain('عند');
566
+ });
567
+ });
568
+
569
+ // =============================================================================
570
+ // Edge Cases
571
+ // =============================================================================
572
+
573
+ describe('Edge Cases', () => {
574
+ it('should handle empty input gracefully', () => {
575
+ const transformer = new GrammarTransformer('en', 'ja');
576
+ const result = transformer.transform('');
577
+ expect(result).toBe('');
578
+ });
579
+
580
+ it('should handle single-word input', () => {
581
+ const transformer = new GrammarTransformer('en', 'ja');
582
+ const result = transformer.transform('toggle');
583
+ expect(result).toBeTruthy();
584
+ });
585
+
586
+ it('should preserve numbers', () => {
587
+ const transformer = new GrammarTransformer('en', 'ja');
588
+ const result = transformer.transform('wait 500');
589
+ expect(result).toContain('500');
590
+ });
591
+
592
+ it('should handle complex selectors with special characters', () => {
593
+ const parsed = parseStatement('on click toggle .menu-item[data-active="true"]');
594
+ expect(parsed?.roles.get('patient')?.value).toContain('data-active');
595
+ });
596
+
597
+ it('should handle multiple spaces in input', () => {
598
+ const parsed = parseStatement('on click toggle .active');
599
+ expect(parsed).not.toBeNull();
600
+ });
601
+ });
602
+
603
+ // =============================================================================
604
+ // Chinese Circumfix Tokenization Tests
605
+ // =============================================================================
606
+
607
+ describe('Chinese Circumfix Parsing', () => {
608
+ it('should split attached 时 suffix from event words', () => {
609
+ // 点击时 should be parsed as two tokens: 点击 + 时
610
+ const parsed = parseStatement('当 点击时 增加 #count', 'zh');
611
+ expect(parsed).not.toBeNull();
612
+ expect(parsed?.type).toBe('event-handler');
613
+ });
614
+
615
+ it('should handle 当...时 circumfix pattern', () => {
616
+ const transformer = new GrammarTransformer('en', 'zh');
617
+ const result = transformer.transform('on click increment #count');
618
+ // Should produce 当 X 时 pattern
619
+ expect(result).toContain('当');
620
+ expect(result).toContain('时');
621
+ });
622
+
623
+ it('should preserve selectors when splitting suffixes', () => {
624
+ const parsed = parseStatement('当 点击时 切换 .active', 'zh');
625
+ // Patient may include the action in some parsing patterns
626
+ expect(parsed?.roles.get('patient')?.value).toContain('.active');
627
+ });
628
+ });
629
+
630
+ // =============================================================================
631
+ // Round-Trip Translation Tests
632
+ // =============================================================================
633
+
634
+ describe('Round-Trip Translation', () => {
635
+ describe('English → Japanese → English', () => {
636
+ it('should preserve semantic roles in round-trip', () => {
637
+ const original = 'on click increment #count';
638
+ const toJapanese = translate(original, 'en', 'ja');
639
+ expect(toJapanese).toContain('#count');
640
+ expect(toJapanese).toContain('を');
641
+
642
+ // Note: Perfect round-trip isn't expected due to translation,
643
+ // but semantic structure should be preserved
644
+ const backToEnglish = translate(toJapanese, 'ja', 'en');
645
+ expect(backToEnglish).toBeTruthy();
646
+ });
647
+
648
+ it('should preserve CSS selectors through round-trip', () => {
649
+ const original = 'toggle .menu-active';
650
+ const toJapanese = translate(original, 'en', 'ja');
651
+ expect(toJapanese).toContain('.menu-active');
652
+
653
+ const backToEnglish = translate(toJapanese, 'ja', 'en');
654
+ expect(backToEnglish).toContain('.menu-active');
655
+ });
656
+ });
657
+
658
+ describe('English → Arabic → English', () => {
659
+ it('should preserve semantic roles with VSO transformation', () => {
660
+ const original = 'on click increment #count';
661
+ const toArabic = translate(original, 'en', 'ar');
662
+ expect(toArabic).toContain('#count');
663
+ // Arabic VSO puts action first
664
+ expect(toArabic).toBeTruthy();
665
+
666
+ const backToEnglish = translate(toArabic, 'ar', 'en');
667
+ expect(backToEnglish).toBeTruthy();
668
+ });
669
+ });
670
+
671
+ describe('English → Chinese → English', () => {
672
+ it('should preserve structure through topic-prominent language', () => {
673
+ const original = 'on click toggle .active';
674
+ const toChinese = translate(original, 'en', 'zh');
675
+ expect(toChinese).toContain('.active');
676
+ expect(toChinese).toContain('当');
677
+
678
+ const backToEnglish = translate(toChinese, 'zh', 'en');
679
+ expect(backToEnglish).toContain('.active');
680
+ });
681
+ });
682
+
683
+ describe('Cross-Language via Pivot', () => {
684
+ it('should translate Japanese → Arabic via English pivot', () => {
685
+ // Start with a simple pattern
686
+ const result = translate('on click log done', 'ja', 'ar');
687
+ expect(result).toBeTruthy();
688
+ });
689
+
690
+ it('should translate Chinese → Korean via English pivot', () => {
691
+ const result = translate('on click toggle .active', 'zh', 'ko');
692
+ expect(result).toBeTruthy();
693
+ expect(result).toContain('.active');
694
+ });
695
+ });
696
+ });
697
+
698
+ // =============================================================================
699
+ // Language-Specific Word Order Integration Tests
700
+ // =============================================================================
701
+
702
+ describe('Word Order Integration Tests', () => {
703
+ describe('SOV Languages (Japanese, Korean, Turkish, Quechua)', () => {
704
+ it('should place patient before action in Japanese', () => {
705
+ const transformer = new GrammarTransformer('en', 'ja');
706
+ const result = transformer.transform('on click increment #count');
707
+ // Japanese SOV: #count を ... 増加
708
+ const countIndex = result.indexOf('#count');
709
+ const actionIndex = result.indexOf('増加');
710
+ expect(countIndex).toBeLessThan(actionIndex);
711
+ });
712
+
713
+ it('should place patient before action in Korean', () => {
714
+ const transformer = new GrammarTransformer('en', 'ko');
715
+ const result = transformer.transform('on click increment #count');
716
+ // Korean SOV: patient comes before action
717
+ expect(result).toContain('#count');
718
+ expect(result).toContain('를'); // Object marker
719
+ });
720
+
721
+ it('should preserve Japanese particle spacing (regression)', () => {
722
+ const transformer = new GrammarTransformer('en', 'ja');
723
+ const result = transformer.transform('on click toggle .active');
724
+ // Japanese particles (を, で, に) should have spaces around them
725
+ // They do NOT use hyphen notation like Turkish suffixes
726
+ expect(result).toContain('.active を'); // Space before particle
727
+ });
728
+
729
+ it('should attach Turkish suffixes correctly', () => {
730
+ const transformer = new GrammarTransformer('en', 'tr');
731
+ const result = transformer.transform('on click toggle .active');
732
+ // Turkish uses case suffixes - should be attached without spaces
733
+ expect(result).toContain('.active');
734
+ // Verify suffixes are attached (no space before suffix)
735
+ expect(result).not.toMatch(/\s-[iae]/); // No space before -i, -a, -e suffixes
736
+ });
737
+
738
+ it('should attach Turkish accusative suffix -i to patient', () => {
739
+ const transformer = new GrammarTransformer('en', 'tr');
740
+ const result = transformer.transform('on click toggle .active');
741
+ // Should be ".activei" not ".active -i"
742
+ expect(result).toMatch(/\.active[iıuü]/); // Vowel harmony variants
743
+ expect(result).not.toContain('.active -i');
744
+ expect(result).not.toContain('.active -ı');
745
+ });
746
+
747
+ it('should attach Turkish locative suffix -de to event', () => {
748
+ const transformer = new GrammarTransformer('en', 'tr');
749
+ const result = transformer.transform('on click toggle .active');
750
+ // Event "tıklama" should have locative attached: "tıklamade" or "tıklamada"
751
+ expect(result).toMatch(/tıklama[dD][aAeE]/);
752
+ });
753
+ });
754
+
755
+ describe('VSO Languages (Arabic)', () => {
756
+ it('should place action first in Arabic', () => {
757
+ const transformer = new GrammarTransformer('en', 'ar');
758
+ const result = transformer.transform('on click increment #count');
759
+ // Arabic VSO: زِد (action) comes first
760
+ const actionIndex = result.indexOf('زِد');
761
+ const patientIndex = result.indexOf('#count');
762
+ expect(actionIndex).toBeLessThan(patientIndex);
763
+ });
764
+ });
765
+
766
+ describe('SVO Languages with Special Features', () => {
767
+ it('should use circumfix pattern for Chinese events', () => {
768
+ const transformer = new GrammarTransformer('en', 'zh');
769
+ const result = transformer.transform('on click increment #count');
770
+ // Chinese uses 当...时 circumfix
771
+ expect(result).toContain('当');
772
+ expect(result).toContain('时');
773
+ });
774
+
775
+ it('should use correct markers for Spanish', () => {
776
+ const transformer = new GrammarTransformer('en', 'es');
777
+ const result = transformer.transform('on click toggle .active');
778
+ // Spanish uses 'en' for events
779
+ expect(result).toContain('.active');
780
+ });
781
+
782
+ it('should handle Indonesian SVO correctly', () => {
783
+ const transformer = new GrammarTransformer('en', 'id');
784
+ const result = transformer.transform('on click toggle .active');
785
+ expect(result).toContain('.active');
786
+ });
787
+
788
+ it('should handle Swahili SVO correctly', () => {
789
+ const transformer = new GrammarTransformer('en', 'sw');
790
+ const result = transformer.transform('on click toggle .active');
791
+ expect(result).toContain('.active');
792
+ });
793
+ });
794
+ });
795
+
796
+ // =============================================================================
797
+ // Line Structure Preservation Tests
798
+ // =============================================================================
799
+
800
+ describe('Line Structure Preservation', () => {
801
+ describe('Indentation Preservation', () => {
802
+ it('should preserve indentation in multi-line statements', () => {
803
+ const input = `on click
804
+ toggle .active on me
805
+ wait 1 second`;
806
+
807
+ const transformer = new GrammarTransformer('en', 'es');
808
+ const result = transformer.transform(input);
809
+
810
+ const lines = result.split('\n');
811
+ expect(lines.length).toBe(3);
812
+ // First line has no indentation
813
+ expect(lines[0]).not.toMatch(/^\s/);
814
+ // Subsequent lines should have indentation
815
+ expect(lines[1]).toMatch(/^\s{4}/);
816
+ expect(lines[2]).toMatch(/^\s{4}/);
817
+ });
818
+
819
+ it('should normalize mixed tab/space indentation', () => {
820
+ const input = `on click
821
+ \ttoggle .active
822
+ wait 1 second`;
823
+
824
+ const transformer = new GrammarTransformer('en', 'ja');
825
+ const result = transformer.transform(input);
826
+
827
+ const lines = result.split('\n');
828
+ expect(lines.length).toBe(3);
829
+ // Both indented lines should use consistent 4-space indentation
830
+ const indent1 = lines[1].match(/^\s*/)?.[0] || '';
831
+ const indent2 = lines[2].match(/^\s*/)?.[0] || '';
832
+ // Tabs normalized to spaces
833
+ expect(indent1).not.toContain('\t');
834
+ expect(indent2).not.toContain('\t');
835
+ });
836
+
837
+ it('should handle deeply nested indentation', () => {
838
+ const input = `on click
839
+ if something
840
+ toggle .active
841
+ wait 1 second`;
842
+
843
+ const transformer = new GrammarTransformer('en', 'ko');
844
+ const result = transformer.transform(input);
845
+
846
+ const lines = result.split('\n');
847
+ expect(lines.length).toBe(4);
848
+ // Check relative indentation is preserved
849
+ const indent1 = (lines[1].match(/^\s*/)?.[0] || '').length;
850
+ const indent2 = (lines[2].match(/^\s*/)?.[0] || '').length;
851
+ const indent3 = (lines[3].match(/^\s*/)?.[0] || '').length;
852
+ expect(indent2).toBeGreaterThan(indent1);
853
+ expect(indent3).toBe(indent2); // Same level as line above
854
+ });
855
+ });
856
+
857
+ describe('Blank Line Preservation', () => {
858
+ it('should preserve blank lines between statements', () => {
859
+ const input = `on click
860
+ toggle .active
861
+
862
+ wait 1 second`;
863
+
864
+ const transformer = new GrammarTransformer('en', 'zh');
865
+ const result = transformer.transform(input);
866
+
867
+ const lines = result.split('\n');
868
+ expect(lines.length).toBe(4);
869
+ expect(lines[2]).toBe(''); // Blank line preserved
870
+ });
871
+
872
+ it('should preserve multiple consecutive blank lines', () => {
873
+ const input = `on click
874
+
875
+
876
+ toggle .active`;
877
+
878
+ const transformer = new GrammarTransformer('en', 'ar');
879
+ const result = transformer.transform(input);
880
+
881
+ const lines = result.split('\n');
882
+ expect(lines.length).toBe(4);
883
+ expect(lines[1]).toBe('');
884
+ expect(lines[2]).toBe('');
885
+ });
886
+
887
+ it('should handle blank lines with only whitespace', () => {
888
+ const input = `on click
889
+ toggle .active
890
+
891
+ wait 1 second`;
892
+
893
+ const transformer = new GrammarTransformer('en', 'tr');
894
+ const result = transformer.transform(input);
895
+
896
+ const lines = result.split('\n');
897
+ expect(lines.length).toBe(4);
898
+ // Line with only whitespace should become empty
899
+ expect(lines[2]).toBe('');
900
+ });
901
+ });
902
+
903
+ describe('Single-line Backward Compatibility', () => {
904
+ it('should not change behavior for single-line input', () => {
905
+ const input = 'on click toggle .active';
906
+ const transformer = new GrammarTransformer('en', 'es');
907
+ const result = transformer.transform(input);
908
+
909
+ // Should not contain newlines
910
+ expect(result).not.toContain('\n');
911
+ });
912
+
913
+ it('should handle then-chains on single line', () => {
914
+ const input = 'on click toggle .active then wait 1 second';
915
+ const transformer = new GrammarTransformer('en', 'ja');
916
+ const result = transformer.transform(input);
917
+
918
+ // Should not contain newlines
919
+ expect(result).not.toContain('\n');
920
+ // Should still have the translated "then" keyword
921
+ expect(result.split(' ').length).toBeGreaterThan(3);
922
+ });
923
+ });
924
+
925
+ describe('Multi-language Structure Preservation', () => {
926
+ const languages = ['es', 'ja', 'ko', 'zh', 'ar', 'tr', 'id', 'qu', 'sw'];
927
+
928
+ for (const lang of languages) {
929
+ it(`should preserve structure when translating to ${lang}`, () => {
930
+ const input = `on click
931
+ toggle .active
932
+
933
+ wait 1 second
934
+ remove .active`;
935
+
936
+ const transformer = new GrammarTransformer('en', lang);
937
+ const result = transformer.transform(input);
938
+
939
+ const lines = result.split('\n');
940
+ expect(lines.length).toBe(5);
941
+ // Verify blank line is preserved
942
+ expect(lines[2]).toBe('');
943
+ // Verify non-blank lines have content
944
+ expect(lines[0].trim().length).toBeGreaterThan(0);
945
+ expect(lines[1].trim().length).toBeGreaterThan(0);
946
+ expect(lines[3].trim().length).toBeGreaterThan(0);
947
+ expect(lines[4].trim().length).toBeGreaterThan(0);
948
+ });
949
+ }
950
+ });
951
+ });
952
+
953
+ // =============================================================================
954
+ // Has/Have Operator Translation Tests
955
+ // =============================================================================
956
+
957
+ describe('Has/Have Operator Translations', () => {
958
+ describe('Dictionary Entries', () => {
959
+ // Import dictionaries to verify has/have entries exist
960
+ it('should have has/have in English dictionary', async () => {
961
+ const { en } = await import('../dictionaries/en');
962
+ expect(en.logical.has).toBe('has');
963
+ expect(en.logical.have).toBe('have');
964
+ });
965
+
966
+ it('should have has/have in Spanish dictionary', async () => {
967
+ const { es } = await import('../dictionaries/es');
968
+ expect(es.logical.has).toBe('tiene'); // third-person
969
+ expect(es.logical.have).toBe('tengo'); // first-person
970
+ });
971
+
972
+ it('should have has/have in Japanese dictionary', async () => {
973
+ const { ja } = await import('../dictionaries/ja');
974
+ expect(ja.logical.has).toBe('ある');
975
+ expect(ja.logical.have).toBe('ある');
976
+ });
977
+
978
+ it('should have has/have in German dictionary', async () => {
979
+ const { de } = await import('../dictionaries/de');
980
+ expect(de.logical.has).toBe('hat'); // third-person
981
+ expect(de.logical.have).toBe('habe'); // first-person
982
+ });
983
+
984
+ it('should have has/have in French dictionary', async () => {
985
+ const { fr } = await import('../dictionaries/fr');
986
+ expect(fr.logical.has).toBe('a'); // third-person
987
+ expect(fr.logical.have).toBe('ai'); // first-person
988
+ });
989
+
990
+ it('should have has/have in Korean dictionary', async () => {
991
+ const { ko } = await import('../dictionaries/ko');
992
+ expect(ko.logical.has).toBe('있다');
993
+ expect(ko.logical.have).toBe('있다');
994
+ });
995
+
996
+ it('should have has/have in Chinese dictionary', async () => {
997
+ const { zh } = await import('../dictionaries/zh');
998
+ expect(zh.logical.has).toBe('有');
999
+ expect(zh.logical.have).toBe('有');
1000
+ });
1001
+
1002
+ it('should have has/have in Arabic dictionary', async () => {
1003
+ const { ar } = await import('../dictionaries/ar');
1004
+ expect(ar.logical.has).toBe('لديه'); // third-person
1005
+ expect(ar.logical.have).toBe('لدي'); // first-person
1006
+ });
1007
+ });
1008
+
1009
+ describe('Conjugating Languages', () => {
1010
+ // Languages that have different forms for has (3rd person) vs have (1st person)
1011
+ const conjugatingLanguages = [
1012
+ { code: 'es', has: 'tiene', have: 'tengo' },
1013
+ { code: 'de', has: 'hat', have: 'habe' },
1014
+ { code: 'fr', has: 'a', have: 'ai' },
1015
+ { code: 'pt', has: 'tem', have: 'tenho' },
1016
+ { code: 'it', has: 'ha', have: 'ho' },
1017
+ { code: 'pl', has: 'ma', have: 'mam' },
1018
+ ];
1019
+
1020
+ for (const lang of conjugatingLanguages) {
1021
+ it(`should have different has/have forms in ${lang.code}`, async () => {
1022
+ const dict = await import(`../dictionaries/${lang.code}`);
1023
+ const dictionary = Object.values(dict)[0] as { logical: { has: string; have: string } };
1024
+ expect(dictionary.logical.has).toBe(lang.has);
1025
+ expect(dictionary.logical.have).toBe(lang.have);
1026
+ });
1027
+ }
1028
+ });
1029
+
1030
+ describe('Non-Conjugating Languages', () => {
1031
+ // Languages that use the same form for both has and have
1032
+ const sameFormLanguages = [
1033
+ { code: 'ja', form: 'ある' },
1034
+ { code: 'ko', form: '있다' },
1035
+ { code: 'zh', form: '有' },
1036
+ { code: 'tr', form: 'var' },
1037
+ { code: 'id', form: 'punya' },
1038
+ { code: 'vi', form: 'có' },
1039
+ { code: 'th', form: 'มี' },
1040
+ { code: 'tl', form: 'may' },
1041
+ { code: 'ms', form: 'ada' },
1042
+ ];
1043
+
1044
+ for (const lang of sameFormLanguages) {
1045
+ it(`should have same has/have form in ${lang.code}`, async () => {
1046
+ const dict = await import(`../dictionaries/${lang.code}`);
1047
+ const dictionary = Object.values(dict)[0] as { logical: { has: string; have: string } };
1048
+ expect(dictionary.logical.has).toBe(lang.form);
1049
+ expect(dictionary.logical.have).toBe(lang.form);
1050
+ });
1051
+ }
1052
+ });
1053
+ });