@lokascript/i18n 2.7.2 → 2.9.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/CHANGELOG.md +51 -0
- package/README.md +1 -1
- package/dist/browser.cjs +649 -180
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +2 -2
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +649 -180
- package/dist/browser.js.map +1 -1
- package/dist/dictionaries/index.cjs +370 -174
- package/dist/dictionaries/index.cjs.map +1 -1
- package/dist/dictionaries/index.js +370 -174
- package/dist/dictionaries/index.js.map +1 -1
- package/dist/index.cjs +649 -180
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +649 -180
- package/dist/index.js.map +1 -1
- package/dist/lokascript-i18n.min.js +1 -1
- package/dist/lokascript-i18n.min.js.map +1 -1
- package/dist/lokascript-i18n.mjs +678 -175
- package/dist/lokascript-i18n.mjs.map +1 -1
- package/dist/plugins/vite.cjs +370 -174
- package/dist/plugins/vite.cjs.map +1 -1
- package/dist/plugins/vite.js +370 -174
- package/dist/plugins/vite.js.map +1 -1
- package/dist/plugins/webpack.cjs +370 -174
- package/dist/plugins/webpack.cjs.map +1 -1
- package/dist/plugins/webpack.js +370 -174
- package/dist/plugins/webpack.js.map +1 -1
- package/dist/{transformer-DM7iplCU.d.cts → transformer--W46PGaW.d.cts} +8 -0
- package/dist/{transformer-BA8YX9u1.d.ts → transformer-FU06J6kG.d.ts} +8 -0
- package/package.json +9 -10
- package/src/dictionaries/ar.ts +20 -15
- package/src/dictionaries/bn.ts +10 -5
- package/src/dictionaries/de.ts +20 -8
- package/src/dictionaries/es.ts +10 -5
- package/src/dictionaries/fr.ts +15 -6
- package/src/dictionaries/he.ts +4 -0
- package/src/dictionaries/hi.ts +3 -2
- package/src/dictionaries/id.ts +31 -16
- package/src/dictionaries/it.ts +11 -4
- package/src/dictionaries/ja.ts +23 -15
- package/src/dictionaries/ko.ts +10 -5
- package/src/dictionaries/ms.ts +4 -1
- package/src/dictionaries/pl.ts +12 -5
- package/src/dictionaries/pt.ts +10 -5
- package/src/dictionaries/qu.ts +30 -19
- package/src/dictionaries/ru.ts +5 -2
- package/src/dictionaries/sw.ts +29 -14
- package/src/dictionaries/th.ts +5 -2
- package/src/dictionaries/tl.ts +4 -1
- package/src/dictionaries/tr.ts +24 -16
- package/src/dictionaries/uk.ts +5 -2
- package/src/dictionaries/vi.ts +23 -17
- package/src/dictionaries/zh.ts +24 -9
- package/src/grammar/grammar.test.ts +152 -0
- package/src/grammar/profiles/index.ts +316 -1
- package/src/grammar/transformer.ts +64 -1
- package/src/new-languages.test.ts +9 -3
- package/src/positional-keyword-drift.test.ts +6 -19
|
@@ -2510,3 +2510,155 @@ describe('qu fused (no-underscore) empty/null value word (chusaq)', () => {
|
|
|
2510
2510
|
expect(out.indexOf('chusaq')).toBeLessThan(out.indexOf('.error'));
|
|
2511
2511
|
});
|
|
2512
2512
|
});
|
|
2513
|
+
|
|
2514
|
+
// =============================================================================
|
|
2515
|
+
// SOV reorder stranding (transformer-rendering arc)
|
|
2516
|
+
// =============================================================================
|
|
2517
|
+
//
|
|
2518
|
+
// Three rendering defects, one family (docs-internal/HANDOFF_transformer-rendering.md):
|
|
2519
|
+
// (a) `wait for X or Y from Z` rendered verb-first (tr) / verb-medial (qu) —
|
|
2520
|
+
// reorderRoles' safety-net appended roles missing from canonicalOrder
|
|
2521
|
+
// AFTER the verb, stranding the or-run/from-phrase outside the clause;
|
|
2522
|
+
// (b) `repeat until event E from S` postposed the from-phrase after the verb;
|
|
2523
|
+
// (c) a trailing block terminator (`… wait 200ms end` fragments from the
|
|
2524
|
+
// then-splitter) was swept into the open role's VALUE and rendered inside
|
|
2525
|
+
// the phrase instead of after the verb.
|
|
2526
|
+
// Fixed by extending tr/qu/bn canonicalOrder to full verb-final role orders
|
|
2527
|
+
// and stripping/re-appending the stranded terminator in transformSingle.
|
|
2528
|
+
|
|
2529
|
+
describe('SOV reorder stranding (transformer-rendering arc)', () => {
|
|
2530
|
+
it('[tr] wait + or-run + from-phrase renders verb-final, from-phrase in-clause', () => {
|
|
2531
|
+
const out = new GrammarTransformer('en', 'tr').transform(
|
|
2532
|
+
'wait for pointermove(clientY) or pointerup(clientY) from document'
|
|
2533
|
+
);
|
|
2534
|
+
expect(out).toContain('veya');
|
|
2535
|
+
expect(out.trim().endsWith('bekle')).toBe(true);
|
|
2536
|
+
expect(out.indexOf('belge den')).toBeGreaterThanOrEqual(0);
|
|
2537
|
+
expect(out.indexOf('belge den')).toBeLessThan(out.indexOf('bekle'));
|
|
2538
|
+
});
|
|
2539
|
+
|
|
2540
|
+
it('[qu] wait + or-run + from-phrase renders verb-final, from-phrase in-clause', () => {
|
|
2541
|
+
const out = new GrammarTransformer('en', 'qu').transform(
|
|
2542
|
+
'wait for pointermove(clientY) or pointerup(clientY) from document'
|
|
2543
|
+
);
|
|
2544
|
+
expect(out).toContain('utaq');
|
|
2545
|
+
expect(out.trim().endsWith('suyay')).toBe(true);
|
|
2546
|
+
expect(out.indexOf('qillqa manta')).toBeGreaterThanOrEqual(0);
|
|
2547
|
+
expect(out.indexOf('qillqa manta')).toBeLessThan(out.indexOf('pointermove'));
|
|
2548
|
+
});
|
|
2549
|
+
|
|
2550
|
+
it('[tr] repeat-until-event keeps the from-phrase before the verb', () => {
|
|
2551
|
+
const out = new GrammarTransformer('en', 'tr').transform(
|
|
2552
|
+
'repeat until event pointerup from document'
|
|
2553
|
+
);
|
|
2554
|
+
expect(out.trim().endsWith('tekrarla')).toBe(true);
|
|
2555
|
+
expect(out.indexOf('belge den')).toBeGreaterThanOrEqual(0);
|
|
2556
|
+
expect(out.indexOf('belge den')).toBeLessThan(out.indexOf('tekrarla'));
|
|
2557
|
+
});
|
|
2558
|
+
|
|
2559
|
+
it('[bn] trailing-end fragment renders the terminator after the verb', () => {
|
|
2560
|
+
// `then`-split fragment of `… then wait 200ms end` (repeat-while body).
|
|
2561
|
+
const out = new GrammarTransformer('en', 'bn').transform('wait 200ms end');
|
|
2562
|
+
expect(out.trim()).toBe('200ms কে অপেক্ষা শেষ');
|
|
2563
|
+
expect(out).not.toContain('শেষ কে'); // terminator no longer inside the object phrase
|
|
2564
|
+
});
|
|
2565
|
+
|
|
2566
|
+
it('[qu] inline-if set keeps its verb-final tail before the terminator', () => {
|
|
2567
|
+
const out = new GrammarTransformer('en', 'qu').transform(
|
|
2568
|
+
'if newWidth < minWidth then set newWidth to minWidth end'
|
|
2569
|
+
);
|
|
2570
|
+
expect(out).toMatch(/man churanay tukuy\s*$/);
|
|
2571
|
+
});
|
|
2572
|
+
|
|
2573
|
+
it('[tr] trailing-end set fragment renders verb-final with trailing terminator', () => {
|
|
2574
|
+
// Previously the set-to rule's end-guard predicate skipped verb-final
|
|
2575
|
+
// reorder for these fragments; with the terminator stripped first, the
|
|
2576
|
+
// rule applies and the terminator follows the verb.
|
|
2577
|
+
const out = new GrammarTransformer('en', 'tr').transform('set dragClass to "sorting" end');
|
|
2578
|
+
expect(out.trim()).toBe('dragClass i "sorting" e ayarla son');
|
|
2579
|
+
});
|
|
2580
|
+
|
|
2581
|
+
const draggableWait =
|
|
2582
|
+
'wait for pointermove(clientX, clientY) or pointerup(clientX, clientY) from document';
|
|
2583
|
+
it('[tr] draggable-shaped wait (clientX, clientY) is verb-final', () => {
|
|
2584
|
+
const out = new GrammarTransformer('en', 'tr').transform(draggableWait);
|
|
2585
|
+
expect(out.trim().endsWith('bekle')).toBe(true);
|
|
2586
|
+
expect(out.indexOf('belge den')).toBeLessThan(out.indexOf('bekle'));
|
|
2587
|
+
});
|
|
2588
|
+
it('[qu] draggable-shaped wait (clientX, clientY) is verb-final', () => {
|
|
2589
|
+
const out = new GrammarTransformer('en', 'qu').transform(draggableWait);
|
|
2590
|
+
expect(out.trim().endsWith('suyay')).toBe(true);
|
|
2591
|
+
expect(out.indexOf('qillqa manta')).toBeLessThan(out.indexOf('pointermove'));
|
|
2592
|
+
});
|
|
2593
|
+
});
|
|
2594
|
+
|
|
2595
|
+
// The form-submit-prevent head fuses TWO juxtaposed commands (`halt the event
|
|
2596
|
+
// call validateForm()`); parseEventHandler sweeps both into one patient blob
|
|
2597
|
+
// and the SOV patient-first reorder fronted it whole — halt's operand stranded
|
|
2598
|
+
// clause-initial, call's operand riding with it (tr bound validateForm() to
|
|
2599
|
+
// halt and the event to call). sovHaltCallFusedRule splits the blob at the
|
|
2600
|
+
// language's call verb and emits both commands verb-final, joined by the
|
|
2601
|
+
// then-connective (R1 deferred-tail Family H,
|
|
2602
|
+
// docs-internal/HANDOFF_r1-deferred-tail.md).
|
|
2603
|
+
describe('SOV fused halt+call heads render both commands verb-final (Family H)', () => {
|
|
2604
|
+
const src =
|
|
2605
|
+
'on submit halt the event call validateForm() if result is false log "Invalid form" end';
|
|
2606
|
+
const EXPECT: Array<[string, string]> = [
|
|
2607
|
+
['ja', 'the イベント を 送信 で 停止 それから validateForm() を 呼び出し'],
|
|
2608
|
+
['ko', 'the 이벤트 를 제출 할 때 정지 그러면 validateForm() 를 호출'],
|
|
2609
|
+
// Batch 3: tr submit event word gönder→gönderme (gönder is the send verb — captured event "send")
|
|
2610
|
+
['tr', 'the olay i gönderme de durdur ardından validateForm() i çağır'],
|
|
2611
|
+
['bn', 'the ঘটনা কে জমা এ থামুন তারপর validateForm() কে কল'],
|
|
2612
|
+
['hi', 'the घटना को जमा पर रोकें फिर validateForm() को कॉल'],
|
|
2613
|
+
// Batch 3: qu submit event word kachay→apaykachay (kachay is the send verb — captured "send" in this slot)
|
|
2614
|
+
['qu', 'the ruway ta apaykachay pi sayay chayqa validateForm() ta qayay'],
|
|
2615
|
+
];
|
|
2616
|
+
for (const [lang, head] of EXPECT) {
|
|
2617
|
+
it(`[${lang}] halt keeps its operand adjacent + verb-final; call follows the connective`, () => {
|
|
2618
|
+
const out = new GrammarTransformer('en', lang).transform(src);
|
|
2619
|
+
expect(out.startsWith(head)).toBe(true);
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
it('[ja] a plain `halt the event` handler keeps its default render (rule stands down)', () => {
|
|
2624
|
+
const out = new GrammarTransformer('en', 'ja').transform('on submit halt the event');
|
|
2625
|
+
expect(out.trim()).toBe('the イベント を 送信 で 停止');
|
|
2626
|
+
});
|
|
2627
|
+
|
|
2628
|
+
it('[tr] a call-only handler keeps its default render (no halt — rule stands down)', () => {
|
|
2629
|
+
const out = new GrammarTransformer('en', 'tr').transform('on click call myFunction()');
|
|
2630
|
+
expect(out.trim()).toBe('myFunction() i tıklama de çağır');
|
|
2631
|
+
});
|
|
2632
|
+
});
|
|
2633
|
+
|
|
2634
|
+
// SOV renders emit nothing between an if-condition's tail and a POSITIONAL-
|
|
2635
|
+
// headed branch operand (`… .modal 最初 <button/> …`), and the semantic fold's
|
|
2636
|
+
// command-start detection can't see that shape — the condition swallowed the
|
|
2637
|
+
// focus operand's head (focus-trap, ja/ko/qu). transformBlockBody now emits
|
|
2638
|
+
// the target's then-connective at the clause/body seam, gated to exactly the
|
|
2639
|
+
// blind shape (R1 deferred-tail Family G).
|
|
2640
|
+
describe('SOV if-blocks get a then-connective before a positional branch head (Family G)', () => {
|
|
2641
|
+
const src =
|
|
2642
|
+
'on keydown[key=="Tab"] from .modal if target matches last <button/> in .modal focus first <button/> in .modal halt end';
|
|
2643
|
+
const SEAM: Array<[string, string]> = [
|
|
2644
|
+
['ja', '.modal それから 最初'],
|
|
2645
|
+
['ko', '.modal 그러면 첫번째'],
|
|
2646
|
+
['qu', '.modal chayqa ñawpaq'],
|
|
2647
|
+
['tr', '.modal ardından ilk'],
|
|
2648
|
+
['bn', '.modal তারপর প্রথম'],
|
|
2649
|
+
['hi', '.modal फिर पहला'],
|
|
2650
|
+
];
|
|
2651
|
+
for (const [lang, seam] of SEAM) {
|
|
2652
|
+
it(`[${lang}] focus-trap renders the connective at the condition/branch seam`, () => {
|
|
2653
|
+
const out = new GrammarTransformer('en', lang).transform(src);
|
|
2654
|
+
expect(out).toContain(seam);
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
it('[qu] a non-positional branch head gains no connective (form-submit if unchanged)', () => {
|
|
2659
|
+
const out = new GrammarTransformer('en', 'qu').transform(
|
|
2660
|
+
'on submit halt the event call validateForm() if result is false log "Invalid form" end'
|
|
2661
|
+
);
|
|
2662
|
+
expect(out).toContain('llulla "Invalid form" ta qillqakuy');
|
|
2663
|
+
});
|
|
2664
|
+
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* language-specific markers and rules.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { LanguageProfile, SemanticRole } from '../types';
|
|
9
|
+
import type { GrammarRule, LanguageProfile, SemanticRole } from '../types';
|
|
10
10
|
import { reorderRoles, insertMarkers } from '../types';
|
|
11
11
|
|
|
12
12
|
// =============================================================================
|
|
@@ -47,6 +47,167 @@ export const englishProfile: LanguageProfile = {
|
|
|
47
47
|
],
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Shared SOV rule factory — fused `halt the event call fn()` heads (Family H,
|
|
52
|
+
// docs-internal/HANDOFF_r1-deferred-tail.md)
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The form-submit-prevent head fuses TWO juxtaposed commands (`on submit halt
|
|
57
|
+
* the event call validateForm()`): parseEventHandler reads `halt` as the
|
|
58
|
+
* action and sweeps `the event call validateForm()` into ONE patient blob, so
|
|
59
|
+
* the SOV patient-first reorder fronts the whole blob — halt's operand lands
|
|
60
|
+
* clause-initial far from its verb, and call's operand rides inside the blob
|
|
61
|
+
* (tr bound validateForm() to halt and the event to call). No roleOrder can
|
|
62
|
+
* fix a single-blob patient; this custom render splits the blob at the
|
|
63
|
+
* language's call verb and emits both commands verb-final, joined by the
|
|
64
|
+
* then-connective (the boundary the semantic compound-splitter already
|
|
65
|
+
* respects — the event-debounce geometry):
|
|
66
|
+
*
|
|
67
|
+
* `<halt-operand> <pm> <event> <em> <halt-verb> <conn> <call-args> <pm> <call-verb>`
|
|
68
|
+
*
|
|
69
|
+
* The trailing EVENTBLOCKPLACEHOLDER (tryTransformEventWithBlockBody's mask
|
|
70
|
+
* for an `if … end` tail) is re-emitted clause-final so the caller's
|
|
71
|
+
* strip-and-append keeps working.
|
|
72
|
+
*/
|
|
73
|
+
function sovHaltCallFusedRule(cfg: {
|
|
74
|
+
callVerb: string; // the dictionary's call rendering — the blob split point
|
|
75
|
+
patientMarker: string;
|
|
76
|
+
eventMarker: string;
|
|
77
|
+
connective: string;
|
|
78
|
+
}): GrammarRule {
|
|
79
|
+
return {
|
|
80
|
+
name: 'halt-call-fused-verb-final',
|
|
81
|
+
description: 'Split the swept `halt <operand> call <args>` blob; both verbs clause-final',
|
|
82
|
+
priority: 95,
|
|
83
|
+
match: {
|
|
84
|
+
commands: ['halt'],
|
|
85
|
+
requiredRoles: ['action', 'event', 'patient'],
|
|
86
|
+
// Only the juxtaposed-call blob: a plain `halt`/`halt the event` handler
|
|
87
|
+
// keeps its default render. The translated call verb must be present to
|
|
88
|
+
// split on — if the dictionary emission drifts, the rule stands down
|
|
89
|
+
// rather than emitting a mangled clause.
|
|
90
|
+
predicate: parsed => {
|
|
91
|
+
const p = parsed.roles.get('patient');
|
|
92
|
+
return (
|
|
93
|
+
!!p && /\bcall\b/.test(p.value) && !!p.translated && p.translated.includes(cfg.callVerb)
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
transform: {
|
|
98
|
+
roleOrder: [], // unused — custom takes over
|
|
99
|
+
custom: parsed => {
|
|
100
|
+
const p = parsed.roles.get('patient')!;
|
|
101
|
+
const ev = parsed.roles.get('event')!;
|
|
102
|
+
const act = parsed.roles.get('action')!;
|
|
103
|
+
let blob = (p.translated ?? p.value).trim();
|
|
104
|
+
const PH = 'EVENTBLOCKPLACEHOLDER';
|
|
105
|
+
let tail = '';
|
|
106
|
+
if (blob.endsWith(PH)) {
|
|
107
|
+
blob = blob.slice(0, -PH.length).trim();
|
|
108
|
+
tail = ` ${PH}`;
|
|
109
|
+
}
|
|
110
|
+
const at = blob.indexOf(cfg.callVerb);
|
|
111
|
+
const haltOperand = blob.slice(0, at).trim();
|
|
112
|
+
const callArgs = blob.slice(at + cfg.callVerb.length).trim();
|
|
113
|
+
return (
|
|
114
|
+
[
|
|
115
|
+
haltOperand,
|
|
116
|
+
cfg.patientMarker,
|
|
117
|
+
ev.translated ?? ev.value,
|
|
118
|
+
cfg.eventMarker,
|
|
119
|
+
act.translated ?? act.value,
|
|
120
|
+
cfg.connective,
|
|
121
|
+
callArgs,
|
|
122
|
+
cfg.patientMarker,
|
|
123
|
+
cfg.callVerb,
|
|
124
|
+
].join(' ') + tail
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// Shared SOV rule factory — pick range rows (`pick characters 0 to 5 of #note`,
|
|
133
|
+
// docs-internal/HANDOFF_pick-text-range-arc3.md)
|
|
134
|
+
// =============================================================================
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The canonical pick range row decomposes in parseStatement/parseEventHandler as
|
|
138
|
+
* patient=`characters 0` + destination=`5 of #note` (the range's `to` reads as a
|
|
139
|
+
* destination marker), so the default SOV reorder scatters the range across the
|
|
140
|
+
* clause — ja emitted `文字 0 を クリック で 選択 5 の #note に` with the `5`
|
|
141
|
+
* stranded after the verb, unparseable in every SOV language. No roleOrder can
|
|
142
|
+
* reunite a range split across two roles; this custom render reassembles the
|
|
143
|
+
* canonical surface and emits the SOV convention shape (big patient phrase
|
|
144
|
+
* first, verb final):
|
|
145
|
+
*
|
|
146
|
+
* `<root> <gen> <unit> <a> <sep> <b> <pm> [<event> <em>] <verb>`
|
|
147
|
+
*
|
|
148
|
+
* e.g. ja `#note の 文字 0 から 5 を クリック で 選択`. The paired semantic
|
|
149
|
+
* pick pattern (patterns/pick.ts VERB_FINAL_SPECS) parses exactly this shape
|
|
150
|
+
* back, with the range fold normalizing `<a> <sep> <b>` to English `a to b` —
|
|
151
|
+
* the two sides co-evolve per the i18n-renders-semantic-patterns-coevolve
|
|
152
|
+
* lesson; change one and you must re-probe the other.
|
|
153
|
+
*
|
|
154
|
+
* Gated tight to the numeric-endpoint corpus shape (`<unit> <int>` patient +
|
|
155
|
+
* `<int> of <root>` destination); word endpoints (`start`/`end`), modes
|
|
156
|
+
* (`inclusive`) and anything else fall through to the default render unchanged.
|
|
157
|
+
*/
|
|
158
|
+
function sovPickRangeRule(cfg: {
|
|
159
|
+
/** Genitive joiner between the root and the unit word (ja `の`, hi `का`). */
|
|
160
|
+
genitive: string;
|
|
161
|
+
/** Range separator between the two endpoints (ja `から`, hi `से`). */
|
|
162
|
+
rangeSep: string;
|
|
163
|
+
/** Patient/object marker after the range (ja `を`, hi `को`). */
|
|
164
|
+
patientMarker: string;
|
|
165
|
+
/** Event marker after the event word (ja `で`, hi `पर`). */
|
|
166
|
+
eventMarker: string;
|
|
167
|
+
}): GrammarRule {
|
|
168
|
+
return {
|
|
169
|
+
name: 'pick-range-verb-final',
|
|
170
|
+
description: 'Reassemble the pick range (unit + endpoints + root) adjacent, verb-final',
|
|
171
|
+
priority: 95,
|
|
172
|
+
match: {
|
|
173
|
+
commands: ['pick'],
|
|
174
|
+
requiredRoles: ['action', 'patient', 'destination'],
|
|
175
|
+
predicate: parsed => {
|
|
176
|
+
const p = parsed.roles.get('patient');
|
|
177
|
+
const d = parsed.roles.get('destination');
|
|
178
|
+
return (
|
|
179
|
+
!!p &&
|
|
180
|
+
!!d &&
|
|
181
|
+
/^\S+\s+\d+$/.test(p.value.trim()) &&
|
|
182
|
+
/^\d+\s+of\s+\S+$/.test(d.value.trim())
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
transform: {
|
|
187
|
+
roleOrder: [], // unused — custom takes over
|
|
188
|
+
custom: parsed => {
|
|
189
|
+
const p = parsed.roles.get('patient')!;
|
|
190
|
+
const d = parsed.roles.get('destination')!;
|
|
191
|
+
const act = parsed.roles.get('action')!;
|
|
192
|
+
const ev = parsed.roles.get('event');
|
|
193
|
+
// Unit word from the TRANSLATED patient (`文字 0` → `文字`); endpoints
|
|
194
|
+
// and root from the UNTRANSLATED values — integers and selectors are
|
|
195
|
+
// language-invariant by construction (the R3 rule).
|
|
196
|
+
const pParts = (p.translated ?? p.value).trim().split(/\s+/);
|
|
197
|
+
const a = pParts[pParts.length - 1]!;
|
|
198
|
+
const unit = pParts.slice(0, -1).join(' ');
|
|
199
|
+
const dMatch = d.value.trim().match(/^(\d+)\s+of\s+(\S+)$/)!;
|
|
200
|
+
const b = dMatch[1]!;
|
|
201
|
+
const root = dMatch[2]!;
|
|
202
|
+
const verb = act.translated ?? act.value;
|
|
203
|
+
const head = [root, cfg.genitive, unit, a, cfg.rangeSep, b, cfg.patientMarker];
|
|
204
|
+
const eventPart = ev ? [ev.translated ?? ev.value, cfg.eventMarker] : [];
|
|
205
|
+
return [...head, ...eventPart, verb].join(' ');
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
50
211
|
// =============================================================================
|
|
51
212
|
// Japanese (SOV, Postpositions)
|
|
52
213
|
// =============================================================================
|
|
@@ -140,6 +301,20 @@ export const japaneseProfile: LanguageProfile = {
|
|
|
140
301
|
insertMarkers: true,
|
|
141
302
|
},
|
|
142
303
|
},
|
|
304
|
+
// the イベント を 送信 で 停止 それから validateForm() を 呼び出し
|
|
305
|
+
sovHaltCallFusedRule({
|
|
306
|
+
callVerb: '呼び出し',
|
|
307
|
+
patientMarker: 'を',
|
|
308
|
+
eventMarker: 'で',
|
|
309
|
+
connective: 'それから',
|
|
310
|
+
}),
|
|
311
|
+
// #note の 文字 0 から 5 を クリック で 選択
|
|
312
|
+
sovPickRangeRule({
|
|
313
|
+
genitive: 'の',
|
|
314
|
+
rangeSep: 'から',
|
|
315
|
+
patientMarker: 'を',
|
|
316
|
+
eventMarker: 'で',
|
|
317
|
+
}),
|
|
143
318
|
],
|
|
144
319
|
};
|
|
145
320
|
|
|
@@ -248,6 +423,20 @@ export const koreanProfile: LanguageProfile = {
|
|
|
248
423
|
insertMarkers: true,
|
|
249
424
|
},
|
|
250
425
|
},
|
|
426
|
+
// the 이벤트 를 제출 할 때 정지 그러면 validateForm() 를 호출
|
|
427
|
+
sovHaltCallFusedRule({
|
|
428
|
+
callVerb: '호출',
|
|
429
|
+
patientMarker: '를',
|
|
430
|
+
eventMarker: '할 때',
|
|
431
|
+
connective: '그러면',
|
|
432
|
+
}),
|
|
433
|
+
// #note 의 문자 0 부터 5 를 클릭 할 때 선택
|
|
434
|
+
sovPickRangeRule({
|
|
435
|
+
genitive: '의',
|
|
436
|
+
rangeSep: '부터',
|
|
437
|
+
patientMarker: '를',
|
|
438
|
+
eventMarker: '할 때',
|
|
439
|
+
}),
|
|
251
440
|
],
|
|
252
441
|
};
|
|
253
442
|
|
|
@@ -420,6 +609,12 @@ export const turkishProfile: LanguageProfile = {
|
|
|
420
609
|
morphology: 'agglutinative',
|
|
421
610
|
direction: 'ltr',
|
|
422
611
|
|
|
612
|
+
// NOTE: roles absent from this order are safety-net-appended AFTER the verb
|
|
613
|
+
// by reorderRoles. That stranding is what the semantic parser's generated
|
|
614
|
+
// patterns + reclaim tolerances were built around, so extending this order
|
|
615
|
+
// wholesale re-renders (and breaks) the whole tr corpus — fix individual
|
|
616
|
+
// defective shapes with per-command `rules` below instead (see the
|
|
617
|
+
// wait/repeat-until rules from the transformer-rendering arc).
|
|
423
618
|
canonicalOrder: ['patient', 'event', 'action'],
|
|
424
619
|
|
|
425
620
|
markers: [
|
|
@@ -516,6 +711,58 @@ export const turkishProfile: LanguageProfile = {
|
|
|
516
711
|
insertMarkers: true,
|
|
517
712
|
},
|
|
518
713
|
},
|
|
714
|
+
{
|
|
715
|
+
name: 'wait-oblique-verb-final',
|
|
716
|
+
description: "Keep wait's from-phrase and or-run inside the clause, verb-final",
|
|
717
|
+
priority: 90,
|
|
718
|
+
match: {
|
|
719
|
+
commands: ['wait', 'bekle'],
|
|
720
|
+
// Only the multi-argument form (`wait for X or Y from Z`) strands:
|
|
721
|
+
// without this rule the safety-net appends duration+source AFTER the
|
|
722
|
+
// verb (`bekle … belge den`), and the stranded from-phrase is captured
|
|
723
|
+
// by the NEXT statement as a junk trigger event (behavior-sortable R3).
|
|
724
|
+
requiredRoles: ['action', 'duration', 'source'],
|
|
725
|
+
// Never reorder a fused single-line handler — its event must stay put.
|
|
726
|
+
predicate: parsed => !parsed.roles.has('event'),
|
|
727
|
+
},
|
|
728
|
+
transform: {
|
|
729
|
+
// belge den pointermove(clientY) veya pointerup(clientY) bekle
|
|
730
|
+
roleOrder: ['patient', 'source', 'duration', 'action'],
|
|
731
|
+
insertMarkers: true,
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: 'repeat-until-event-verb-final',
|
|
736
|
+
description: "Keep repeat-until-event's from-phrase before the verb",
|
|
737
|
+
priority: 90,
|
|
738
|
+
match: {
|
|
739
|
+
commands: ['repeat', 'tekrarla'],
|
|
740
|
+
requiredRoles: ['action', 'patient', 'source'],
|
|
741
|
+
// Gate to the until-event head; `repeat for item in .items` also
|
|
742
|
+
// carries patient+source and must keep its (parseable) default order.
|
|
743
|
+
predicate: parsed => /^repeat\s+until\s+event\b/i.test((parsed.original ?? '').trim()),
|
|
744
|
+
},
|
|
745
|
+
transform: {
|
|
746
|
+
// kadar olay pointerup i belge den tekrarla — matched semantic-side by
|
|
747
|
+
// repeat-tr-until-head-verb-final.
|
|
748
|
+
roleOrder: ['patient', 'source', 'action'],
|
|
749
|
+
insertMarkers: true,
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
// the olay i gönder de durdur ardından validateForm() i çağır
|
|
753
|
+
sovHaltCallFusedRule({
|
|
754
|
+
callVerb: 'çağır',
|
|
755
|
+
patientMarker: 'i',
|
|
756
|
+
eventMarker: 'de',
|
|
757
|
+
connective: 'ardından',
|
|
758
|
+
}),
|
|
759
|
+
// #note nin karakterler 0 ile 5 i tıklama de seç
|
|
760
|
+
sovPickRangeRule({
|
|
761
|
+
genitive: 'nin',
|
|
762
|
+
rangeSep: 'ile',
|
|
763
|
+
patientMarker: 'i',
|
|
764
|
+
eventMarker: 'de',
|
|
765
|
+
}),
|
|
519
766
|
],
|
|
520
767
|
};
|
|
521
768
|
|
|
@@ -692,6 +939,8 @@ export const quechuaProfile: LanguageProfile = {
|
|
|
692
939
|
morphology: 'agglutinative', // Actually polysynthetic
|
|
693
940
|
direction: 'ltr',
|
|
694
941
|
|
|
942
|
+
// NOTE: roles absent from this order are safety-net-appended AFTER the verb
|
|
943
|
+
// (see the Turkish profile note) — extend via per-command `rules`, not here.
|
|
695
944
|
canonicalOrder: ['patient', 'source', 'destination', 'event', 'action'],
|
|
696
945
|
|
|
697
946
|
markers: [
|
|
@@ -705,6 +954,42 @@ export const quechuaProfile: LanguageProfile = {
|
|
|
705
954
|
{ form: 'wan', role: 'style', position: 'postposition', required: false },
|
|
706
955
|
{ form: 'hina', role: 'method', position: 'postposition', required: false }, // "as/like"
|
|
707
956
|
],
|
|
957
|
+
|
|
958
|
+
rules: [
|
|
959
|
+
{
|
|
960
|
+
name: 'wait-oblique-verb-final',
|
|
961
|
+
description: "Keep wait's from-phrase and or-run inside the clause, verb-final",
|
|
962
|
+
priority: 90,
|
|
963
|
+
match: {
|
|
964
|
+
commands: ['wait', 'suyay'],
|
|
965
|
+
// Only `wait for X or Y from Z` strands: duration is missing from
|
|
966
|
+
// canonicalOrder, so the or-run lands AFTER the verb and the next
|
|
967
|
+
// trigger glues it into its event (behavior-sortable R3). See the
|
|
968
|
+
// matching Turkish rule.
|
|
969
|
+
requiredRoles: ['action', 'duration', 'source'],
|
|
970
|
+
predicate: parsed => !parsed.roles.has('event'),
|
|
971
|
+
},
|
|
972
|
+
transform: {
|
|
973
|
+
// qillqa manta pointermove(clientY) utaq pointerup(clientY) suyay
|
|
974
|
+
roleOrder: ['patient', 'source', 'duration', 'action'],
|
|
975
|
+
insertMarkers: true,
|
|
976
|
+
},
|
|
977
|
+
},
|
|
978
|
+
// the ruway ta kachay pi sayay chayqa validateForm() ta qayay
|
|
979
|
+
sovHaltCallFusedRule({
|
|
980
|
+
callVerb: 'qayay',
|
|
981
|
+
patientMarker: 'ta',
|
|
982
|
+
eventMarker: 'pi',
|
|
983
|
+
connective: 'chayqa',
|
|
984
|
+
}),
|
|
985
|
+
// #note pa sanampa 0 kama 5 ta ñitiy pi akllay
|
|
986
|
+
sovPickRangeRule({
|
|
987
|
+
genitive: 'pa',
|
|
988
|
+
rangeSep: 'kama',
|
|
989
|
+
patientMarker: 'ta',
|
|
990
|
+
eventMarker: 'pi',
|
|
991
|
+
}),
|
|
992
|
+
],
|
|
708
993
|
};
|
|
709
994
|
|
|
710
995
|
// =============================================================================
|
|
@@ -749,6 +1034,8 @@ export const bengaliProfile: LanguageProfile = {
|
|
|
749
1034
|
|
|
750
1035
|
// Bengali: Object comes before verb, postpositions follow nouns
|
|
751
1036
|
// "on click increment #count" → "#count কে ক্লিক এ বৃদ্ধি"
|
|
1037
|
+
// NOTE: roles absent from this order are safety-net-appended AFTER the verb
|
|
1038
|
+
// (see the Turkish profile note) — extend via per-command `rules`, not here.
|
|
752
1039
|
canonicalOrder: ['patient', 'event', 'action'],
|
|
753
1040
|
|
|
754
1041
|
markers: [
|
|
@@ -827,6 +1114,20 @@ export const bengaliProfile: LanguageProfile = {
|
|
|
827
1114
|
insertMarkers: true,
|
|
828
1115
|
},
|
|
829
1116
|
},
|
|
1117
|
+
// the ঘটনা কে জমা এ থামুন তারপর validateForm() কে কল
|
|
1118
|
+
sovHaltCallFusedRule({
|
|
1119
|
+
callVerb: 'কল',
|
|
1120
|
+
patientMarker: 'কে',
|
|
1121
|
+
eventMarker: 'এ',
|
|
1122
|
+
connective: 'তারপর',
|
|
1123
|
+
}),
|
|
1124
|
+
// #note র অক্ষর 0 থেকে 5 কে ক্লিক এ বাছুন
|
|
1125
|
+
sovPickRangeRule({
|
|
1126
|
+
genitive: 'র',
|
|
1127
|
+
rangeSep: 'থেকে',
|
|
1128
|
+
patientMarker: 'কে',
|
|
1129
|
+
eventMarker: 'এ',
|
|
1130
|
+
}),
|
|
830
1131
|
],
|
|
831
1132
|
};
|
|
832
1133
|
|
|
@@ -1040,6 +1341,20 @@ export const hindiProfile: LanguageProfile = {
|
|
|
1040
1341
|
insertMarkers: true,
|
|
1041
1342
|
},
|
|
1042
1343
|
},
|
|
1344
|
+
// the घटना को जमा पर रोकें फिर validateForm() को कॉल
|
|
1345
|
+
sovHaltCallFusedRule({
|
|
1346
|
+
callVerb: 'कॉल',
|
|
1347
|
+
patientMarker: 'को',
|
|
1348
|
+
eventMarker: 'पर',
|
|
1349
|
+
connective: 'फिर',
|
|
1350
|
+
}),
|
|
1351
|
+
// #note का अक्षर 0 से 5 को क्लिक पर चुनें
|
|
1352
|
+
sovPickRangeRule({
|
|
1353
|
+
genitive: 'का',
|
|
1354
|
+
rangeSep: 'से',
|
|
1355
|
+
patientMarker: 'को',
|
|
1356
|
+
eventMarker: 'पर',
|
|
1357
|
+
}),
|
|
1043
1358
|
],
|
|
1044
1359
|
};
|
|
1045
1360
|
|
|
@@ -1901,6 +1901,18 @@ export class GrammarTransformer {
|
|
|
1901
1901
|
return this.transformBlock(block);
|
|
1902
1902
|
}
|
|
1903
1903
|
|
|
1904
|
+
// 0a. Fragment carrying a stranded trailing block terminator (`wait 200ms
|
|
1905
|
+
// end`, `set x to y end` — what the `then`-splitter leaves from
|
|
1906
|
+
// `if … then <cmd> end`). `end` is not a marker, so left in place it is
|
|
1907
|
+
// swept into the open role's VALUE and rendered inside that phrase
|
|
1908
|
+
// (bn `200ms শেষ কে অপেক্ষা`). Strip it, transform the clause alone, and
|
|
1909
|
+
// re-append the translated terminator after the verb — the same tail
|
|
1910
|
+
// position transformBlockBody emits for event-headed blocks.
|
|
1911
|
+
const strippedEnd = this.transformWithTrailingEnd(input);
|
|
1912
|
+
if (strippedEnd !== null) {
|
|
1913
|
+
return strippedEnd;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1904
1916
|
// 0b. `set @attr to V on <scope>` (S1 tabs-aria): strip the trailing
|
|
1905
1917
|
// `on <scope>`, transform the scope-less set normally, then re-insert
|
|
1906
1918
|
// `on <scope>` where the semantic set patterns expect it.
|
|
@@ -1951,6 +1963,36 @@ export class GrammarTransformer {
|
|
|
1951
1963
|
return joinTokens(reordered.map(e => e.translated || e.value));
|
|
1952
1964
|
}
|
|
1953
1965
|
|
|
1966
|
+
/**
|
|
1967
|
+
* `<command …> end` fragments: transform the command without its stranded
|
|
1968
|
+
* terminator, then re-append the translated terminator as a standalone
|
|
1969
|
+
* trailing token. Fragments that open a block of their own (`if … end`,
|
|
1970
|
+
* `repeat … end`, `js … end`) bail — their terminator belongs to them and
|
|
1971
|
+
* their dedicated paths handle it.
|
|
1972
|
+
*/
|
|
1973
|
+
private transformWithTrailingEnd(input: string): string | null {
|
|
1974
|
+
const src = this.sourceProfile.code;
|
|
1975
|
+
const tokens = input.trim().split(/\s+/);
|
|
1976
|
+
if (tokens.length < 2) {
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
const sourceEnd = translateWord('end', 'en', src).toLowerCase();
|
|
1980
|
+
if (tokens[tokens.length - 1].toLowerCase() !== sourceEnd) {
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
const openers = new Set(
|
|
1984
|
+
['if', 'repeat', 'unless', 'while', 'when', 'live', 'js'].map(k =>
|
|
1985
|
+
translateWord(k, 'en', src).toLowerCase()
|
|
1986
|
+
)
|
|
1987
|
+
);
|
|
1988
|
+
if (tokens.slice(0, -1).some(t => openers.has(t.toLowerCase()))) {
|
|
1989
|
+
return null;
|
|
1990
|
+
}
|
|
1991
|
+
const inner = this.transformSingle(tokens.slice(0, -1).join(' '));
|
|
1992
|
+
const endT = translateWord(tokens[tokens.length - 1], src, this.targetProfile.code);
|
|
1993
|
+
return `${inner} ${endT}`;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1954
1996
|
/**
|
|
1955
1997
|
* Detect and transform an inline JS block (`[on <event>] js <raw js> end`).
|
|
1956
1998
|
*
|
|
@@ -2327,7 +2369,28 @@ export class GrammarTransformer {
|
|
|
2327
2369
|
const clauseT = clause ? translateMultiWordValue(clause, src, dst) : '';
|
|
2328
2370
|
const bodyT = this.transformConditionalBody(bodyTokens);
|
|
2329
2371
|
|
|
2330
|
-
|
|
2372
|
+
// SOV condition/branch boundary (R1 deferred-tail Family G): the SOV body
|
|
2373
|
+
// renders its first command operand-first (`最初 <button/> の中 .modal を
|
|
2374
|
+
// フォーカス`), so nothing marks where the condition ends and the branch
|
|
2375
|
+
// operand begins — the semantic fold's command-start detection needs a
|
|
2376
|
+
// `{value}{particle}` run followed by a verb, which a POSITIONAL-headed
|
|
2377
|
+
// operand (`first <button/> …`) never forms, and the condition scan
|
|
2378
|
+
// swallows the operand's head (focus-trap: ja focus.patient fell to the
|
|
2379
|
+
// `me` default, ko/qu to the `.modal` tail). Emit the target's
|
|
2380
|
+
// then-connective at the seam — the boundary the fold already respects
|
|
2381
|
+
// (isThenKeyword) — gated to exactly the blind shape: SOV target, a
|
|
2382
|
+
// positional keyword right after the branch's command verb, and no `then`
|
|
2383
|
+
// already ending the condition.
|
|
2384
|
+
const POSITIONAL_BRANCH_HEADS = new Set(['first', 'last', 'next', 'previous', 'closest']);
|
|
2385
|
+
const thenT =
|
|
2386
|
+
this.targetProfile.wordOrder === 'SOV' &&
|
|
2387
|
+
clauseT &&
|
|
2388
|
+
POSITIONAL_BRANCH_HEADS.has(bodyTokens[1]?.toLowerCase()) &&
|
|
2389
|
+
inner[bodyStart - 1]?.toLowerCase() !== 'then'
|
|
2390
|
+
? translateWord('then', src, dst)
|
|
2391
|
+
: '';
|
|
2392
|
+
|
|
2393
|
+
return [headT, clauseT, thenT, bodyT, tailT].filter(s => s.length > 0).join(' ');
|
|
2331
2394
|
}
|
|
2332
2395
|
|
|
2333
2396
|
/**
|
|
@@ -47,7 +47,9 @@ describe('New Language Support', () => {
|
|
|
47
47
|
|
|
48
48
|
it('should handle events properly', () => {
|
|
49
49
|
const result = translator.translate('on mouseenter add .hover', { from: 'en', to: 'tr' });
|
|
50
|
-
|
|
50
|
+
// V3c burn-down 2026-07-14: fare_gir never round-tripped on the parse
|
|
51
|
+
// side (broken listener); mouseenter is English passthrough now.
|
|
52
|
+
expect(result).toContain('mouseenter');
|
|
51
53
|
expect(result).toContain('ekle');
|
|
52
54
|
});
|
|
53
55
|
|
|
@@ -198,8 +200,12 @@ describe('New Language Support', () => {
|
|
|
198
200
|
|
|
199
201
|
it('should handle double-click events', () => {
|
|
200
202
|
const result = translator.translate('on dblclick focus on me', { from: 'en', to: 'sw' });
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
// V3c burn-down 2026-07-14: bonyeza_mara_mbili never round-tripped on
|
|
204
|
+
// the parse side (broken listener); dblclick is English passthrough now.
|
|
205
|
+
expect(result).toContain('dblclick');
|
|
206
|
+
// V3 Batch 2: events.focus realigned zingatia → lenga (zingatia never
|
|
207
|
+
// tokenized as an event — matches commands.focus and the S5b table)
|
|
208
|
+
expect(result).toContain('lenga'); // focus → lenga
|
|
203
209
|
});
|
|
204
210
|
|
|
205
211
|
it('should translate navigation commands', () => {
|