@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.
- package/README.md +286 -0
- package/dist/browser.cjs +7669 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +50 -0
- package/dist/browser.d.ts +50 -0
- package/dist/browser.js +7592 -0
- package/dist/browser.js.map +1 -0
- package/dist/hyperfixi-i18n.min.js +2 -0
- package/dist/hyperfixi-i18n.min.js.map +1 -0
- package/dist/hyperfixi-i18n.mjs +8558 -0
- package/dist/hyperfixi-i18n.mjs.map +1 -0
- package/dist/index.cjs +14205 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +947 -0
- package/dist/index.d.ts +947 -0
- package/dist/index.js +14095 -0
- package/dist/index.js.map +1 -0
- package/dist/transformer-Ckask-yw.d.cts +1041 -0
- package/dist/transformer-Ckask-yw.d.ts +1041 -0
- package/package.json +84 -0
- package/src/browser.ts +122 -0
- package/src/compatibility/browser-tests/grammar-demo.spec.ts +169 -0
- package/src/constants.ts +366 -0
- package/src/dictionaries/ar.ts +233 -0
- package/src/dictionaries/bn.ts +156 -0
- package/src/dictionaries/de.ts +233 -0
- package/src/dictionaries/derive.ts +515 -0
- package/src/dictionaries/en.ts +237 -0
- package/src/dictionaries/es.ts +233 -0
- package/src/dictionaries/fr.ts +233 -0
- package/src/dictionaries/hi.ts +270 -0
- package/src/dictionaries/id.ts +233 -0
- package/src/dictionaries/index.ts +238 -0
- package/src/dictionaries/it.ts +233 -0
- package/src/dictionaries/ja.ts +233 -0
- package/src/dictionaries/ko.ts +233 -0
- package/src/dictionaries/ms.ts +276 -0
- package/src/dictionaries/pl.ts +239 -0
- package/src/dictionaries/pt.ts +237 -0
- package/src/dictionaries/qu.ts +233 -0
- package/src/dictionaries/ru.ts +270 -0
- package/src/dictionaries/sw.ts +233 -0
- package/src/dictionaries/th.ts +156 -0
- package/src/dictionaries/tl.ts +276 -0
- package/src/dictionaries/tr.ts +233 -0
- package/src/dictionaries/uk.ts +270 -0
- package/src/dictionaries/vi.ts +210 -0
- package/src/dictionaries/zh.ts +233 -0
- package/src/enhanced-i18n.test.ts +454 -0
- package/src/enhanced-i18n.ts +713 -0
- package/src/examples/new-languages.ts +326 -0
- package/src/formatting.test.ts +213 -0
- package/src/formatting.ts +416 -0
- package/src/grammar/direct-mappings.ts +353 -0
- package/src/grammar/grammar.test.ts +1053 -0
- package/src/grammar/index.ts +59 -0
- package/src/grammar/profiles/index.ts +860 -0
- package/src/grammar/transformer.ts +1318 -0
- package/src/grammar/types.ts +630 -0
- package/src/index.ts +202 -0
- package/src/new-languages.test.ts +389 -0
- package/src/parser/analyze-conflicts.test.ts +229 -0
- package/src/parser/ar.ts +40 -0
- package/src/parser/create-provider.ts +309 -0
- package/src/parser/de.ts +36 -0
- package/src/parser/es.ts +31 -0
- package/src/parser/fr.ts +31 -0
- package/src/parser/id.ts +34 -0
- package/src/parser/index.ts +50 -0
- package/src/parser/ja.ts +36 -0
- package/src/parser/ko.ts +37 -0
- package/src/parser/locale-manager.test.ts +198 -0
- package/src/parser/locale-manager.ts +197 -0
- package/src/parser/parser-integration.test.ts +439 -0
- package/src/parser/pt.ts +37 -0
- package/src/parser/qu.ts +37 -0
- package/src/parser/sw.ts +37 -0
- package/src/parser/tr.ts +38 -0
- package/src/parser/types.ts +113 -0
- package/src/parser/zh.ts +38 -0
- package/src/plugins/vite.ts +224 -0
- package/src/plugins/webpack.ts +124 -0
- package/src/pluralization.test.ts +197 -0
- package/src/pluralization.ts +393 -0
- package/src/runtime.ts +441 -0
- package/src/ssr-integration.ts +225 -0
- package/src/test-setup.ts +195 -0
- package/src/translation-validation.test.ts +171 -0
- package/src/translator.test.ts +252 -0
- package/src/translator.ts +297 -0
- package/src/types.ts +209 -0
- package/src/utils/locale.ts +190 -0
- package/src/utils/tokenizer-adapter.ts +469 -0
- package/src/utils/tokenizer.ts +19 -0
- package/src/validators/index.ts +174 -0
- 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
|
+
});
|