@lokascript/i18n 2.5.1 → 2.7.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 (73) hide show
  1. package/dist/browser.cjs +1274 -154
  2. package/dist/browser.cjs.map +1 -1
  3. package/dist/browser.d.cts +3 -3
  4. package/dist/browser.d.ts +3 -3
  5. package/dist/browser.js +1274 -155
  6. package/dist/browser.js.map +1 -1
  7. package/dist/dictionaries/index.cjs +364 -133
  8. package/dist/dictionaries/index.cjs.map +1 -1
  9. package/dist/dictionaries/index.d.cts +1 -1
  10. package/dist/dictionaries/index.d.ts +1 -1
  11. package/dist/dictionaries/index.js +364 -133
  12. package/dist/dictionaries/index.js.map +1 -1
  13. package/dist/index.cjs +8860 -7740
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +3 -3
  16. package/dist/index.d.ts +3 -3
  17. package/dist/index.js +8860 -7741
  18. package/dist/index.js.map +1 -1
  19. package/dist/lokascript-i18n.min.js +1 -1
  20. package/dist/lokascript-i18n.min.js.map +1 -1
  21. package/dist/lokascript-i18n.mjs +1697 -146
  22. package/dist/lokascript-i18n.mjs.map +1 -1
  23. package/dist/plugins/vite.cjs +359 -132
  24. package/dist/plugins/vite.cjs.map +1 -1
  25. package/dist/plugins/vite.js +359 -132
  26. package/dist/plugins/vite.js.map +1 -1
  27. package/dist/plugins/webpack.cjs +359 -132
  28. package/dist/plugins/webpack.cjs.map +1 -1
  29. package/dist/plugins/webpack.js +359 -132
  30. package/dist/plugins/webpack.js.map +1 -1
  31. package/dist/{transformer-BLv389qz.d.ts → transformer-BA8YX9u1.d.ts} +120 -2
  32. package/dist/{transformer-PFsYELrc.d.cts → transformer-DM7iplCU.d.cts} +120 -2
  33. package/dist/{types-BcALAO6h.d.cts → types-BYtpqGq3.d.cts} +1 -1
  34. package/dist/{types-BcALAO6h.d.ts → types-BYtpqGq3.d.ts} +1 -1
  35. package/package.json +9 -6
  36. package/src/browser.ts +1 -0
  37. package/src/command-primary-roles.test.ts +59 -0
  38. package/src/constants.ts +51 -0
  39. package/src/dictionaries/ar.ts +8 -3
  40. package/src/dictionaries/bn.ts +3 -1
  41. package/src/dictionaries/de.ts +19 -4
  42. package/src/dictionaries/derive.ts +4 -0
  43. package/src/dictionaries/en.ts +9 -0
  44. package/src/dictionaries/es.ts +2 -2
  45. package/src/dictionaries/fr.ts +6 -2
  46. package/src/dictionaries/he.ts +7 -1
  47. package/src/dictionaries/hi.ts +17 -10
  48. package/src/dictionaries/id.ts +15 -8
  49. package/src/dictionaries/it.ts +6 -3
  50. package/src/dictionaries/ja.ts +10 -4
  51. package/src/dictionaries/ko.ts +9 -4
  52. package/src/dictionaries/ms.ts +2 -1
  53. package/src/dictionaries/pl.ts +11 -3
  54. package/src/dictionaries/pt.ts +10 -4
  55. package/src/dictionaries/qu.ts +46 -22
  56. package/src/dictionaries/ru.ts +14 -7
  57. package/src/dictionaries/sw.ts +37 -10
  58. package/src/dictionaries/th.ts +3 -1
  59. package/src/dictionaries/tl.ts +14 -7
  60. package/src/dictionaries/tr.ts +22 -9
  61. package/src/dictionaries/uk.ts +13 -7
  62. package/src/dictionaries/vi.ts +3 -3
  63. package/src/dictionaries/zh.ts +10 -2
  64. package/src/examples/new-languages.ts +1 -1
  65. package/src/grammar/grammar.test.ts +1242 -1
  66. package/src/grammar/index.ts +1 -0
  67. package/src/grammar/profiles/index.ts +358 -12
  68. package/src/grammar/transformer.ts +1053 -16
  69. package/src/grammar/types.ts +22 -1
  70. package/src/index.ts +1 -0
  71. package/src/new-languages.test.ts +7 -3
  72. package/src/parser/parser-integration.test.ts +5 -1
  73. package/src/positional-keyword-drift.test.ts +138 -0
@@ -17,6 +17,49 @@ const ENGLISH_MODIFIER_ROLES = {
17
17
  over: 'duration',
18
18
  for: 'duration',
19
19
  };
20
+ /**
21
+ * Maps a command verb to its **primary** semantic role — the role of the
22
+ * command's first/leading argument when no explicit modifier keyword marks it.
23
+ *
24
+ * The generic argument parser in the transformer defaults the first unmarked
25
+ * argument to `patient`, which is correct for the majority of commands
26
+ * (`toggle .x`, `add .x`, …) but wrong for commands whose leading argument is a
27
+ * non-patient — e.g. `wait <duration>`. Marking a duration as a patient emits a
28
+ * spurious object-marker in the target language (Chinese `等待 把 1s`, ungrammatical;
29
+ * Japanese `1s を 待つ`; Korean `1s 를 대기`), which the semantic parser then fails to
30
+ * match — dropping the command.
31
+ *
32
+ * Only commands whose primary role is **not** `patient` are listed here (the
33
+ * `patient` default already covers the rest). Mirrors the `primaryRole` field of
34
+ * the semantic package's command schemas (`@lokascript/semantic`); kept in sync by
35
+ * `schema-alignment.test.ts` so this local copy can't drift without the bundle
36
+ * having to pull in the whole semantic graph.
37
+ *
38
+ * @see packages/i18n/src/schema-alignment.test.ts
39
+ * @see docs-internal/ZH_BLOCK_BODY_SCOPE.md (#1 — transformer role model)
40
+ */
41
+ const COMMAND_PRIMARY_ROLES = {
42
+ set: 'destination',
43
+ on: 'event',
44
+ trigger: 'event',
45
+ send: 'event',
46
+ wait: 'duration',
47
+ fetch: 'source',
48
+ get: 'source',
49
+ if: 'condition',
50
+ unless: 'condition',
51
+ while: 'condition',
52
+ repeat: 'loopType',
53
+ go: 'destination',
54
+ scroll: 'destination',
55
+ tell: 'destination',
56
+ default: 'destination',
57
+ swap: 'destination',
58
+ // morph deliberately absent: its schema primaryRole is `patient` (the
59
+ // element being morphed — aligned with the transformer's patient marking
60
+ // in the session-9 role-layout swap), and patient is the default.
61
+ bind: 'destination',
62
+ };
20
63
  /**
21
64
  * English modifier keywords (derived from ENGLISH_MODIFIER_ROLES)
22
65
  */
@@ -96,6 +139,13 @@ const ENGLISH_COMMANDS = new Set([
96
139
  'trigger',
97
140
  'unless',
98
141
  'wait',
142
+ // v2.x reactive + realtime + service-worker DSL commands
143
+ 'live',
144
+ 'bind',
145
+ 'eventsource',
146
+ 'socket',
147
+ 'worker',
148
+ 'intercept',
99
149
  ]);
100
150
  // =============================================================================
101
151
  // English Keywords (Non-Commands)
@@ -622,7 +672,7 @@ const es$1 = {
622
672
  swap: 'intercambiar',
623
673
  morph: 'transformar',
624
674
  settle: 'estabilizar',
625
- append: 'añadir',
675
+ append: 'anexar', // profile primary; 'añadir' is add's word (parsed as add)
626
676
  exit: 'salir',
627
677
  install: 'instalar',
628
678
  breakpoint: 'punto-interrupción',
@@ -755,7 +805,7 @@ const es$1 = {
755
805
  prev: 'ant',
756
806
  at: 'en',
757
807
  random: 'aleatorio',
758
- closest: 'máscercano',
808
+ closest: 'cercano',
759
809
  parent: 'padre',
760
810
  children: 'hijos',
761
811
  within: 'dentro',
@@ -814,7 +864,7 @@ const ja$1 = {
814
864
  hide: '隠す',
815
865
  show: '表示',
816
866
  if: 'もし',
817
- unless: 'でなければ',
867
+ unless: 'ない限り', // not でなければ — leading で particle shatters the marker
818
868
  repeat: '繰り返し',
819
869
  for: 'ために',
820
870
  while: 'の間',
@@ -823,7 +873,7 @@ const ja$1 = {
823
873
  break: '中断',
824
874
  halt: '停止',
825
875
  wait: '待つ',
826
- fetch: '取得',
876
+ fetch: 'フェッチ',
827
877
  call: '呼び出し',
828
878
  return: '戻る',
829
879
  make: '作る',
@@ -848,7 +898,11 @@ const ja$1 = {
848
898
  morph: '変形',
849
899
  settle: '安定',
850
900
  append: '末尾追加',
851
- exit: '終了',
901
+ // 終了 → 退出: realigned to the semantic profile primary (ja.ts `exit: 退出`).
902
+ // 終了 was the historic exit/end collision word (see isEndKeyword's ja note);
903
+ // rendered as 終了 it tokenized as a plain identifier, so a guard-clause
904
+ // `exit` inside `if … end` never parsed (behavior-sortable ja).
905
+ exit: '退出',
852
906
  install: 'インストール',
853
907
  breakpoint: 'ブレークポイント',
854
908
  clear: 'クリア',
@@ -857,6 +911,7 @@ const ja$1 = {
857
911
  select: '選択',
858
912
  clone: '複製',
859
913
  prepend: '先頭追加',
914
+ socket: 'ソケット',
860
915
  },
861
916
  modifiers: {
862
917
  to: 'に',
@@ -905,7 +960,7 @@ const ja$1 = {
905
960
  touchcancel: 'タッチキャンセル',
906
961
  },
907
962
  logical: {
908
- when: 'とき',
963
+ when: '時',
909
964
  where: 'どこ',
910
965
  and: 'そして',
911
966
  or: 'または',
@@ -923,6 +978,7 @@ const ja$1 = {
923
978
  otherwise: 'そうでなければ',
924
979
  end: '終わり',
925
980
  live: 'ライブ',
981
+ bind: 'バインド',
926
982
  changes: '変わったら',
927
983
  },
928
984
  temporal: {
@@ -1041,6 +1097,9 @@ const fr$1 = {
1041
1097
  add: 'ajouter',
1042
1098
  remove: 'supprimer',
1043
1099
  toggle: 'basculer',
1100
+ // command blur (the events section has the blur EVENT word; without this
1101
+ // entry the transformer fell back to it and no profile read it as the verb)
1102
+ blur: 'défocaliser',
1044
1103
  hide: 'cacher',
1045
1104
  show: 'montrer',
1046
1105
  if: 'si',
@@ -1077,7 +1136,7 @@ const fr$1 = {
1077
1136
  swap: 'échanger',
1078
1137
  morph: 'transformer',
1079
1138
  settle: 'stabiliser',
1080
- append: 'ajouter',
1139
+ append: 'annexer', // profile primary; 'ajouter' is add's word (parsed as add)
1081
1140
  exit: 'sortir',
1082
1141
  install: 'installer',
1083
1142
  breakpoint: 'point-arrêt',
@@ -1087,6 +1146,7 @@ const fr$1 = {
1087
1146
  select: 'sélectionner',
1088
1147
  clone: 'cloner',
1089
1148
  prepend: 'préfixer',
1149
+ focus: 'focaliser',
1090
1150
  },
1091
1151
  modifiers: {
1092
1152
  to: 'à',
@@ -1210,7 +1270,7 @@ const fr$1 = {
1210
1270
  prev: 'préc',
1211
1271
  at: 'à',
1212
1272
  random: 'aléatoire',
1213
- closest: 'plusproche',
1273
+ closest: 'proche',
1214
1274
  parent: 'parent',
1215
1275
  children: 'enfants',
1216
1276
  within: 'dans',
@@ -1266,9 +1326,17 @@ const de$1 = {
1266
1326
  add: 'hinzufügen',
1267
1327
  remove: 'entfernen',
1268
1328
  toggle: 'umschalten',
1329
+ // command blur (the events section has the blur EVENT word; without this
1330
+ // entry the transformer fell back to it and no profile read it as the verb)
1331
+ blur: 'defokussieren',
1269
1332
  hide: 'verstecken',
1270
1333
  show: 'zeigen',
1271
- if: 'wenn',
1334
+ // Align with the semantic German profile's `if` primary (`falls`). The previous
1335
+ // `wenn` collides with the profile's `when` keyword (German `wenn` = both
1336
+ // "if" and "when"), so a transformed `if` resolved to `when` and the conditional
1337
+ // wrapper never formed (`if`/`put` dropped). Same dict↔profile alignment as id
1338
+ // `toggle` / get-value. `when` is unaffected (separate dict entry).
1339
+ if: 'falls',
1272
1340
  unless: 'wennnicht',
1273
1341
  repeat: 'wiederholen',
1274
1342
  for: 'für',
@@ -1278,7 +1346,7 @@ const de$1 = {
1278
1346
  break: 'unterbrechen',
1279
1347
  halt: 'anhalten',
1280
1348
  wait: 'warten',
1281
- fetch: 'holen',
1349
+ fetch: 'abrufen',
1282
1350
  call: 'aufrufen',
1283
1351
  return: 'zurückgeben',
1284
1352
  make: 'erstellen',
@@ -1306,12 +1374,13 @@ const de$1 = {
1306
1374
  exit: 'beenden',
1307
1375
  install: 'installieren',
1308
1376
  breakpoint: 'haltepunkt',
1309
- clear: 'löschen',
1377
+ clear: 'bereinigen', // löschen is a remove alternative in the profile
1310
1378
  close: 'schließen',
1311
1379
  open: 'öffnen',
1312
1380
  select: 'auswählen',
1313
1381
  clone: 'klonen',
1314
1382
  prepend: 'voranstellen',
1383
+ focus: 'fokussieren',
1315
1384
  },
1316
1385
  modifiers: {
1317
1386
  to: 'zu',
@@ -1435,7 +1504,13 @@ const de$1 = {
1435
1504
  prev: 'vorh',
1436
1505
  at: 'bei',
1437
1506
  random: 'zufällig',
1438
- closest: 'nächste',
1507
+ // `nächste` already serves `next` (line above). German `nächste` is
1508
+ // genuinely ambiguous (next/nearest), so emitting it for `closest` too
1509
+ // collided: the semantic tokenizer normalizes `nächste`→next (a deliberate
1510
+ // last-wins choice — see german.ts), and a closest reading is impossible to
1511
+ // recover. Use the unambiguous `nächstgelegene` ("nearest-located") for
1512
+ // closest so the two readings stay distinct end-to-end.
1513
+ closest: 'nächstgelegene',
1439
1514
  parent: 'elternteil',
1440
1515
  children: 'kinder',
1441
1516
  within: 'innerhalb',
@@ -1499,7 +1574,10 @@ const ar$1 = {
1499
1574
  hide: 'اخف',
1500
1575
  show: 'اظهر',
1501
1576
  if: 'إذا',
1502
- unless: 'إلا إذا',
1577
+ // Single-token form: the semantic ar tokenizer splits the two-word `إلا إذا`
1578
+ // into `إلا` + `إذا`(if), which derails the `unless` block. `إلا` alone is the
1579
+ // conventional Arabic shorthand and matches the ar profile's unless keyword.
1580
+ unless: 'إلا',
1503
1581
  repeat: 'كرر',
1504
1582
  for: 'لكل',
1505
1583
  while: 'بينما',
@@ -1529,19 +1607,20 @@ const ar$1 = {
1529
1607
  js: 'جافاسكربت',
1530
1608
  async: 'متزامن',
1531
1609
  render: 'ارسم',
1532
- swap: 'بدّل',
1610
+ swap: 'استبدل', // profile primary; 'بدّل' is toggle's word (parsed as toggle)
1533
1611
  morph: 'حوّل',
1534
1612
  settle: 'استقر',
1535
1613
  append: 'ألحق',
1536
1614
  exit: 'اخرج',
1537
1615
  install: 'ثبّت',
1538
1616
  breakpoint: 'نقطة-توقف',
1539
- clear: 'امسح',
1617
+ clear: 'نظّف', // امسح is a remove alternative in the profile
1540
1618
  close: 'أغلق',
1541
1619
  open: 'افتح',
1542
1620
  select: 'اختر',
1543
1621
  clone: 'استنسخ',
1544
1622
  prepend: 'سبق',
1623
+ socket: 'مقبس',
1545
1624
  },
1546
1625
  modifiers: {
1547
1626
  to: 'إلى',
@@ -1608,6 +1687,7 @@ const ar$1 = {
1608
1687
  otherwise: 'خلاف ذلك',
1609
1688
  end: 'النهاية',
1610
1689
  live: 'حي',
1690
+ bind: 'اربط',
1611
1691
  changes: 'يتغير',
1612
1692
  },
1613
1693
  temporal: {
@@ -1733,9 +1813,9 @@ const ko$1 = {
1733
1813
  hide: '숨기다',
1734
1814
  show: '보이다',
1735
1815
  if: '만약',
1736
- unless: '아니면',
1816
+ unless: '아니라면', // distinct from else `아니면` (line ~131) — was a homonym
1737
1817
  repeat: '반복',
1738
- for: '동안',
1818
+ for: '각각', // profile's for word — 동안 is while (collision)
1739
1819
  while: '동안',
1740
1820
  until: '까지',
1741
1821
  continue: '계속',
@@ -1766,8 +1846,11 @@ const ko$1 = {
1766
1846
  swap: '교환',
1767
1847
  morph: '변형',
1768
1848
  settle: '안정',
1769
- append: '추가',
1770
- exit: '종료',
1849
+ append: '덧붙이다', // profile primary; '추가' is add's word (parsed as add)
1850
+ // 종료 → 나가기: realigned to the semantic profile primary (ko.ts `exit: 나가기`,
1851
+ // whose comment already flags the 종료 end-collision). Rendered as 종료 it
1852
+ // tokenized as a plain identifier, so a guard-clause `exit` never parsed.
1853
+ exit: '나가기',
1771
1854
  install: '설치',
1772
1855
  breakpoint: '중단점',
1773
1856
  clear: '지우기',
@@ -1776,6 +1859,7 @@ const ko$1 = {
1776
1859
  select: '선택',
1777
1860
  clone: '복제',
1778
1861
  prepend: '앞에추가',
1862
+ socket: '소켓',
1779
1863
  },
1780
1864
  modifiers: {
1781
1865
  to: '에',
@@ -1842,6 +1926,7 @@ const ko$1 = {
1842
1926
  otherwise: '그렇지않으면',
1843
1927
  end: '끝',
1844
1928
  live: '라이브',
1929
+ bind: '바인드',
1845
1930
  changes: '변경되면',
1846
1931
  },
1847
1932
  temporal: {
@@ -1954,7 +2039,11 @@ const zh$1 = {
1954
2039
  tell: '告诉',
1955
2040
  trigger: '触发',
1956
2041
  send: '发送',
1957
- take: '获取',
2042
+ // `拿取` (the semantic zh profile's take primary), not `获取` — `获取` is the
2043
+ // profile's GET primary, so emitting it for take made `take …` parse as get
2044
+ // (take-class-from-siblings: phantom get, take dropped). Exposed by the get-zh-ba
2045
+ // pattern; same dict↔profile homonym family as pl get/fetch.
2046
+ take: '拿取',
1958
2047
  put: '放置',
1959
2048
  set: '设置',
1960
2049
  get: '获得',
@@ -1973,7 +2062,10 @@ const zh$1 = {
1973
2062
  break: '中断',
1974
2063
  halt: '停止',
1975
2064
  wait: '等待',
1976
- fetch: '获取',
2065
+ // 抓取 (zhuāqǔ, "grab/scrape") is the semantic zh profile's `fetch` primary;
2066
+ // 获取 (huòqǔ) is its `get` primary, so emitting 获取 here made the semantic
2067
+ // parser read a transformed `fetch` as `get`. See ZH_BLOCK_BODY_SCOPE.md #3.
2068
+ fetch: '抓取',
1977
2069
  call: '调用',
1978
2070
  return: '返回',
1979
2071
  make: '制作',
@@ -2073,6 +2165,7 @@ const zh$1 = {
2073
2165
  otherwise: '要不然',
2074
2166
  end: '结束',
2075
2167
  live: '实时',
2168
+ bind: '绑定',
2076
2169
  changes: '改变时',
2077
2170
  },
2078
2171
  temporal: {
@@ -2186,7 +2279,7 @@ const tr$1 = {
2186
2279
  tell: 'söyle',
2187
2280
  trigger: 'tetikle',
2188
2281
  send: 'gönder',
2189
- take: 'al',
2282
+ take: 'tut', // auditor: dict emitted a word the profile reads differently
2190
2283
  put: 'koy',
2191
2284
  set: 'ayarla',
2192
2285
  get: 'al',
@@ -2199,7 +2292,11 @@ const tr$1 = {
2199
2292
  unless: 'değilse',
2200
2293
  repeat: 'tekrarla',
2201
2294
  for: 'için',
2202
- while: 'iken',
2295
+ // `süresince` (the semantic tr profile's while primary), not `iken`: `iken`
2296
+ // is the profile's WHEN primary (logical.when below also emits it), so a
2297
+ // fronted repeat-while head tokenized as `when` and the condition dropped
2298
+ // wholesale. Same dict↔profile homonym disambiguation as pl get/pobierz.
2299
+ while: 'süresince',
2203
2300
  until: 'kadar',
2204
2301
  continue: 'devam',
2205
2302
  break: 'dur',
@@ -2229,7 +2326,7 @@ const tr$1 = {
2229
2326
  swap: 'takas',
2230
2327
  morph: 'dönüştür',
2231
2328
  settle: 'sabitlen',
2232
- append: 'ekle',
2329
+ append: 'iliştir', // profile primary; 'ekle' is add's word (parsed as add)
2233
2330
  exit: 'çık',
2234
2331
  install: 'kur',
2235
2332
  breakpoint: 'kesme-noktası',
@@ -2239,6 +2336,7 @@ const tr$1 = {
2239
2336
  select: 'seç',
2240
2337
  clone: 'klonla',
2241
2338
  prepend: 'başınaekle',
2339
+ socket: 'soket',
2242
2340
  },
2243
2341
  modifiers: {
2244
2342
  to: 'e',
@@ -2261,8 +2359,11 @@ const tr$1 = {
2261
2359
  events: {
2262
2360
  click: 'tıklama',
2263
2361
  dblclick: 'çift_tıklama',
2264
- mousedown: 'fare_bas',
2265
- mouseup: 'fare_bırak',
2362
+ // Fused (no `_`): the semantic tokenizer splits on `_`, breaking event
2363
+ // recognition (mousedown → "bas" / mouseup mis-captured). Mirrors the #535
2364
+ // ru/uk fuse + #510 resize fix.
2365
+ mousedown: 'farebas',
2366
+ mouseup: 'farebırak',
2266
2367
  mouseenter: 'fare_gir',
2267
2368
  mouseleave: 'fare_çık',
2268
2369
  mouseover: 'fare_üstü',
@@ -2279,7 +2380,7 @@ const tr$1 = {
2279
2380
  reset: 'sıfırla',
2280
2381
  load: 'yükle',
2281
2382
  unload: 'yükle_kaldır',
2282
- resize: 'boyut_değiştir',
2383
+ resize: 'boyutlandırma',
2283
2384
  scroll: 'kaydır',
2284
2385
  touchstart: 'dokunma_başla',
2285
2386
  touchend: 'dokunma_bitir',
@@ -2300,7 +2401,7 @@ const tr$1 = {
2300
2401
  equals: 'eşittir',
2301
2402
  has: 'var',
2302
2403
  have: 'var',
2303
- then: 'sonra',
2404
+ then: 'ardından',
2304
2405
  else: 'yoksa',
2305
2406
  otherwise: 'aksi_halde',
2306
2407
  end: 'son',
@@ -2356,13 +2457,18 @@ const tr$1 = {
2356
2457
  },
2357
2458
  expressions: {
2358
2459
  first: 'ilk',
2359
- last: 'son',
2460
+ // 'son' is also the END keyword emission (block terminator), and the
2461
+ // parser's end-recognizers match it by value — so a positional `last`
2462
+ // emission of 'son' terminated the enclosing block mid-condition
2463
+ // (focus-trap's `eşleşir son <button/>` dropped the branch body; the sw
2464
+ // mwisho class). 'sonuncu' ("the last one") is unambiguous.
2465
+ last: 'sonuncu',
2360
2466
  next: 'sonraki',
2361
2467
  previous: 'önceki',
2362
2468
  prev: 'önc',
2363
2469
  at: 'de',
2364
2470
  random: 'rastgele',
2365
- closest: 'en_yakın',
2471
+ closest: 'enyakın',
2366
2472
  parent: 'ebeveyn',
2367
2473
  children: 'çocuklar',
2368
2474
  within: 'içinde',
@@ -2424,7 +2530,7 @@ const id$1 = {
2424
2530
  get: 'dapatkan',
2425
2531
  add: 'tambah',
2426
2532
  remove: 'hapus',
2427
- toggle: 'ganti',
2533
+ toggle: 'alihkan',
2428
2534
  hide: 'sembunyikan',
2429
2535
  show: 'tampilkan',
2430
2536
  if: 'jika',
@@ -2437,7 +2543,10 @@ const id$1 = {
2437
2543
  break: 'hentikan',
2438
2544
  halt: 'berhenti',
2439
2545
  wait: 'tunggu',
2440
- fetch: 'ambil',
2546
+ // profile primary; 'ambil' is take's word — emitting it for fetch made every
2547
+ // fetch parse as take once the fused take-event pattern came alive (the de
2548
+ // holen/abrufen bug class)
2549
+ fetch: 'muat',
2441
2550
  call: 'panggil',
2442
2551
  return: 'kembali',
2443
2552
  make: 'buat',
@@ -2446,7 +2555,11 @@ const id$1 = {
2446
2555
  catch: 'tangkap',
2447
2556
  measure: 'ukur',
2448
2557
  transition: 'transisi',
2449
- increment: 'tambahkan',
2558
+ // naikkan (raise), not tambahkan — tambahkan is the semantic profile's
2559
+ // `add` alternative, so emitting it made increment-counter parse as ADD
2560
+ // (the #373 collision family). naikkan is the profile's increment
2561
+ // alternative, pairing naturally with the decrement side.
2562
+ increment: 'naikkan',
2450
2563
  decrement: 'kurangi',
2451
2564
  default: 'bawaan',
2452
2565
  go: 'pergi',
@@ -2457,15 +2570,15 @@ const id$1 = {
2457
2570
  beep: 'bunyi',
2458
2571
  js: 'js',
2459
2572
  async: 'asinkron',
2460
- render: 'tampilkan',
2573
+ render: 'olah', // auditor: dict emitted a word the profile reads differently
2461
2574
  swap: 'tukar',
2462
- morph: 'ubah_bentuk',
2463
- settle: 'stabil',
2464
- append: 'tambah_akhir',
2575
+ morph: 'ubah', // auditor: dict emitted a word the profile reads differently
2576
+ settle: 'stabilkan', // auditor: dict emitted a word the profile reads differently
2577
+ append: 'sisipkan', // profile primary; 'tambah_akhir' splits and is unrecognized
2465
2578
  exit: 'keluar',
2466
2579
  install: 'pasang',
2467
2580
  breakpoint: 'titik-henti',
2468
- clear: 'hapus',
2581
+ clear: 'bersihkan', // hapus is the remove PRIMARY in the profile
2469
2582
  close: 'tutup',
2470
2583
  open: 'buka',
2471
2584
  select: 'pilih',
@@ -2646,20 +2759,31 @@ const qu$1 = {
2646
2759
  tell: 'niy',
2647
2760
  trigger: 'kichay',
2648
2761
  send: 'kachay',
2649
- take: 'hurquy',
2762
+ take: 'hapiy', // auditor: dict emitted a word the profile reads differently
2650
2763
  put: 'churay',
2651
- set: 'churay',
2652
- get: 'chaskiy',
2764
+ // churay is the semantic qu profile's `put` primary; `set` is churanay. Emitting
2765
+ // churay for set made the semantic parser read a transformed `set` as `put`.
2766
+ // See ZH_BLOCK_BODY_SCOPE.md (#2 sweep — keyword realignment, cf. zh fetch).
2767
+ set: 'churanay',
2768
+ // Align with the semantic quechua profile's `get` primary (`taripay`); the
2769
+ // previous `chaskiy` had no profile entry, so the transformed `get` dropped
2770
+ // (get-value parsed degenerate). Same dict↔profile alignment as id `toggle`.
2771
+ get: 'taripay',
2653
2772
  add: 'yapay',
2654
2773
  remove: 'qichuy',
2655
2774
  toggle: 'tikray',
2656
2775
  hide: 'pakay',
2657
2776
  show: 'rikuchiy',
2658
2777
  if: 'sichus',
2659
- unless: 'mana_sichus',
2660
- repeat: 'kutichiy',
2661
- for: 'rayku',
2662
- while: 'kay_kaq',
2778
+ unless: 'mana sichus', // spaced phrase ("if not"); `_` form split to `mana`+`sichus`(=if) at parse time
2779
+ repeat: 'kutipay',
2780
+ for: 'sapankaq', // profile's for word — rayku doubles as `by`
2781
+ // `kaykamaqa` (the semantic qu profile's while primary), not `kay_kaq`: the
2782
+ // `_` form is unknown to the profile (and `_` words split at parse time, cf.
2783
+ // qhipaman_yapay), so the fronted repeat-while head never formed a `while`
2784
+ // node — the condition dropped wholesale. Same dict↔profile alignment as
2785
+ // vi render / de abrufen.
2786
+ while: 'kaykamaqa',
2663
2787
  until: 'hayk_akama',
2664
2788
  continue: 'purichiy',
2665
2789
  break: 'p_akiy',
@@ -2669,12 +2793,18 @@ const qu$1 = {
2669
2793
  call: 'qayay',
2670
2794
  return: 'kutimuy',
2671
2795
  make: 'ruray',
2672
- log: 'qillqay',
2796
+ // qillqay is the semantic qu profile's `copy` primary; `log` is qillqakuy.
2797
+ // Emitting qillqay for log made the parser read every transformed log as
2798
+ // copy (log-value/log-element/get-value lossy) — same realign as set above.
2799
+ log: 'qillqakuy',
2673
2800
  throw: 'wikchuy',
2674
2801
  catch: 'hapsiy',
2675
2802
  measure: 'tupuy',
2676
- transition: 'tikray',
2677
- increment: 'yapay',
2803
+ // pasay, not tikray: the semantic qu profile's toggle keyword is
2804
+ // t'ikray/tikray, so a tikray render collided with toggle and the
2805
+ // transition command never anchored (sw kama precedent, #569).
2806
+ transition: 'pasay',
2807
+ increment: 'yapachiy',
2678
2808
  decrement: 'pisiyachiy',
2679
2809
  default: 'ñawpaq_kaq',
2680
2810
  go: 'riy',
@@ -2685,13 +2815,13 @@ const qu$1 = {
2685
2815
  beep: 'waqay',
2686
2816
  js: 'js',
2687
2817
  async: 'mana_suyaspa',
2688
- render: 'rikuchiy',
2689
- swap: 'rantin_tikray',
2690
- morph: 'tikrachiy',
2691
- settle: 'tiyay',
2692
- append: 'qhipaman_yapay',
2818
+ render: 'rikurichiy', // auditor: dict emitted a word the profile reads differently
2819
+ swap: "t'inkuy", // profile primary; 'rantin_tikray' splits, 'tikray' is toggle's word
2820
+ morph: 'tukunay', // auditor: dict emitted a word the profile reads differently
2821
+ settle: 'tiyakuy', // auditor: dict emitted a word the profile reads differently
2822
+ append: 'qatichiy', // profile's append primary (single token); `qhipaman_yapay` `_`-split to qhipaman+yapay(=add) at parse time
2693
2823
  exit: 'lluqsiy',
2694
- install: 'churay',
2824
+ install: 'tarpuy',
2695
2825
  breakpoint: 'sayachinay',
2696
2826
  clear: 'pichay',
2697
2827
  close: 'wichqay',
@@ -2699,6 +2829,7 @@ const qu$1 = {
2699
2829
  select: 'akllay',
2700
2830
  clone: 'kikinchay',
2701
2831
  prepend: 'ñawpachiy',
2832
+ socket: 'tinkina',
2702
2833
  },
2703
2834
  modifiers: {
2704
2835
  to: 'man',
@@ -2721,8 +2852,10 @@ const qu$1 = {
2721
2852
  events: {
2722
2853
  click: 'ñitiy',
2723
2854
  dblclick: 'iskay_ñitiy',
2724
- mousedown: 'rat_ñitiy',
2725
- mouseup: 'rat_huqariy',
2855
+ // Fused (no `_`): the semantic tokenizer splits on `_` — and `rat_ñitiy` then
2856
+ // mis-captured as the `click` event (ñitiy→click). Mirrors the #535 ru/uk fuse.
2857
+ mousedown: 'ratñitiy',
2858
+ mouseup: 'rathuqariy',
2726
2859
  mouseenter: 'rat_yaykuy',
2727
2860
  mouseleave: 'rat_lluqsiy',
2728
2861
  mouseover: 'rat_hawapi',
@@ -2761,7 +2894,11 @@ const qu$1 = {
2761
2894
  has: 'kachkan',
2762
2895
  have: 'kachkani',
2763
2896
  then: 'chayqa',
2764
- else: 'mana_chayqa',
2897
+ // Align to the semantic profile's else word `manachus`. The old `mana_chayqa`
2898
+ // tokenized as `mana`(false) + `_` + `chayqa`(then) — no else keyword formed,
2899
+ // so qu conditionals never split their else branch. `manachus` is a single
2900
+ // token the profile-derived keyword map reads as `else` (dict↔profile align).
2901
+ else: 'manachus',
2765
2902
  otherwise: 'huk_kaqpi',
2766
2903
  end: 'tukuy',
2767
2904
  live: 'kawsay',
@@ -2784,7 +2921,7 @@ const qu$1 = {
2784
2921
  values: {
2785
2922
  true: 'cheqaq',
2786
2923
  false: 'llulla',
2787
- null: 'ch_usaq',
2924
+ null: 'chusaq',
2788
2925
  undefined: 'mana_riqsisqa',
2789
2926
  it: 'chay',
2790
2927
  its: 'chaypaq',
@@ -2822,12 +2959,12 @@ const qu$1 = {
2822
2959
  prev: 'ñawpaq',
2823
2960
  at: 'pi',
2824
2961
  random: 'imaymanata',
2825
- closest: 'aswan_kaylla',
2962
+ closest: 'kaylla',
2826
2963
  parent: 'mama_tayta',
2827
2964
  children: 'wawakuna',
2828
2965
  within: 'ukupi',
2829
2966
  no: 'mana_kanchu',
2830
- empty: 'ch_usaq',
2967
+ empty: 'chusaq',
2831
2968
  some: 'wakin',
2832
2969
  'starts with': 'qallarisqa wan',
2833
2970
  'ends with': 'tukusqa wan',
@@ -2879,11 +3016,17 @@ const sw$1 = {
2879
3016
  send: 'tuma',
2880
3017
  take: 'chukua',
2881
3018
  put: 'weka',
2882
- set: 'weka',
3019
+ // weka is the semantic sw profile's `put` primary; `set` is seti. Emitting weka
3020
+ // for set made the semantic parser read a transformed `set` as `put`.
3021
+ // See ZH_BLOCK_BODY_SCOPE.md (#2 sweep — keyword realignment, cf. zh fetch).
3022
+ set: 'seti',
2883
3023
  get: 'pata',
2884
3024
  add: 'ongeza',
2885
3025
  remove: 'ondoa',
2886
3026
  toggle: 'badilisha',
3027
+ // command blur (the events section has the blur EVENT word; without this
3028
+ // entry the transformer fell back to it and no profile read it as the verb)
3029
+ blur: 'blur',
2887
3030
  hide: 'ficha',
2888
3031
  show: 'onyesha',
2889
3032
  if: 'kama',
@@ -2899,13 +3042,13 @@ const sw$1 = {
2899
3042
  fetch: 'leta',
2900
3043
  call: 'ita',
2901
3044
  return: 'rudi',
2902
- make: 'fanya',
3045
+ make: 'tengeneza', // auditor: dict emitted a word the profile reads differently
2903
3046
  log: 'andika',
2904
3047
  throw: 'tupa',
2905
3048
  catch: 'shika',
2906
3049
  measure: 'pima',
2907
3050
  transition: 'mpito',
2908
- increment: 'ongeza',
3051
+ increment: 'ongezeko',
2909
3052
  decrement: 'punguza',
2910
3053
  default: 'msingi',
2911
3054
  go: 'nenda',
@@ -2918,18 +3061,20 @@ const sw$1 = {
2918
3061
  async: 'sainkroni',
2919
3062
  render: 'chora',
2920
3063
  swap: 'badilishana',
2921
- morph: 'badilishaUmbo',
3064
+ morph: 'geuza', // auditor: dict emitted a word the profile reads differently
2922
3065
  settle: 'tulia',
2923
- append: 'ongezaMwisho',
3066
+ append: 'ambatanisha', // profile primary; 'ongezaMwisho' is unrecognized
2924
3067
  exit: 'toka',
2925
3068
  install: 'sakinisha',
2926
3069
  breakpoint: 'nukta-simama',
2927
- clear: 'futa',
3070
+ clear: 'safisha', // futa is a remove alternative in the profile
2928
3071
  close: 'funga',
2929
3072
  open: 'fungua',
2930
3073
  select: 'chagua',
2931
3074
  clone: 'nakili',
2932
3075
  prepend: 'tanguliza',
3076
+ focus: 'lenga',
3077
+ socket: 'soketi',
2933
3078
  },
2934
3079
  modifiers: {
2935
3080
  to: 'kwa',
@@ -2939,7 +3084,14 @@ const sw$1 = {
2939
3084
  at: 'katika',
2940
3085
  in: 'ndani',
2941
3086
  of: 'ya',
2942
- as: 'kama',
3087
+ // `kuwa` ("to be/become" — the conversion sense, cf. `badilisha kuwa`), not
3088
+ // `kama`: sw `kama` is the IF keyword (commands.if above and the semantic sw
3089
+ // profile's if primary), so emitting it for `as` grew a phantom `if` command
3090
+ // out of every transformed `as <Type>` tail (`kama JSON`, `kama Number`) when
3091
+ // the semantic parser split event-body statements. Same dict↔profile homonym
3092
+ // disambiguation as pl get/pobierz. The semantic fetch-sw pattern still
3093
+ // tolerates hand-written `kama` in as-marker position.
3094
+ as: 'kuwa',
2943
3095
  by: 'na',
2944
3096
  before: 'kabla',
2945
3097
  after: 'baada',
@@ -2995,7 +3147,7 @@ const sw$1 = {
2995
3147
  else: 'sivyo',
2996
3148
  otherwise: 'vinginevyo',
2997
3149
  end: 'mwisho',
2998
- live: 'moja_kwa_moja',
3150
+ live: 'mubashara',
2999
3151
  changes: 'inabadilika',
3000
3152
  },
3001
3153
  temporal: {
@@ -3047,13 +3199,25 @@ const sw$1 = {
3047
3199
  },
3048
3200
  expressions: {
3049
3201
  first: 'kwanza',
3050
- last: 'mwisho',
3202
+ // 'mwisho' is the END keyword (block terminator) — the tokenizer's keyword
3203
+ // map is last-wins, so a positional `last` emission of 'mwisho' reads as
3204
+ // `end` and terminates the enclosing block mid-condition (focus-trap's
3205
+ // `… inafanana mwisho <button/>` dropped the whole branch body). Emit the
3206
+ // concatenated adjective form instead (wa mwisho — the saufsi/wennnicht/
3207
+ // enyakın single-token class); the tokenizer reads it as `last`.
3208
+ last: 'wamwisho',
3051
3209
  next: 'ijayo',
3052
3210
  previous: 'iliyopita',
3053
3211
  prev: 'awali',
3054
3212
  at: 'katika',
3055
3213
  random: 'nasibu',
3056
- closest: 'karibu_zaidi',
3214
+ // Natural Swahili: `karibu` ("near/close") is the ordinary word and already
3215
+ // maps to closest. The earlier `karibu_zaidi` ("nearer") underscore-split to
3216
+ // karibu/_/zaidi (the stray `_ zaidi` broke positional capture); the fused
3217
+ // `karibuzaidi` parsed but isn't real Swahili. `karibu` alone is both natural
3218
+ // and correct (the sw tokenizer is single-word-only, so a spaced `karibu
3219
+ // zaidi` would strand `zaidi` regardless).
3220
+ closest: 'karibu',
3057
3221
  parent: 'mzazi',
3058
3222
  children: 'watoto',
3059
3223
  within: 'ndani_ya',
@@ -3115,17 +3279,21 @@ const pt$1 = {
3115
3279
  add: 'adicionar',
3116
3280
  remove: 'remover',
3117
3281
  toggle: 'alternar',
3282
+ // command blur (the events section has the blur EVENT word; without this
3283
+ // entry the transformer fell back to it and no profile read it as the verb)
3284
+ blur: 'desfocar',
3118
3285
  hide: 'esconder',
3119
3286
  show: 'mostrar',
3120
3287
  if: 'se',
3121
- unless: 'a_menos',
3288
+ unless: 'salvo', // a_menos splits at _ in the pt extractor (see profile)
3122
3289
  repeat: 'repetir',
3123
3290
  for: 'para',
3124
3291
  while: 'enquanto',
3125
3292
  until: 'até',
3126
3293
  continue: 'continuar',
3127
- break: 'parar',
3128
- halt: 'interromper',
3294
+ // un-crossed: the profile/tokenizer read parar=halt, interromper=break
3295
+ break: 'interromper',
3296
+ halt: 'parar',
3129
3297
  wait: 'esperar',
3130
3298
  fetch: 'buscar',
3131
3299
  call: 'chamar',
@@ -3161,6 +3329,8 @@ const pt$1 = {
3161
3329
  select: 'selecionar',
3162
3330
  clone: 'clonar',
3163
3331
  prepend: 'preceder',
3332
+ focus: 'focar',
3333
+ socket: 'soquete',
3164
3334
  },
3165
3335
  modifiers: {
3166
3336
  to: 'para',
@@ -3284,7 +3454,7 @@ const pt$1 = {
3284
3454
  prev: 'ant',
3285
3455
  at: 'em',
3286
3456
  random: 'aleatório',
3287
- closest: 'mais_próximo',
3457
+ closest: 'maispróximo',
3288
3458
  parent: 'pai',
3289
3459
  children: 'filhos',
3290
3460
  within: 'dentro',
@@ -3349,7 +3519,7 @@ const it$1 = {
3349
3519
  hide: 'nascondere',
3350
3520
  show: 'mostrare',
3351
3521
  if: 'se',
3352
- unless: 'a meno che',
3522
+ unless: 'salvo',
3353
3523
  repeat: 'ripetere',
3354
3524
  for: 'per',
3355
3525
  while: 'mentre',
@@ -3382,7 +3552,7 @@ const it$1 = {
3382
3552
  swap: 'scambiare',
3383
3553
  morph: 'trasformare',
3384
3554
  settle: 'stabilizzare',
3385
- append: 'aggiungere',
3555
+ append: 'accodare', // profile primary; 'aggiungere' is add's word (parsed as add)
3386
3556
  exit: 'uscire',
3387
3557
  install: 'installare',
3388
3558
  breakpoint: 'punto-interruzione',
@@ -3392,6 +3562,9 @@ const it$1 = {
3392
3562
  select: 'selezionare',
3393
3563
  clone: 'clonare',
3394
3564
  prepend: 'anteporre',
3565
+ // profile primary; without a commands entry the transformer fell back to
3566
+ // the EVENTS noun 'fuoco', which generated focus patterns can't match
3567
+ focus: 'focalizzare',
3395
3568
  },
3396
3569
  modifiers: {
3397
3570
  to: 'a',
@@ -3515,7 +3688,7 @@ const it$1 = {
3515
3688
  prev: 'prec',
3516
3689
  at: 'a',
3517
3690
  random: 'casuale',
3518
- closest: 'piùvicino',
3691
+ closest: 'vicino',
3519
3692
  parent: 'genitore',
3520
3693
  children: 'figli',
3521
3694
  within: 'dentro',
@@ -3583,7 +3756,7 @@ const vi$1 = {
3583
3756
  beep: 'beep',
3584
3757
  js: 'js',
3585
3758
  async: 'bất đồng bộ',
3586
- render: 'hiển thị',
3759
+ render: 'kết xuất', // distinct from `show` (was `hiển thị`, which collides with show: the semantic profile reads `hiển thị` as show, `kết xuất` as render)
3587
3760
  swap: 'hoán đổi',
3588
3761
  morph: 'biến đổi',
3589
3762
  settle: 'ổn định',
@@ -3598,7 +3771,7 @@ const vi$1 = {
3598
3771
  prepend: 'thêm đầu',
3599
3772
  else: 'không thì',
3600
3773
  breakpoint: 'điểm-dừng',
3601
- clear: 'xóa',
3774
+ clear: 'tẩy', // xóa is the remove PRIMARY in the profile
3602
3775
  close: 'đóng',
3603
3776
  open: 'mở',
3604
3777
  select: 'chọn',
@@ -3662,7 +3835,7 @@ const vi$1 = {
3662
3835
  else: 'không thì',
3663
3836
  end: 'kết thúc',
3664
3837
  then: 'rồi',
3665
- live: 'trực tiếp',
3838
+ live: 'live',
3666
3839
  changes: 'thay đổi',
3667
3840
  },
3668
3841
  temporal: {
@@ -3749,15 +3922,22 @@ const pl$1 = {
3749
3922
  commands: {
3750
3923
  on: 'gdy',
3751
3924
  tell: 'powiedz',
3752
- trigger: 'wywołaj',
3925
+ trigger: 'wyzwól', // auditor: dict emitted a word the profile reads differently
3753
3926
  send: 'wyślij',
3754
3927
  take: 'weź',
3755
3928
  put: 'umieść',
3756
3929
  set: 'ustaw',
3757
- get: 'pobierz',
3930
+ // `uzyskaj` (the semantic pl profile's get primary), not `pobierz`: pl `pobierz`
3931
+ // ("download") is the profile's FETCH primary, so emitting it for get made the
3932
+ // parser read every transformed get as fetch (get-value lossy + a phantom fetch).
3933
+ // Same dict↔profile homonym disambiguation as tr load/install, qu get.
3934
+ get: 'uzyskaj',
3758
3935
  add: 'dodaj',
3759
3936
  remove: 'usuń',
3760
3937
  toggle: 'przełącz',
3938
+ // command blur (the events section has the blur EVENT word; without this
3939
+ // entry the transformer fell back to it and no profile read it as the verb)
3940
+ blur: 'rozmyj',
3761
3941
  hide: 'ukryj',
3762
3942
  show: 'pokaż',
3763
3943
  if: 'jeśli',
@@ -3798,12 +3978,13 @@ const pl$1 = {
3798
3978
  exit: 'wyjdź',
3799
3979
  install: 'zainstaluj',
3800
3980
  breakpoint: 'punkt-przerwania',
3801
- clear: 'wyczyść',
3981
+ clear: 'zeruj', // wyczyść is a remove alternative in the profile
3802
3982
  close: 'zamknij',
3803
3983
  open: 'otwórz',
3804
3984
  select: 'wybierz',
3805
3985
  clone: 'sklonuj',
3806
3986
  prepend: 'poprzedź',
3987
+ focus: 'skup',
3807
3988
  },
3808
3989
  modifiers: {
3809
3990
  to: 'do',
@@ -3954,7 +4135,7 @@ const russianDictionary = {
3954
4135
  commands: {
3955
4136
  on: 'при',
3956
4137
  tell: 'сказать',
3957
- trigger: 'вызвать',
4138
+ trigger: 'запустить', // auditor: dict emitted a word the profile reads differently
3958
4139
  send: 'отправить',
3959
4140
  take: 'взять',
3960
4141
  put: 'положить',
@@ -3965,8 +4146,12 @@ const russianDictionary = {
3965
4146
  toggle: 'переключить',
3966
4147
  hide: 'скрыть',
3967
4148
  show: 'показать',
4149
+ // command verb; the events section keeps the noun прокрутка for `on scroll`.
4150
+ // Without this entry the transformer fell back to the event noun, which the
4151
+ // parser doesn't read as the scroll command (#321 focus family).
4152
+ scroll: 'прокрутить',
3968
4153
  if: 'если',
3969
- unless: 'если_не',
4154
+ unless: 'кроме',
3970
4155
  repeat: 'повторить',
3971
4156
  for: 'для',
3972
4157
  while: 'пока',
@@ -3999,10 +4184,10 @@ const russianDictionary = {
3999
4184
  swap: 'поменять',
4000
4185
  morph: 'трансформировать',
4001
4186
  settle: 'стабилизировать',
4002
- append: 'добавить_в_конец',
4187
+ append: 'дописать', // profile alternative; the compound splits and добавить parses as add
4003
4188
  exit: 'выйти',
4004
4189
  else: 'иначе',
4005
- install: 'установить',
4190
+ install: 'инсталлировать', // loanword — `установить` is `set` (homonym); see russian profile
4006
4191
  init: 'инициализировать',
4007
4192
  behavior: 'поведение',
4008
4193
  focus: 'сфокусировать',
@@ -4037,8 +4222,11 @@ const russianDictionary = {
4037
4222
  events: {
4038
4223
  click: 'клик',
4039
4224
  dblclick: 'двойной_клик',
4040
- mousedown: 'мышь_вниз',
4041
- mouseup: 'мышь_вверх',
4225
+ // Fused (no `_`): the semantic tokenizer splits on `_`, so an underscore form
4226
+ // breaks event recognition (mousedown → bare expression). Mirrors the #510 tr
4227
+ // resize fix (boyut_değiştir → boyutlandırma) and the enyakın superlative.
4228
+ mousedown: 'мышьвниз',
4229
+ mouseup: 'мышьвверх',
4042
4230
  mouseenter: 'мышь_вход',
4043
4231
  mouseleave: 'мышь_выход',
4044
4232
  mouseover: 'наведение',
@@ -4055,7 +4243,7 @@ const russianDictionary = {
4055
4243
  reset: 'сброс',
4056
4244
  load: 'загрузка',
4057
4245
  unload: 'выгрузка',
4058
- resize: 'изменение_размера',
4246
+ resize: 'изменениеразмера', // fused (no `_`) — see mousedown note above
4059
4247
  scroll: 'прокрутка',
4060
4248
  touchstart: 'касание_начало',
4061
4249
  touchend: 'касание_конец',
@@ -4185,7 +4373,7 @@ const ukrainianDictionary = {
4185
4373
  commands: {
4186
4374
  on: 'при',
4187
4375
  tell: 'сказати',
4188
- trigger: 'викликати',
4376
+ trigger: 'запустити', // auditor: dict emitted a word the profile reads differently
4189
4377
  send: 'надіслати',
4190
4378
  take: 'взяти',
4191
4379
  put: 'покласти',
@@ -4196,8 +4384,12 @@ const ukrainianDictionary = {
4196
4384
  toggle: 'перемкнути',
4197
4385
  hide: 'сховати',
4198
4386
  show: 'показати',
4387
+ // command verb; the events section keeps the noun прокрутка for `on scroll`.
4388
+ // Without this entry the transformer fell back to the event noun, which the
4389
+ // parser doesn't read as the scroll command (#321 focus family).
4390
+ scroll: 'прокрутити',
4199
4391
  if: 'якщо',
4200
- unless: 'якщо_не',
4392
+ unless: 'крім',
4201
4393
  repeat: 'повторити',
4202
4394
  for: 'для',
4203
4395
  while: 'поки',
@@ -4230,10 +4422,10 @@ const ukrainianDictionary = {
4230
4422
  swap: 'поміняти',
4231
4423
  morph: 'трансформувати',
4232
4424
  settle: 'стабілізувати',
4233
- append: 'додати_в_кінець',
4425
+ append: 'дописати', // profile alternative; the compound splits and додати parses as add
4234
4426
  exit: 'вийти',
4235
4427
  else: 'інакше',
4236
- install: 'встановити',
4428
+ install: 'інсталювати', // loanword — `встановити` is `set` (homonym); see ukrainian profile
4237
4429
  init: 'ініціалізувати',
4238
4430
  behavior: 'поведінка',
4239
4431
  focus: 'сфокусувати',
@@ -4268,8 +4460,10 @@ const ukrainianDictionary = {
4268
4460
  events: {
4269
4461
  click: 'клік',
4270
4462
  dblclick: 'подвійний_клік',
4271
- mousedown: 'миша_вниз',
4272
- mouseup: 'миша_вгору',
4463
+ // Fused (no `_`): the semantic tokenizer splits on `_`, so an underscore form
4464
+ // breaks event recognition. Mirrors the #510 tr resize fix + enyakın.
4465
+ mousedown: 'мишавниз',
4466
+ mouseup: 'мишавгору',
4273
4467
  mouseenter: 'миша_вхід',
4274
4468
  mouseleave: 'миша_вихід',
4275
4469
  mouseover: 'наведення',
@@ -4286,7 +4480,7 @@ const ukrainianDictionary = {
4286
4480
  reset: 'скидання',
4287
4481
  load: 'завантаження',
4288
4482
  unload: 'вивантаження',
4289
- resize: 'зміна_розміру',
4483
+ resize: 'змінарозміру', // fused (no `_`) — see mousedown note above
4290
4484
  scroll: 'прокрутка',
4291
4485
  touchstart: 'дотик_початок',
4292
4486
  touchend: 'дотик_кінець',
@@ -4428,10 +4622,10 @@ const hindiDictionary = {
4428
4622
  hide: 'छिपाएं',
4429
4623
  show: 'दिखाएं',
4430
4624
  if: 'अगर',
4431
- unless: 'जब_तक_नहीं',
4625
+ unless: 'जब तक नहीं', // spaced (not जब_तक_नहीं) → one multi-word `unless` token
4432
4626
  repeat: 'दोहराएं',
4433
- for: 'के_लिए',
4434
- while: 'जब_तक',
4627
+ for: 'हेतु', // single token — के_लिए splits at _ in the hi extractor
4628
+ while: 'जब तक',
4435
4629
  until: 'तक',
4436
4630
  continue: 'जारी',
4437
4631
  break: 'रोकें',
@@ -4458,10 +4652,10 @@ const hindiDictionary = {
4458
4652
  js: 'जेएस',
4459
4653
  async: 'असिंक',
4460
4654
  render: 'रेंडर',
4461
- swap: 'बदलें',
4655
+ swap: 'विनिमय',
4462
4656
  morph: 'रूपांतर',
4463
4657
  settle: 'स्थिर',
4464
- append: 'जोड़ें_अंत',
4658
+ append: 'संलग्न',
4465
4659
  exit: 'बाहर',
4466
4660
  else: 'वरना',
4467
4661
  install: 'इंस्टॉल',
@@ -4476,6 +4670,7 @@ const hindiDictionary = {
4476
4670
  close: 'बंद-करें',
4477
4671
  open: 'खोलें',
4478
4672
  select: 'चुनें',
4673
+ socket: 'सॉकेट',
4479
4674
  },
4480
4675
  modifiers: {
4481
4676
  to: 'को',
@@ -4487,8 +4682,8 @@ const hindiDictionary = {
4487
4682
  of: 'का',
4488
4683
  as: 'के_रूप_में',
4489
4684
  by: 'द्वारा',
4490
- before: 'से_पहले',
4491
- after: 'के_बाद',
4685
+ before: 'से पहले',
4686
+ after: 'के बाद',
4492
4687
  over: 'के_ऊपर',
4493
4688
  under: 'के_नीचे',
4494
4689
  between: 'के_बीच',
@@ -4499,8 +4694,10 @@ const hindiDictionary = {
4499
4694
  events: {
4500
4695
  click: 'क्लिक',
4501
4696
  dblclick: 'डबल_क्लिक',
4502
- mousedown: 'माउस_नीचे',
4503
- mouseup: 'माउस_ऊपर',
4697
+ // Fused (no `_`): the semantic tokenizer splits on `_`, breaking event
4698
+ // recognition (mousedown → "नीचे"). Mirrors the #535 ru/uk fuse.
4699
+ mousedown: 'माउसनीचे',
4700
+ mouseup: 'माउसऊपर',
4504
4701
  mouseenter: 'माउस_प्रवेश',
4505
4702
  mouseleave: 'माउस_बाहर',
4506
4703
  mouseover: 'माउस_ओवर',
@@ -4532,7 +4729,11 @@ const hindiDictionary = {
4532
4729
  not: 'नहीं',
4533
4730
  is: 'है',
4534
4731
  exists: 'मौजूद',
4535
- matches: 'मेल_खाता',
4732
+ // Natural spaced phrase `मेल खाता` ("matches") — how Hindi writes it. The base
4733
+ // tokenizer now matches multi-word profile keywords, so the hindi profile maps
4734
+ // `मेल खाता`→matches without the underscore (`मेल_खाता` split) or the
4735
+ // concatenated `मेलखाता` (parsed, but unnatural) workarounds.
4736
+ matches: 'मेल खाता',
4536
4737
  contains: 'शामिल',
4537
4738
  includes: 'में_है',
4538
4739
  equals: 'बराबर',
@@ -4674,7 +4875,7 @@ const bengaliDictionary = {
4674
4875
  throw: 'নিক্ষেপ',
4675
4876
  continue: 'চালিয়ে যান',
4676
4877
  make: 'তৈরি করুন',
4677
- append: 'শেষে যোগ',
4878
+ append: 'জুড়ুন',
4678
4879
  prepend: 'শুরুতে যোগ',
4679
4880
  focus: 'ফোকাস',
4680
4881
  blur: 'ঝাপসা',
@@ -4703,6 +4904,7 @@ const bengaliDictionary = {
4703
4904
  close: 'বন্ধ',
4704
4905
  open: 'খুলুন',
4705
4906
  select: 'নির্বাচন',
4907
+ socket: 'সকেট',
4706
4908
  },
4707
4909
  modifiers: {
4708
4910
  to: 'কে',
@@ -4761,6 +4963,7 @@ const bengaliDictionary = {
4761
4963
  end: 'শেষ',
4762
4964
  then: 'তারপর',
4763
4965
  live: 'লাইভ',
4966
+ bind: 'বাইন্ড',
4764
4967
  changes: 'পরিবর্তিত হলে',
4765
4968
  },
4766
4969
  temporal: {
@@ -4860,7 +5063,9 @@ const thaiDictionary = {
4860
5063
  prepend: 'เพิ่มหน้า',
4861
5064
  focus: 'โฟกัส',
4862
5065
  blur: 'เบลอ',
4863
- transition: 'เปลี่ยน',
5066
+ // เปลี่ยนผ่าน, not เปลี่ยน: the semantic th profile's `change` keyword is
5067
+ // เปลี่ยน, so a เปลี่ยน render could never anchor a transition command.
5068
+ transition: 'เปลี่ยนผ่าน',
4864
5069
  settle: 'คงที่',
4865
5070
  measure: 'วัด',
4866
5071
  async: 'อะซิงค์',
@@ -5067,10 +5272,11 @@ const malayDictionary = {
5067
5272
  init: 'mula',
5068
5273
  clone: 'klon',
5069
5274
  breakpoint: 'titik-henti',
5070
- clear: 'padam',
5275
+ clear: 'bersihkan', // padam is a remove alternative in the profile
5071
5276
  close: 'tutup',
5072
5277
  open: 'buka',
5073
5278
  select: 'pilih',
5279
+ socket: 'soket', // derived from the ms profile (socket.primary); see generate-i18n-dictionaries
5074
5280
  },
5075
5281
  modifiers: {
5076
5282
  to: 'ke',
@@ -5247,10 +5453,13 @@ const tagalogDictionary = {
5247
5453
  tell: 'sabihin',
5248
5454
  trigger: 'palitawin',
5249
5455
  send: 'ipadala',
5250
- take: 'kunin',
5456
+ take: 'kumuha', // auditor: dict emitted a word the profile reads differently
5251
5457
  put: 'ilagay',
5252
5458
  set: 'itakda',
5253
- get: 'kuhanin',
5459
+ // Align with the semantic tl profile's `get` primary (`kunin`); the previous
5460
+ // `kuhanin` is the base of fetch's `kuhanin_mula`, so the transformed `get`
5461
+ // dropped (get-value parsed degenerate). Same dict↔profile alignment as id `toggle`.
5462
+ get: 'kunin',
5254
5463
  add: 'idagdag',
5255
5464
  remove: 'alisin',
5256
5465
  toggle: 'palitan',
@@ -5274,7 +5483,7 @@ const tagalogDictionary = {
5274
5483
  throw: 'ihagis',
5275
5484
  catch: 'hulihin',
5276
5485
  measure: 'sukatin',
5277
- transition: 'baguhin',
5486
+ transition: 'lumipat',
5278
5487
  increment: 'dagdagan',
5279
5488
  decrement: 'bawasan',
5280
5489
  default: 'pamantayan',
@@ -5286,10 +5495,10 @@ const tagalogDictionary = {
5286
5495
  beep: 'tunog',
5287
5496
  js: 'js',
5288
5497
  async: 'sabay',
5289
- render: 'ipakita',
5498
+ render: 'irender', // auditor: dict emitted a word the profile reads differently
5290
5499
  swap: 'palitan_pwesto',
5291
- morph: 'baguhin_hugis',
5292
- settle: 'ayusin',
5500
+ morph: 'baguhin', // auditor: dict emitted a word the profile reads differently
5501
+ settle: 'magpatahimik', // auditor: dict emitted a word the profile reads differently
5293
5502
  append: 'idagdag_sa_dulo',
5294
5503
  prepend: 'idagdag_sa_simula',
5295
5504
  exit: 'lumabas',
@@ -5317,7 +5526,11 @@ const tagalogDictionary = {
5317
5526
  as: 'bilang',
5318
5527
  by: 'sa_pamamagitan_ng',
5319
5528
  before: 'bago',
5320
- after: 'pagkatapos',
5529
+ // `matapos` (not `pagkatapos`) for the positional "after": `pagkatapos` is
5530
+ // also Tagalog "then", so emitting it for the position modifier collides with
5531
+ // the sequence connector. `matapos` is the unambiguous positional form and
5532
+ // matches the semantic tl profile's `after`.
5533
+ after: 'matapos',
5321
5534
  over: 'sa_ibabaw',
5322
5535
  under: 'sa_ilalim',
5323
5536
  between: 'sa_pagitan',
@@ -5483,6 +5696,7 @@ const he$1 = {
5483
5696
  beep: 'ביפ',
5484
5697
  break: 'שבור',
5485
5698
  call: 'קרא',
5699
+ clear: 'נקה', // profile's clear word — was untranslated (parsed as nothing)
5486
5700
  continue: 'המשך',
5487
5701
  copy: 'העתק',
5488
5702
  decrement: 'הקטן',
@@ -5495,6 +5709,7 @@ const he$1 = {
5495
5709
  halt: 'עצור',
5496
5710
  hide: 'הסתר',
5497
5711
  if: 'אם',
5712
+ unless: 'אלא',
5498
5713
  increment: 'הגדל',
5499
5714
  install: 'התקן',
5500
5715
  js: 'js',
@@ -5509,6 +5724,7 @@ const he$1 = {
5509
5724
  render: 'רנדר',
5510
5725
  repeat: 'חזור',
5511
5726
  return: 'החזר',
5727
+ scroll: 'גלול',
5512
5728
  send: 'שלח',
5513
5729
  set: 'קבע',
5514
5730
  settle: 'התייצב',
@@ -5546,6 +5762,7 @@ const he$1 = {
5546
5762
  end: 'סוף',
5547
5763
  then: 'אז',
5548
5764
  live: 'חי',
5765
+ bind: 'קשור',
5549
5766
  changes: 'משתנה',
5550
5767
  when: 'כאשר',
5551
5768
  where: 'איפה',
@@ -5564,7 +5781,9 @@ const he$1 = {
5564
5781
  your: 'שלך',
5565
5782
  },
5566
5783
  attributes: {},
5567
- expressions: {},
5784
+ expressions: {
5785
+ last: 'אחרון',
5786
+ },
5568
5787
  };
5569
5788
 
5570
5789
  // packages/i18n/src/parser/he.ts
@@ -5810,6 +6029,13 @@ const en$1 = {
5810
6029
  increment: 'increment',
5811
6030
  decrement: 'decrement',
5812
6031
  default: 'default',
6032
+ // Reactivity Commands
6033
+ bind: 'bind',
6034
+ // Realtime / service-worker Commands
6035
+ eventsource: 'eventsource',
6036
+ socket: 'socket',
6037
+ worker: 'worker',
6038
+ intercept: 'intercept',
5813
6039
  // Navigation Commands
5814
6040
  go: 'go',
5815
6041
  pushUrl: 'pushUrl',
@@ -6281,7 +6507,26 @@ function reorderRoles(roles, targetOrder) {
6281
6507
  function insertMarkers(elements, markers, adpositionType) {
6282
6508
  const result = [];
6283
6509
  for (const element of elements) {
6284
- const marker = markers.find(m => m.role === element.role);
6510
+ let marker = markers.find(m => m.role === element.role);
6511
+ // A selector-shaped "event" is never a trigger. It's the dangling target of
6512
+ // a locative `on` (`set @role to "alert" on #sr-announce`) that
6513
+ // splitOnCommandBoundaries splits off as a headless pseudo-handler (set/put
6514
+ // are deliberately NOT in ON_TARGET_COMMANDS — dual-destination collision).
6515
+ // Emitting the event marker after it (ko 할 때, ja で, bn এ, tr de) plants a
6516
+ // spurious mid-stream event anchor in the emission; suppress the marker and
6517
+ // emit the bare selector, which body parsers skip harmlessly.
6518
+ if (marker?.role === 'event' && /^[#.<@[]/.test(element.translated || element.value)) {
6519
+ marker = undefined;
6520
+ }
6521
+ // A pronoun-valued "duration" is never a time expression. It's the target
6522
+ // phrase of `take … for me` (en maps `for` → duration lexically), kept
6523
+ // in-clause by splitOnCommandBoundaries' loop-head guard. Emitting the
6524
+ // duration marker after it (bn জন্য — also bn's `for` loop keyword) anchors
6525
+ // a spurious `for` command in the semantic parse; suppress the marker and
6526
+ // emit the bare pronoun, which take-pattern matchers skip harmlessly.
6527
+ if (marker?.role === 'duration' && /^(me|you|it)$/i.test(element.value)) {
6528
+ marker = undefined;
6529
+ }
6285
6530
  if (marker) {
6286
6531
  if (adpositionType === 'preposition') {
6287
6532
  // Marker before element: "to element"
@@ -6455,6 +6700,41 @@ const japaneseProfile = {
6455
6700
  insertMarkers: true,
6456
6701
  },
6457
6702
  },
6703
+ {
6704
+ name: 'set-to',
6705
+ description: 'Transform set X to Y to Japanese verb-final order',
6706
+ priority: 90,
6707
+ match: {
6708
+ commands: ['set', '設定'],
6709
+ requiredRoles: ['action', 'patient', 'destination'],
6710
+ // Skip a set whose value swept up a trailing block terminator (`set X to Y
6711
+ // end` — the body of an inline `if … then set … end`): verb-final reorder
6712
+ // would push the verb past `end` and break the block. These parse fine
6713
+ // verb-medial (the default).
6714
+ predicate: parsed => !/(^|\s)end$/i.test((parsed.original ?? '').trim()),
6715
+ },
6716
+ transform: {
6717
+ // X を Y に 設定 — verb-final, matching the generated SOV set pattern
6718
+ // (markerOverride: を on the destination / に on the value). Default reorder
6719
+ // emitted verb-MEDIAL (`X を 設定 Y に`), unmatched by any set pattern.
6720
+ roleOrder: ['patient', 'destination', 'action'],
6721
+ insertMarkers: true,
6722
+ },
6723
+ },
6724
+ {
6725
+ name: 'bind-to',
6726
+ description: 'Transform bind $var to #el to Japanese verb-final order',
6727
+ priority: 90,
6728
+ match: {
6729
+ commands: ['bind', 'バインド'],
6730
+ requiredRoles: ['action', 'patient', 'destination'],
6731
+ },
6732
+ transform: {
6733
+ // $var を #el に バインド
6734
+ roleOrder: ['patient', 'destination', 'action'],
6735
+ insertMarkers: true,
6736
+ },
6737
+ },
6458
6738
  ],
6459
6739
  };
6460
6740
  // =============================================================================
@@ -6473,6 +6753,10 @@ const koreanProfile = {
6473
6753
  { form: '를', role: 'patient', position: 'postposition', required: true, alternatives: ['을'] },
6474
6754
  { form: '에', role: 'destination', position: 'postposition', required: true },
6475
6755
  { form: '에서', role: 'source', position: 'postposition', required: true },
6756
+ // Event marker — the semantic *-event-ko-sov-* patterns anchor on 할 때
6757
+ // (the generator profile's eventMarker); without it every ko handler
6758
+ // emitted a bare event name no fused pattern could anchor.
6759
+ { form: '할 때', role: 'event', position: 'postposition', required: true },
6476
6760
  {
6477
6761
  form: '로',
6478
6762
  role: 'style',
@@ -6503,6 +6787,56 @@ const koreanProfile = {
6503
6787
  insertMarkers: true,
6504
6788
  },
6505
6789
  },
6790
+ {
6791
+ name: 'put-into',
6792
+ description: 'Transform put X into Y to Korean verb-final order',
6793
+ priority: 90,
6794
+ match: {
6795
+ commands: ['put', '넣다'],
6796
+ requiredRoles: ['action', 'patient', 'destination'],
6797
+ // Standalone put only (e.g. a then-chain clause): an event handler whose
6798
+ // action is `put` must keep the event mid-stream, so don't verb-final it.
6799
+ predicate: parsed => !parsed.roles.has('event'),
6800
+ },
6801
+ transform: {
6802
+ // X 를 Y 에 넣다 (patient, destination, verb-last)
6803
+ roleOrder: ['patient', 'destination', 'action'],
6804
+ insertMarkers: true,
6805
+ },
6806
+ },
6807
+ {
6808
+ name: 'set-to',
6809
+ description: 'Transform set X to Y to Korean verb-final order',
6810
+ priority: 90,
6811
+ match: {
6812
+ commands: ['set', '설정'],
6813
+ requiredRoles: ['action', 'patient', 'destination'],
6814
+ // Skip a set whose value swept up a trailing block terminator (`set X to Y
6815
+ // end` — the body of an inline `if … then set … end`): verb-final reorder
6816
+ // would push the verb past `end` and break the block. These parse fine
6817
+ // verb-medial (the default).
6818
+ predicate: parsed => !/(^|\s)end$/i.test((parsed.original ?? '').trim()),
6819
+ },
6820
+ transform: {
6821
+ // X 를 Y 에 설정 — verb-final, matching the generated SOV set pattern.
6822
+ roleOrder: ['patient', 'destination', 'action'],
6823
+ insertMarkers: true,
6824
+ },
6825
+ },
6826
+ {
6827
+ name: 'bind-to',
6828
+ description: 'Transform bind $var to #el to Korean verb-final order',
6829
+ priority: 90,
6830
+ match: {
6831
+ commands: ['bind', '바인드'],
6832
+ requiredRoles: ['action', 'patient', 'destination'],
6833
+ },
6834
+ transform: {
6835
+ // $var 를 #el 에 바인드
6836
+ roleOrder: ['patient', 'destination', 'action'],
6837
+ insertMarkers: true,
6838
+ },
6839
+ },
6506
6840
  ],
6507
6841
  };
6508
6842
  // =============================================================================
@@ -6537,19 +6871,29 @@ const chineseProfile = {
6537
6871
  requiredRoles: ['event', 'action'],
6538
6872
  },
6539
6873
  transform: {
6540
- // 当 点击 时 增加 #count
6874
+ // 当 EVENT 时 ACTION [patient] [destination] [source] [style] [method] ...
6541
6875
  roleOrder: ['event', 'action', 'patient'],
6542
6876
  insertMarkers: true,
6543
- custom: (parsed, _profile) => {
6544
- // Handle 当...时 circumfix
6877
+ custom: (parsed, profile) => {
6878
+ // Handle 当...时 circumfix wrapping the event, then emit the action,
6879
+ // then any remaining roles in canonical order with their markers.
6880
+ // Earlier this function emitted only event/action/patient and silently
6881
+ // dropped destination, source, style, etc. — that produced truncated
6882
+ // output for `send X to Y`, `fetch /api with method:...`, etc.
6545
6883
  const event = parsed.roles.get('event');
6546
6884
  const action = parsed.roles.get('action');
6547
- const patient = parsed.roles.get('patient');
6548
- const parts = ['当', event?.translated || event?.value, '时'];
6549
- parts.push(action?.translated || action?.value || '');
6550
- if (patient) {
6551
- parts.push(patient.translated || patient.value);
6552
- }
6885
+ // Build the post-action role list using the canonical order minus
6886
+ // event/action, plus a safety net for any roles not in the order.
6887
+ const remaining = new Map(parsed.roles);
6888
+ remaining.delete('event');
6889
+ remaining.delete('action');
6890
+ const remainingOrder = (profile.canonicalOrder || []).filter(r => r !== 'event' && r !== 'action');
6891
+ const reordered = reorderRoles(remaining, remainingOrder);
6892
+ const markedTokens = insertMarkers(reordered, profile.markers, profile.adpositionType);
6893
+ const parts = ['当', event?.translated || event?.value || '', '时'];
6894
+ if (action)
6895
+ parts.push(action.translated || action.value);
6896
+ parts.push(...markedTokens);
6553
6897
  return parts.filter(Boolean).join(' ');
6554
6898
  },
6555
6899
  },
@@ -6568,6 +6912,30 @@ const chineseProfile = {
6568
6912
  insertMarkers: true,
6569
6913
  },
6570
6914
  },
6915
+ {
6916
+ name: 'bind-to',
6917
+ description: 'Bind binds a variable to an element: 绑定 X 到 Y (no 把)',
6918
+ priority: 90,
6919
+ match: {
6920
+ commands: ['bind', '绑定'],
6921
+ requiredRoles: ['action', 'patient', 'destination'],
6922
+ },
6923
+ transform: {
6924
+ // Plain SVO without the 把 object-fronting: the bound variable is the
6925
+ // semantic destination (not a fronted patient), and `到` marks the
6926
+ // target element. Custom so the patient does not pick up 把.
6927
+ roleOrder: ['action', 'patient', 'destination'],
6928
+ custom: parsed => {
6929
+ const action = parsed.roles.get('action');
6930
+ const patient = parsed.roles.get('patient');
6931
+ const destination = parsed.roles.get('destination');
6932
+ const verb = action?.translated || action?.value || '';
6933
+ const v = patient?.translated || patient?.value || '';
6934
+ const d = destination?.translated || destination?.value || '';
6935
+ return [verb, v, '到', d].filter(Boolean).join(' ');
6936
+ },
6937
+ },
6938
+ },
6571
6939
  ],
6572
6940
  };
6573
6941
  // =============================================================================
@@ -6664,6 +7032,56 @@ const turkishProfile = {
6664
7032
  insertMarkers: true,
6665
7033
  },
6666
7034
  },
7035
+ {
7036
+ name: 'put-into',
7037
+ description: 'Transform put X into Y to Turkish verb-final order',
7038
+ priority: 90,
7039
+ match: {
7040
+ commands: ['put', 'koy'],
7041
+ requiredRoles: ['action', 'patient', 'destination'],
7042
+ // Standalone put only (e.g. a then-chain clause): an event handler whose
7043
+ // action is `put` must keep the event mid-stream, so don't verb-final it.
7044
+ predicate: parsed => !parsed.roles.has('event'),
7045
+ },
7046
+ transform: {
7047
+ // X i Y e koy (patient, destination, verb-last)
7048
+ roleOrder: ['patient', 'destination', 'action'],
7049
+ insertMarkers: true,
7050
+ },
7051
+ },
7052
+ {
7053
+ name: 'set-to',
7054
+ description: 'Transform set X to Y to Turkish verb-final order',
7055
+ priority: 90,
7056
+ match: {
7057
+ commands: ['set', 'ayarla'],
7058
+ requiredRoles: ['action', 'patient', 'destination'],
7059
+ // Skip a set whose value swept up a trailing block terminator (`set X to Y
7060
+ // end` — the body of an inline `if … then set … end`): verb-final reorder
7061
+ // would push the verb past `end` and break the block. These parse fine
7062
+ // verb-medial (the default).
7063
+ predicate: parsed => !/(^|\s)end$/i.test((parsed.original ?? '').trim()),
7064
+ },
7065
+ transform: {
7066
+ // X i Y e ayarla — verb-final, matching the generated SOV set pattern.
7067
+ roleOrder: ['patient', 'destination', 'action'],
7068
+ insertMarkers: true,
7069
+ },
7070
+ },
7071
+ {
7072
+ name: 'bind-to',
7073
+ description: 'Transform bind $var to #el to Turkish verb-final order',
7074
+ priority: 90,
7075
+ match: {
7076
+ commands: ['bind'],
7077
+ requiredRoles: ['action', 'patient', 'destination'],
7078
+ },
7079
+ transform: {
7080
+ // $var i #el e bağla
7081
+ roleOrder: ['patient', 'destination', 'action'],
7082
+ insertMarkers: true,
7083
+ },
7084
+ },
6667
7085
  ],
6668
7086
  };
6669
7087
  // =============================================================================
@@ -6773,6 +7191,30 @@ const indonesianProfile = {
6773
7191
  ],
6774
7192
  };
6775
7193
  // =============================================================================
7194
+ // Malay (SVO, Austronesian) — grammar mirrors Indonesian (shared function words:
7195
+ // pada / ke / dari / dengan / sebagai). Without this profile the transformer threw
7196
+ // "Unknown target locale: ms", so no ms translation could be generated at all.
7197
+ // =============================================================================
7198
+ const malayProfile = {
7199
+ code: 'ms',
7200
+ name: 'Bahasa Melayu',
7201
+ wordOrder: 'SVO',
7202
+ adpositionType: 'preposition',
7203
+ morphology: 'agglutinative',
7204
+ direction: 'ltr',
7205
+ canonicalOrder: ['event', 'action', 'patient', 'destination'],
7206
+ markers: [
7207
+ // ms marks the event clause with `apabila` ("when/on") — matching the semantic
7208
+ // ms event-handler pattern (`apabila {event} {body}`) and the ms dict's
7209
+ // `on: apabila`. (Indonesian uses `pada` here; the two diverge on the event head.)
7210
+ { form: 'apabila', role: 'event', position: 'preposition', required: true },
7211
+ { form: 'ke', role: 'destination', position: 'preposition', required: false },
7212
+ { form: 'dari', role: 'source', position: 'preposition', required: false },
7213
+ { form: 'dengan', role: 'style', position: 'preposition', required: false },
7214
+ { form: 'sebagai', role: 'method', position: 'preposition', required: false },
7215
+ ],
7216
+ };
7217
+ // =============================================================================
6776
7218
  // Quechua (SOV, Quechuan)
6777
7219
  // =============================================================================
6778
7220
  const quechuaProfile = {
@@ -6811,7 +7253,10 @@ const swahiliProfile = {
6811
7253
  { form: 'kwa', role: 'destination', position: 'preposition', required: false },
6812
7254
  { form: 'kutoka', role: 'source', position: 'preposition', required: false },
6813
7255
  { form: 'na', role: 'style', position: 'preposition', required: false },
6814
- { form: 'kama', role: 'method', position: 'preposition', required: false }, // "as/like"
7256
+ // `kuwa`, not `kama`: sw `kama` is the IF keyword — rendering the method
7257
+ // role ("as X") with it grew a phantom `if` at semantic parse time. See the
7258
+ // matching comment in dictionaries/sw.ts (modifiers.as).
7259
+ { form: 'kuwa', role: 'method', position: 'preposition', required: false }, // "as/become"
6815
7260
  ],
6816
7261
  };
6817
7262
  // =============================================================================
@@ -6852,6 +7297,56 @@ const bengaliProfile = {
6852
7297
  insertMarkers: true,
6853
7298
  },
6854
7299
  },
7300
+ {
7301
+ name: 'put-into',
7302
+ description: 'Transform put X into Y to Bengali verb-final order',
7303
+ priority: 90,
7304
+ match: {
7305
+ commands: ['put', 'রাখুন'],
7306
+ requiredRoles: ['action', 'patient', 'destination'],
7307
+ // Standalone put only (e.g. a then-chain clause): an event handler whose
7308
+ // action is `put` must keep the event mid-stream, so don't verb-final it.
7309
+ predicate: parsed => !parsed.roles.has('event'),
7310
+ },
7311
+ transform: {
7312
+ // X কে Y তে রাখুন (patient, destination, verb-last)
7313
+ roleOrder: ['patient', 'destination', 'action'],
7314
+ insertMarkers: true,
7315
+ },
7316
+ },
7317
+ {
7318
+ name: 'set-to',
7319
+ description: 'Transform set X to Y to Bengali verb-final order',
7320
+ priority: 90,
7321
+ match: {
7322
+ commands: ['set', 'সেট'],
7323
+ requiredRoles: ['action', 'patient', 'destination'],
7324
+ // Skip a set whose value swept up a trailing block terminator (`set X to Y
7325
+ // end` — the body of an inline `if … then set … end`): verb-final reorder
7326
+ // would push the verb past `end` and break the block. These parse fine
7327
+ // verb-medial (the default).
7328
+ predicate: parsed => !/(^|\s)end$/i.test((parsed.original ?? '').trim()),
7329
+ },
7330
+ transform: {
7331
+ // X কে Y তে সেট — verb-final, matching the generated SOV set pattern.
7332
+ roleOrder: ['patient', 'destination', 'action'],
7333
+ insertMarkers: true,
7334
+ },
7335
+ },
7336
+ {
7337
+ name: 'bind-to',
7338
+ description: 'Transform bind $var to #el to Bengali verb-final order',
7339
+ priority: 90,
7340
+ match: {
7341
+ commands: ['bind', 'বাইন্ড'],
7342
+ requiredRoles: ['action', 'patient', 'destination'],
7343
+ },
7344
+ transform: {
7345
+ // $var কে #el তে বাইন্ড
7346
+ roleOrder: ['patient', 'destination', 'action'],
7347
+ insertMarkers: true,
7348
+ },
7349
+ },
6855
7350
  ],
6856
7351
  };
6857
7352
  // =============================================================================
@@ -6984,6 +7479,61 @@ const hindiProfile = {
6984
7479
  insertMarkers: true,
6985
7480
  },
6986
7481
  },
7482
+ {
7483
+ name: 'put-into',
7484
+ description: 'Transform put X into Y to Hindi verb-final order',
7485
+ priority: 90,
7486
+ match: {
7487
+ commands: ['put', 'रखें'],
7488
+ requiredRoles: ['action', 'patient', 'destination'],
7489
+ },
7490
+ transform: {
7491
+ // "hi" को #out में रखें
7492
+ roleOrder: ['patient', 'destination', 'action'],
7493
+ insertMarkers: true,
7494
+ },
7495
+ },
7496
+ {
7497
+ name: 'set-to',
7498
+ description: 'Transform set X to Y to Hindi verb-final order',
7499
+ priority: 90,
7500
+ match: {
7501
+ commands: ['set', 'सेट'],
7502
+ requiredRoles: ['action', 'patient', 'destination'],
7503
+ // Skip a set whose value swept up a trailing block terminator (`set X to Y
7504
+ // end` — the body of an inline `if … then set … end`): verb-final reorder
7505
+ // would push the verb past `end` and break the block. These parse fine
7506
+ // verb-medial (the default).
7507
+ predicate: parsed => !/(^|\s)end$/i.test((parsed.original ?? '').trim()),
7508
+ },
7509
+ transform: {
7510
+ // #x.innerText को इसका.name में सेट — verb-final, matching the generated
7511
+ // SOV set pattern (set schema markerOverride puts को on the destination /
7512
+ // में on the value). The default reorder emitted verb-MEDIAL
7513
+ // (`#x को सेट … में`), which no set pattern matched → bare-event fallback.
7514
+ roleOrder: ['patient', 'destination', 'action'],
7515
+ insertMarkers: true,
7516
+ },
7517
+ },
7518
+ {
7519
+ name: 'bind-to',
7520
+ description: 'Transform bind $var to #el to Hindi verb-final order',
7521
+ priority: 90,
7522
+ match: {
7523
+ commands: ['bind', 'बाइंड', 'बांधें'],
7524
+ requiredRoles: ['action', 'patient', 'destination'],
7525
+ },
7526
+ transform: {
7527
+ // $greeting को #name-input में bind — verb-final, matching the generated SOV
7528
+ // bind pattern (`{destination} को {source} में bind`). The default reorder
7529
+ // emitted verb-MEDIAL (`$greeting को bind #name-input में`), which the
7530
+ // verb-final pattern never matched → the bare-event fallback mis-anchored
7531
+ // the fronted `$greeting` as a phantom `on` event (the rf=0.00 bind residue).
7532
+ // ja/ko/zh/tr/bn already carry this rule; hi was the only SOV gap.
7533
+ roleOrder: ['patient', 'destination', 'action'],
7534
+ insertMarkers: true,
7535
+ },
7536
+ },
6987
7537
  ],
6988
7538
  };
6989
7539
  // =============================================================================
@@ -7001,7 +7551,11 @@ const tagalogProfile = {
7001
7551
  markers: [
7002
7552
  { form: 'kapag', role: 'event', position: 'preposition', required: true },
7003
7553
  { form: 'sa', role: 'destination', position: 'preposition', required: false },
7004
- { form: 'mula sa', role: 'source', position: 'preposition', required: false },
7554
+ // mula_sa (underscore) matches the tl dict and the semantic profile's
7555
+ // source marker — the spaced form emitted two tokens the generated
7556
+ // patterns' single mula_sa literal could never match, so `remove X
7557
+ // mula sa Y` lost its source and the schema default fabricated me.
7558
+ { form: 'mula_sa', role: 'source', position: 'preposition', required: false },
7005
7559
  { form: 'nang', role: 'style', position: 'preposition', required: false },
7006
7560
  { form: 'bilang', role: 'method', position: 'preposition', required: false },
7007
7561
  ],
@@ -7091,6 +7645,32 @@ const hebrewProfile = {
7091
7645
  { form: 'עם', role: 'style', position: 'preposition', required: false },
7092
7646
  { form: 'כ', role: 'method', position: 'preposition', required: false },
7093
7647
  ],
7648
+ rules: [
7649
+ {
7650
+ name: 'bind-to',
7651
+ description: 'Bind binds a variable to an element: קשור X ל Y',
7652
+ priority: 90,
7653
+ match: {
7654
+ commands: ['bind', 'קשור'],
7655
+ requiredRoles: ['action', 'patient', 'destination'],
7656
+ },
7657
+ transform: {
7658
+ // The bound variable is the semantic destination (kept bare, no את
7659
+ // object marker), and ל marks the target element. Custom so the
7660
+ // variable does not pick up את and the element uses ל (not על).
7661
+ roleOrder: ['action', 'patient', 'destination'],
7662
+ custom: parsed => {
7663
+ const action = parsed.roles.get('action');
7664
+ const patient = parsed.roles.get('patient');
7665
+ const destination = parsed.roles.get('destination');
7666
+ const verb = action?.translated || action?.value || '';
7667
+ const v = patient?.translated || patient?.value || '';
7668
+ const d = destination?.translated || destination?.value || '';
7669
+ return [verb, v, 'ל', d].filter(Boolean).join(' ');
7670
+ },
7671
+ },
7672
+ },
7673
+ ],
7094
7674
  };
7095
7675
  // =============================================================================
7096
7676
  // Profile Registry
@@ -7107,6 +7687,7 @@ const profiles = {
7107
7687
  fr: frenchProfile,
7108
7688
  pt: portugueseProfile,
7109
7689
  id: indonesianProfile,
7690
+ ms: malayProfile,
7110
7691
  qu: quechuaProfile,
7111
7692
  sw: swahiliProfile,
7112
7693
  bn: bengaliProfile,
@@ -7585,6 +8166,95 @@ function getCommandKeywordsForLocale(locale) {
7585
8166
  }
7586
8167
  return keywords;
7587
8168
  }
8169
+ /**
8170
+ * English copula forms. A command keyword IMMEDIATELY after one of these is a
8171
+ * predicate adjective, not a command verb: in `if my value is empty add .error
8172
+ * to me` the `empty` belongs to the condition (`empty` is also a hyperscript
8173
+ * command, v0.9.90). Without this guard the condition/body scans cut the
8174
+ * condition at `is` and displace the adjective into the body's argument zone,
8175
+ * where it anchors a spurious `empty-{lang}-generated` parse AND steals a
8176
+ * neighboring role (the empty ×8 bn/hi/tr family). Source-locale copulas are
8177
+ * added via the dictionary in `isPredicateAdjectivePosition`.
8178
+ */
8179
+ const EN_COPULAS = ['is', 'are', 'was', 'were', 'am', 'be'];
8180
+ function getCopulasForLocale(locale) {
8181
+ const copulas = new Set(EN_COPULAS);
8182
+ if (locale !== 'en') {
8183
+ for (const form of EN_COPULAS) {
8184
+ copulas.add(translateWord(form, 'en', locale).toLowerCase());
8185
+ }
8186
+ }
8187
+ return copulas;
8188
+ }
8189
+ /**
8190
+ * True when tokens[i] sits right after a copula — a predicate-adjective
8191
+ * position that must never be read as the start of a body command.
8192
+ */
8193
+ function isPredicateAdjectivePosition(tokens, i, copulas) {
8194
+ const prev = tokens[i - 1]?.toLowerCase();
8195
+ return !!prev && copulas.has(prev);
8196
+ }
8197
+ /**
8198
+ * Source-locale surface forms of the `for` command keyword and the loop's
8199
+ * `in` preposition, for the loop-head test below.
8200
+ */
8201
+ function getForLoopWordsForLocale(locale) {
8202
+ const forWords = new Set(['for']);
8203
+ const inWords = new Set(['in']);
8204
+ if (locale !== 'en') {
8205
+ forWords.add(translateWord('for', 'en', locale).toLowerCase());
8206
+ inWords.add(translateWord('in', 'en', locale).toLowerCase());
8207
+ }
8208
+ return { forWords, inWords };
8209
+ }
8210
+ /**
8211
+ * True when a `for` at tokens[i] heads a real loop. Hyperscript's only
8212
+ * statement-head `for` is `for <var> in <iterable>`, so a `for` with no `in`
8213
+ * among the tokens before the next command keyword is a ROLE PHRASE — take's
8214
+ * target (`take .active from .tab-button for me`) or a duration (`for 2s`) —
8215
+ * and must never split the statement: the split shattered the phrase into a
8216
+ * dangling `then for me` clause, which six SOV languages then parsed as a
8217
+ * spurious `for` loop with patient "me" (the take-class ×6 family).
8218
+ */
8219
+ function isLoopHeadFor(tokens, i, inWords, commandKeywords) {
8220
+ for (let j = i + 1; j < tokens.length; j++) {
8221
+ const lt = tokens[j].toLowerCase();
8222
+ if (inWords.has(lt))
8223
+ return true;
8224
+ if (commandKeywords.has(lt))
8225
+ return false;
8226
+ }
8227
+ return false;
8228
+ }
8229
+ /**
8230
+ * Repair a FRONTED Hebrew accusative marker in transformed output.
8231
+ *
8232
+ * When an event-handler body leads with a command-modifier (`on click once add …`,
8233
+ * via {@link GrammarTransformer.tryTransformEventWithModifierBody}) or is a control
8234
+ * block (`on blur if … add … end`, via `tryTransformEventWithBlockBody`), the body
8235
+ * command's accusative marker את can be emitted AHEAD of its verb — `add .error to me`
8236
+ * renders `… את הוסף .error …` instead of the canonical `… הוסף את .error …`. את before
8237
+ * a verb is always ungrammatical Hebrew (it only ever marks a FOLLOWING definite
8238
+ * object), and the semantic parser drops the command in every parse path (fused-event,
8239
+ * multi-clause, conditional-body) when the marker is fronted but parses it when the
8240
+ * marker follows the verb. So an `<accusative-marker> <command-verb>` adjacency is a
8241
+ * pure transformer artifact: swap it back. Idempotent and safe — only touches `את
8242
+ * <verb>`, never the ~40 generated `<verb> את {patient}` patterns that embed את legitimately.
8243
+ */
8244
+ function repairHebrewFrontedAccusative(text) {
8245
+ const ACC = 'את'; // hebrewProfile.markers patient marker
8246
+ const verbs = getCommandKeywordsForLocale('he');
8247
+ const tokens = text.split(/\s+/);
8248
+ let changed = false;
8249
+ for (let i = 0; i + 1 < tokens.length; i++) {
8250
+ if (tokens[i] === ACC && verbs.has(tokens[i + 1].toLowerCase())) {
8251
+ [tokens[i], tokens[i + 1]] = [tokens[i + 1], tokens[i]];
8252
+ changed = true;
8253
+ i++; // skip the marker we just moved
8254
+ }
8255
+ }
8256
+ return changed ? tokens.join(' ') : text;
8257
+ }
7588
8258
  /**
7589
8259
  * Detect a reactive block (`live ... end`, `when X changes Y [end]`,
7590
8260
  * `unless X Y [end]`) and decompose it. Returns `null` when the input
@@ -7642,11 +8312,14 @@ function extractBlockStructure(input, sourceLocale) {
7642
8312
  // `unless <cond> <body>`: condition runs up to the first command
7643
8313
  // keyword in `inner`. Heuristic — works because hyperscript bodies
7644
8314
  // always start with a command verb, and `unless` conditions rarely
7645
- // contain bare command keywords as values.
8315
+ // contain bare command keywords as values. A candidate right after a
8316
+ // copula (`… is empty`) is a predicate adjective inside the condition,
8317
+ // not a body verb — skip it.
7646
8318
  const commands = getCommandKeywordsForLocale(sourceLocale);
8319
+ const copulas = getCopulasForLocale(sourceLocale);
7647
8320
  let bodyStart = -1;
7648
8321
  for (let i = 0; i < inner.length; i++) {
7649
- if (commands.has(inner[i].toLowerCase())) {
8322
+ if (commands.has(inner[i].toLowerCase()) && !isPredicateAdjectivePosition(inner, i, copulas)) {
7650
8323
  bodyStart = i;
7651
8324
  break;
7652
8325
  }
@@ -7803,7 +8476,13 @@ function reconstructWithLineStructure(transformedParts, lineMetadata, partToLine
7803
8476
  * - "on <event> <command>" stays together (event handler with first command)
7804
8477
  * - Modifiers like "to", "from" don't trigger splits
7805
8478
  */
7806
- /** Modifier keywords that should not trigger command boundary splits */
8479
+ /**
8480
+ * English modifier keywords that should not trigger command boundary splits.
8481
+ * These are the base set; localized equivalents (e.g. Japanese `に`, Spanish
8482
+ * `a`, Arabic `إلى`) are layered on per source locale by
8483
+ * `getBoundaryModifiersForLocale`, so a preposition in a non-English source
8484
+ * is also recognized as a modifier rather than a spurious command boundary.
8485
+ */
7807
8486
  const BOUNDARY_MODIFIERS = new Set([
7808
8487
  'to',
7809
8488
  'into',
@@ -7817,6 +8496,81 @@ const BOUNDARY_MODIFIERS = new Set([
7817
8496
  'of',
7818
8497
  'over',
7819
8498
  ]);
8499
+ /**
8500
+ * Boundary modifiers resolved for a given source locale: the English base set
8501
+ * (always kept, since input may mix English keywords) plus every grammatical
8502
+ * marker form declared by the locale's profile. Profile markers are the
8503
+ * surface realizations of semantic roles (destination, source, style, …) —
8504
+ * they always bind to a following value, so none should be treated as a
8505
+ * command boundary. Cached per locale because profiles are static.
8506
+ */
8507
+ const boundaryModifiersCache = new Map();
8508
+ function getBoundaryModifiersForLocale(locale) {
8509
+ const cached = boundaryModifiersCache.get(locale);
8510
+ if (cached)
8511
+ return cached;
8512
+ const modifiers = new Set(BOUNDARY_MODIFIERS);
8513
+ const profile = getProfile(locale);
8514
+ profile?.markers.forEach(marker => {
8515
+ const form = marker.form.replace(/^-|-$/g, '').toLowerCase();
8516
+ if (form)
8517
+ modifiers.add(form);
8518
+ marker.alternatives?.forEach(alt => {
8519
+ const altForm = alt.replace(/^-|-$/g, '').toLowerCase();
8520
+ if (altForm)
8521
+ modifiers.add(altForm);
8522
+ });
8523
+ });
8524
+ boundaryModifiersCache.set(locale, modifiers);
8525
+ return modifiers;
8526
+ }
8527
+ /**
8528
+ * Commands for which a trailing `on <element>` is the TARGET the command acts
8529
+ * on (a destination), not a new event-handler clause. For these, the locative
8530
+ * `on` must neither split the statement (`splitOnCommandBoundaries`) nor be read
8531
+ * as a fresh `event` role (`buildArgumentModifierMap`) — see those call sites.
8532
+ *
8533
+ * Deliberately narrow. `on` is overloaded (event-handler head vs. locative
8534
+ * target), and the change has cross-language blast radius, so we only enable
8535
+ * the locative reading for the DOM class/attribute mutators where it's the
8536
+ * documented idiom (`toggle .open on #menu`, `toggle @hidden on #panel`).
8537
+ *
8538
+ * `trigger`/`send` — `trigger X on Y` / `send X on Y` fire an event on a target
8539
+ * element; the trailing `on Y` is that target (destination), not a new clause.
8540
+ * Previously excluded: keeping `on Y` attached produced `trigger X → #y` output
8541
+ * that was thought to destabilise the semantic parser's multi-line behaviour
8542
+ * fallback (behavior-sortable's `trigger sortable:start on me`). That concern is
8543
+ * stale — the recent semantic-parser body increments fold the surrounding block
8544
+ * cleanly, and splitting instead injected a spurious `then` (`disparar
8545
+ * sortable:start entonces en yo`) that glued the following `repeat until event`
8546
+ * loop into a then-chain and dropped it. Keeping `on Y` attached restores
8547
+ * behavior-sortable to faithful across the SVO languages.
8548
+ *
8549
+ * Excluded on purpose:
8550
+ * - `set` — `set @attr to V on Y` carries BOTH `to` (value) and `on` (target);
8551
+ * English marks both as `destination`, so remapping `on` would collide with
8552
+ * and clobber the value. Needs distinct value/target roles first (deferred).
8553
+ * - `put` — `put X on Y into Z` has the same dual-destination collision.
8554
+ *
8555
+ * `add`/`remove` use `to`/`from` for their target in practice (not `on`), so
8556
+ * their inclusion is a harmless no-op that documents intent.
8557
+ */
8558
+ const ON_TARGET_COMMANDS = new Set(['toggle', 'add', 'remove', 'trigger', 'send']);
8559
+ /**
8560
+ * Find the command verb of a partially-collected statement: the first command
8561
+ * keyword that is neither the event-handler head (`on`/その他 event markers) nor
8562
+ * an argument-introducing preposition. For `on click toggle .open` → `toggle`;
8563
+ * for `trigger sortable:start` → `trigger`.
8564
+ */
8565
+ function commandVerbOf(tokens, commandKeywords) {
8566
+ for (const token of tokens) {
8567
+ const lt = token.toLowerCase();
8568
+ if (commandKeywords.has(lt) && !BOUNDARY_MODIFIERS.has(lt) && !EVENT_KEYWORDS.has(lt)) {
8569
+ return lt;
8570
+ }
8571
+ }
8572
+ return null;
8573
+ }
7820
8574
  /**
7821
8575
  * Block-introducing keywords whose body should not be split at command
7822
8576
  * boundaries by `splitOnCommandBoundaries`. Inputs starting with one of
@@ -7829,8 +8583,26 @@ const BOUNDARY_MODIFIERS = new Set([
7829
8583
  * `splitOnThen` + `parseConditional` path.
7830
8584
  */
7831
8585
  const BLOCK_HEAD_KEYWORDS = new Set(['live', 'when', 'unless']);
8586
+ /**
8587
+ * Source-language block-introducing command keywords whose body is a clause plus a
8588
+ * branch/loop body (harness source is English, so these are English-keyed). Used to
8589
+ * locate a block body inside an event handler and to track block depth when finding
8590
+ * a top-level `else`.
8591
+ */
8592
+ const BLOCK_BODY_KEYWORDS = new Set(['if', 'repeat', 'unless', 'while', 'for']);
8593
+ /**
8594
+ * SVO targets that mark the object/patient with a particle (he את, zh 把) and so
8595
+ * mangle an inline `on <event> unless <cond> <body>` guard — the unless tail is
8596
+ * swept into one patient blob and the marker lands ahead of the condition. These
8597
+ * route through `tryTransformEventWithUnlessGuard`. SOV/VSO object-markers
8598
+ * (ja/ko/tr/ar) are excluded: their event does not lead, so the SVO event-first
8599
+ * emission there would be wrong (and they don't exhibit the artifact today).
8600
+ */
8601
+ const UNLESS_GUARD_OBJECT_MARKING_LOCALES = new Set(['he', 'zh']);
7832
8602
  function splitOnCommandBoundaries(input, sourceLocale) {
7833
8603
  const commandKeywords = getCommandKeywordsForLocale(sourceLocale);
8604
+ const boundaryModifiers = getBoundaryModifiersForLocale(sourceLocale);
8605
+ const { forWords, inWords } = getForLoopWordsForLocale(sourceLocale);
7834
8606
  const tokens = input.split(/\s+/);
7835
8607
  if (tokens.length === 0)
7836
8608
  return [input];
@@ -7876,7 +8648,48 @@ function splitOnCommandBoundaries(input, sourceLocale) {
7876
8648
  currentPart.push(token);
7877
8649
  continue;
7878
8650
  }
7879
- if (!BOUNDARY_MODIFIERS.has(prevLower) && !commandKeywords.has(prevLower)) {
8651
+ // A `for` that doesn't head a real loop (`for <var> in <iterable>`) is a
8652
+ // role phrase of the current command — see isLoopHeadFor.
8653
+ if (forWords.has(lowerToken) && !isLoopHeadFor(tokens, i, inWords, commandKeywords)) {
8654
+ currentPart.push(token);
8655
+ continue;
8656
+ }
8657
+ // Locative `on` for a DOM-target command (`toggle X on Y`) is the target
8658
+ // element, NOT a new command. `on` lands in `commandKeywords` only
8659
+ // incidentally — the EN dictionary registers `commands.on = 'on'` for the
8660
+ // event-handler head — so without this guard the destination `on` split
8661
+ // the statement, the join re-inserted a spurious `then` (ثم/pagkatapos/…),
8662
+ // and the dangling `on Y` was misread as a second event handler. Keep it
8663
+ // attached so the role parser can assign it to `destination`. Restricted
8664
+ // to ON_TARGET_COMMANDS so `trigger X on me` etc. keep their prior split.
8665
+ if (BOUNDARY_MODIFIERS.has(lowerToken)) {
8666
+ const verb = commandVerbOf(currentPart, commandKeywords);
8667
+ if (verb && ON_TARGET_COMMANDS.has(verb)) {
8668
+ currentPart.push(token);
8669
+ continue;
8670
+ }
8671
+ // `set @attr to V on <scope>` (S1 tabs-aria): the trailing `on <scope>`
8672
+ // is the element(s) the attribute is set on, not a new command. `set` is
8673
+ // deliberately NOT in ON_TARGET_COMMANDS (its role parser would clobber
8674
+ // the value), so this is a dedicated guard — kept attached only when a
8675
+ // selector/reference scope follows, then positioned by transformSingle's
8676
+ // set-scope handler (transformSetWithScope). A `set` whose `on` is
8677
+ // followed by a verb is left to split as before.
8678
+ if (lowerToken === 'on' && verb === 'set') {
8679
+ const nextTok = tokens[i + 1];
8680
+ const nextLower = nextTok?.toLowerCase();
8681
+ const scopeLike = !!nextTok &&
8682
+ (/^[#.<@[]/.test(nextTok) ||
8683
+ nextLower === 'me' ||
8684
+ nextLower === 'it' ||
8685
+ nextLower === 'you');
8686
+ if (scopeLike) {
8687
+ currentPart.push(token);
8688
+ continue;
8689
+ }
8690
+ }
8691
+ }
8692
+ if (!boundaryModifiers.has(prevLower) && !commandKeywords.has(prevLower)) {
7880
8693
  // This looks like a command boundary - save current part and start new one
7881
8694
  parts.push(currentPart.join(' '));
7882
8695
  currentPart = [token];
@@ -7963,6 +8776,29 @@ function deriveEventKeywordsFromProfiles() {
7963
8776
  }
7964
8777
  /** Event keywords derived from language profiles */
7965
8778
  const EVENT_KEYWORDS = deriveEventKeywordsFromProfiles();
8779
+ /**
8780
+ * Conjunctions that join multiple events in an event handler head
8781
+ * (`on click or keypress ...`). Source hyperscript is English, so the
8782
+ * canonical form is `or`; localized equivalents are added defensively for
8783
+ * non-English sources.
8784
+ */
8785
+ const EVENT_CONJUNCTIONS = new Set(['or']);
8786
+ /**
8787
+ * Command-modifier keywords that may lead an event handler body
8788
+ * (`on click async fetch …`, `on click once add …`). They modify the handler /
8789
+ * following command rather than acting as the verb, so the transformer must lift
8790
+ * them out before role assignment instead of treating them as the action.
8791
+ * Source hyperscript is English, so these are the canonical English forms — the
8792
+ * semantic parser recognizes the same literals when stripping them pre-parse.
8793
+ */
8794
+ const BODY_MODIFIER_KEYWORDS = new Set([
8795
+ 'async',
8796
+ 'once',
8797
+ 'debounced',
8798
+ 'debounce',
8799
+ 'throttled',
8800
+ 'throttle',
8801
+ ]);
7966
8802
  // =============================================================================
7967
8803
  // Helper: Dynamic Modifier Map
7968
8804
  // =============================================================================
@@ -7996,6 +8832,48 @@ function generateModifierMap(profile) {
7996
8832
  }
7997
8833
  return map;
7998
8834
  }
8835
+ /**
8836
+ * Modifier map for parsing the ARGUMENTS of a statement (everything after the
8837
+ * command verb), as opposed to the statement head.
8838
+ *
8839
+ * Why a separate map: a few languages reuse one word for both the event-handler
8840
+ * head marker and a locative/target preposition. English is the prime case —
8841
+ * `on` is the event head (`on click …`) AND the toggle/set target preposition
8842
+ * (`toggle .x on #y`). `generateModifierMap` maps `on → event` (from the EN
8843
+ * profile's event marker), so when the destination `on` was read in argument
8844
+ * position it overwrote the already-captured head event (the `click` got
8845
+ * dropped, e.g. `toggle @hidden on #panel` → `… عند #panel` with نقر/click gone).
8846
+ *
8847
+ * The fix: in argument position, remap the event role to `destination`. This is
8848
+ * safe because a trigger event only ever appears at the statement head (which is
8849
+ * consumed before argument parsing begins). Any "event marker" reached while
8850
+ * scanning arguments is therefore being used as a locative — i.e. the element
8851
+ * the command acts ON — which is exactly the `destination` role (the semantic
8852
+ * parser also models `toggle .x on #y` as patient `.x` + destination `#y`).
8853
+ *
8854
+ * Restricted to (a) SVO source profiles and (b) ON_TARGET_COMMANDS:
8855
+ * - SVO: in VSO/SOV languages the event marker can legitimately appear
8856
+ * mid-statement (Arabic VSO renders an event handler as `بدل X عند نقر`,
8857
+ * classified as a command), so remapping there would mis-read a real event
8858
+ * as a destination. English — and the en→lang gate path — is SVO.
8859
+ * - command: only the DOM class/attr mutators use a locative `on` target.
8860
+ * For other commands (`trigger X on me`, `set X to V on Y`) the remap is
8861
+ * either destabilising or collides with `to`/`into` — see ON_TARGET_COMMANDS.
8862
+ *
8863
+ * When neither applies the unmodified map is returned (event stays event).
8864
+ */
8865
+ function buildArgumentModifierMap(profile, actionVerb) {
8866
+ const map = generateModifierMap(profile);
8867
+ const verb = actionVerb?.toLowerCase();
8868
+ if (profile.wordOrder !== 'SVO' || !verb || !ON_TARGET_COMMANDS.has(verb)) {
8869
+ return map;
8870
+ }
8871
+ const remapped = {};
8872
+ for (const [form, role] of Object.entries(map)) {
8873
+ remapped[form] = role === 'event' ? 'destination' : role;
8874
+ }
8875
+ return remapped;
8876
+ }
7999
8877
  // =============================================================================
8000
8878
  // Statement Parser
8001
8879
  // =============================================================================
@@ -8102,6 +8980,8 @@ function tokenize(input, profile) {
8102
8980
  let current = '';
8103
8981
  let inSelector = false;
8104
8982
  let selectorDepth = 0;
8983
+ let bracketDepth = 0;
8984
+ let parenDepth = 0;
8105
8985
  for (let i = 0; i < input.length; i++) {
8106
8986
  const char = input[i];
8107
8987
  // Track CSS selector context
@@ -8114,8 +8994,41 @@ function tokenize(input, profile) {
8114
8994
  if (selectorDepth === 0)
8115
8995
  inSelector = false;
8116
8996
  }
8117
- // Split on whitespace unless in selector
8118
- if (/\s/.test(char) && !inSelector) {
8997
+ // Track event-guard / attribute brackets so `[key is 'Escape']` (which has
8998
+ // internal spaces) stays a single token instead of splitting into
8999
+ // `[key` / `is` / `'Escape']` — which mis-assigns `is` as the action verb.
9000
+ if (char === '[') {
9001
+ bracketDepth++;
9002
+ }
9003
+ else if (char === ']' && bracketDepth > 0) {
9004
+ bracketDepth--;
9005
+ }
9006
+ // Track EVERY parenthesized group as a depth scope so it stays ONE token:
9007
+ //
9008
+ // - An ATTACHED `(` (a call or event destructure: `pointerdown(clientX,
9009
+ // clientY)`, `Resizable(a, b)`) must not split at the comma-space — the
9010
+ // event-handler-head reorder would separate the halves and drop the
9011
+ // whole handler (tl behavior-resizable degenerate).
9012
+ // - A STANDALONE `(` opening an expression (`to (the value of #price as
9013
+ // Number) * (my value as Number)`) must be OPAQUE to role segmentation:
9014
+ // left loose, its interior `of`/`as`/`from` keywords hit the argument
9015
+ // modifier map, so the parser split the expression across roles — the
9016
+ // R1 cluster E mangle (computed-value: the transformer reordered INSIDE
9017
+ // the parens, embedded the event phrase mid-expression, and dropped the
9018
+ // whole second operand in every language). Interior keywords still
9019
+ // translate IN PLACE: role values are re-split on whitespace by
9020
+ // translateMultiWordValue, and translateWord strips paren punctuation
9021
+ // before its dictionary lookup (`($count or 0)` → `($count o 0)` in tl —
9022
+ // the operator translates without the group being torn apart).
9023
+ if (char === '(') {
9024
+ parenDepth++;
9025
+ }
9026
+ else if (char === ')' && parenDepth > 0) {
9027
+ parenDepth--;
9028
+ }
9029
+ // Split on whitespace unless inside a selector, a bracket guard, or an
9030
+ // attached parenthesized argument list
9031
+ if (/\s/.test(char) && !inSelector && bracketDepth === 0 && parenDepth === 0) {
8119
9032
  if (current) {
8120
9033
  tokens.push(current);
8121
9034
  current = '';
@@ -8165,11 +9078,26 @@ function parseEventHandler(tokens, profile) {
8165
9078
  let startIndex = EVENT_KEYWORDS.has(tokens[0]?.toLowerCase()) ? 1 : 0;
8166
9079
  // Next token is the event
8167
9080
  if (tokens[startIndex]) {
9081
+ const eventTokens = [tokens[startIndex]];
9082
+ startIndex++;
9083
+ // Fold "or"-conjoined events into the event value (e.g.
9084
+ // "on click or keypress[...] toggle .active"). Without this, "or" would be
9085
+ // read as the action verb and the second event swept into role values,
9086
+ // hoisting "or <event2>" ahead of the command on reorder. Keeping the whole
9087
+ // "<event1> or <event2>" string in the event role lets it translate and
9088
+ // reorder as a single event clause. Only consumes "or" that immediately
9089
+ // follows the event, before any source/action — so a later "or" in a guard
9090
+ // or value is untouched.
9091
+ while (tokens[startIndex] &&
9092
+ EVENT_CONJUNCTIONS.has(tokens[startIndex].toLowerCase()) &&
9093
+ tokens[startIndex + 1]) {
9094
+ eventTokens.push(tokens[startIndex], tokens[startIndex + 1]);
9095
+ startIndex += 2;
9096
+ }
8168
9097
  roles.set('event', {
8169
9098
  role: 'event',
8170
- value: tokens[startIndex],
9099
+ value: eventTokens.join(' '),
8171
9100
  });
8172
- startIndex++;
8173
9101
  }
8174
9102
  // Check for event source modifier before the action (e.g., "from #source" in "on input from #firstName set ...")
8175
9103
  // Only handle 'from' (event source) — other modifiers like circumfix markers (Chinese 时) should not be consumed.
@@ -8201,9 +9129,12 @@ function parseEventHandler(tokens, profile) {
8201
9129
  startIndex++;
8202
9130
  }
8203
9131
  // Parse remaining tokens with modifier awareness (like parseCommand does)
8204
- // This handles "by 3" in "on click increment #count by 3"
9132
+ // This handles "by 3" in "on click increment #count by 3".
9133
+ // Argument map: for DOM-target commands the head event is already captured, so
9134
+ // a locative `on` (`toggle @hidden on #panel`) maps to `destination` rather
9135
+ // than overwriting the head `event`.
8205
9136
  if (tokens[startIndex]) {
8206
- const modifierMap = generateModifierMap(profile);
9137
+ const modifierMap = buildArgumentModifierMap(profile, roles.get('action')?.value);
8207
9138
  let currentRole = 'patient';
8208
9139
  let currentValue = [];
8209
9140
  for (let i = startIndex; i < tokens.length; i++) {
@@ -8256,9 +9187,11 @@ function parseCommand(tokens, profile) {
8256
9187
  role: 'action',
8257
9188
  value: tokens[0],
8258
9189
  });
8259
- // Generate dynamic modifier map from language profile
8260
- // This enables parsing non-English input (e.g., Japanese に, Korean 에, Arabic إلى)
8261
- const modifierMap = generateModifierMap(profile);
9190
+ // Generate dynamic modifier map from language profile (argument position).
9191
+ // This enables parsing non-English input (e.g., Japanese に, Korean 에, Arabic إلى).
9192
+ // For a DOM-target command a locative `on` (`toggle .active on me`) maps to
9193
+ // `destination` (SVO sources) rather than spawning a bogus `event` role.
9194
+ const modifierMap = buildArgumentModifierMap(profile, tokens[0]);
8262
9195
  let currentRole = 'patient';
8263
9196
  let currentValue = [];
8264
9197
  for (let i = 1; i < tokens.length; i++) {
@@ -8296,6 +9229,72 @@ function parseCommand(tokens, profile) {
8296
9229
  original: tokens.join(' '),
8297
9230
  };
8298
9231
  }
9232
+ /**
9233
+ * Re-assign a command's mis-marked primary argument from the default `patient`
9234
+ * role to its true primary role (e.g. `wait`'s leading argument is a `duration`,
9235
+ * not a fronted object).
9236
+ *
9237
+ * The generic argument parser in `parseCommand` / `parseEventHandler` defaults the
9238
+ * first unmarked argument to `patient`. For most commands that is correct, but for a
9239
+ * command whose primary role is a non-patient *and which the target language does not
9240
+ * give a marker* (a duration, a measure — never a BA/object construction), the
9241
+ * `patient` assignment makes `insertMarkers` emit a spurious object-marker (Chinese
9242
+ * `把`, Japanese `を`, Korean `를`). The marked form is ungrammatical and the semantic
9243
+ * parser fails to match it, dropping the command.
9244
+ *
9245
+ * The fix is deliberately scoped to **literal/measure** primary roles
9246
+ * ({@link LITERAL_PRIMARY_ROLES}: `duration`, `quantity`) — values that are *never*
9247
+ * the object of an object-marking construction in any supported language, so moving
9248
+ * them off `patient` can only ever *remove* a spurious marker. Marker-bearing
9249
+ * primaries are intentionally left alone:
9250
+ * - `set`→destination `到`, `fetch`→source `从` carry a *correct* marker; touching
9251
+ * them risks no benefit.
9252
+ * - `send`/`trigger`→event: in a language without an event marker (e.g. Korean has
9253
+ * no event particle) un-marking the leading argument makes the semantic parser
9254
+ * mis-read it as a bare event handler and emit a phantom `on` action — an
9255
+ * over-generation that recall-based fidelity would not catch. So `event`-primary
9256
+ * commands stay on the `patient` default.
9257
+ *
9258
+ * Roles are still reordered by the safety-net in `reorderRoles`, so no value is lost.
9259
+ * A belt-and-suspenders target-marker guard keeps the change inert should a profile
9260
+ * ever add a marker for one of these literal roles.
9261
+ *
9262
+ * Must run *before* `translateElements`, while the `action` value is still the
9263
+ * source-language (English) keyword, so the schema lookup resolves.
9264
+ *
9265
+ * @see docs-internal/ZH_BLOCK_BODY_SCOPE.md (#1 — transformer role model)
9266
+ */
9267
+ const LITERAL_PRIMARY_ROLES = new Set([
9268
+ 'duration',
9269
+ 'quantity',
9270
+ ]);
9271
+ function applyPrimaryRole(parsed, targetProfile) {
9272
+ // Only re-mark standalone command statements. In an event handler (`on click
9273
+ // wait 2s …`) a verb-final SOV language without an event particle (e.g. Korean)
9274
+ // relies on the leading argument's patient marker as the structural cue that
9275
+ // anchors the handler; un-marking it makes the semantic parser lose the event.
9276
+ // The block-body / then-chain `wait` clauses this fix targets are each parsed as
9277
+ // their own command statement, so they are still covered.
9278
+ if (parsed.type !== 'command')
9279
+ return;
9280
+ const action = parsed.roles.get('action')?.value;
9281
+ if (!action)
9282
+ return;
9283
+ const primaryRole = COMMAND_PRIMARY_ROLES[action.toLowerCase()];
9284
+ if (!primaryRole || !LITERAL_PRIMARY_ROLES.has(primaryRole))
9285
+ return;
9286
+ // Only act on a leading argument the generic parser defaulted to `patient`,
9287
+ // and only when the primary slot isn't already filled by an explicit marker.
9288
+ const patientEl = parsed.roles.get('patient');
9289
+ if (!patientEl || parsed.roles.has(primaryRole))
9290
+ return;
9291
+ // Guard: never introduce a marker that wasn't there. If the target language
9292
+ // marks the primary role, leave the command as-is.
9293
+ if (targetProfile.markers.some(m => m.role === primaryRole))
9294
+ return;
9295
+ parsed.roles.delete('patient');
9296
+ parsed.roles.set(primaryRole, { ...patientEl, role: primaryRole });
9297
+ }
8299
9298
  /**
8300
9299
  * Parse a conditional statement
8301
9300
  */
@@ -8315,6 +9314,18 @@ function parseConditional(tokens, _profile) {
8315
9314
  value: conditionValue,
8316
9315
  });
8317
9316
  }
9317
+ else if (thenIndex === -1 && tokens.length > 1) {
9318
+ // Block-style `if <cond>` with the body on following lines (no inline
9319
+ // `then`). Capture everything after `if` as the condition; otherwise the
9320
+ // condition is silently dropped and the rendered block becomes a bare
9321
+ // `if`/`אם`/`如果`, which the semantic block parser then rejects (null
9322
+ // parse). This is the dominant failure for nested control-flow bodies in
9323
+ // non-Latin languages (he, zh).
9324
+ roles.set('condition', {
9325
+ role: 'condition',
9326
+ value: tokens.slice(1).join(' '),
9327
+ });
9328
+ }
8318
9329
  return {
8319
9330
  type: 'conditional',
8320
9331
  roles,
@@ -8336,6 +9347,24 @@ function translateWord(word, sourceLocale, targetLocale) {
8336
9347
  if (/^\d+/.test(word)) {
8337
9348
  return word;
8338
9349
  }
9350
+ // A whole parenthesized group fused by the tokenizer (`(the value of #price
9351
+ // as Number)`) reaching a single-token translate path: translate its
9352
+ // interior word-by-word IN ORDER — never reordered, never re-segmented.
9353
+ if (/\s/.test(word) && word.startsWith('(')) {
9354
+ return word
9355
+ .split(/\s+/)
9356
+ .map(w => translateWord(w, sourceLocale, targetLocale))
9357
+ .join(' ');
9358
+ }
9359
+ // A word carrying paren punctuation from a fused group after whitespace
9360
+ // re-splitting (`(my` / `valor)` / `(($x`): strip the parens for the
9361
+ // dictionary lookup and re-attach, so interior keywords still translate.
9362
+ if (word.length > 1 && (word.startsWith('(') || word.endsWith(')'))) {
9363
+ const m = word.match(/^(\(*)([^()]+)(\)*)$/);
9364
+ if (m && (m[1] || m[3])) {
9365
+ return m[1] + translateWord(m[2], sourceLocale, targetLocale) + m[3];
9366
+ }
9367
+ }
8339
9368
  const sourceDict = sourceLocale === 'en' ? null : dictionaries[sourceLocale];
8340
9369
  const targetDict = dictionaries[targetLocale];
8341
9370
  if (!targetDict)
@@ -8366,9 +9395,21 @@ const POSSESSIVE_MARKERS = {
8366
9395
  ko: { type: 'suffix', marker: '의' },
8367
9396
  zh: { type: 'suffix', marker: '的' },
8368
9397
  ar: { type: 'preposition', marker: 'لـ' },
8369
- tr: { type: 'suffix', marker: "'ın" },
9398
+ // Spaced genitive particle (not the glued `'ın`), so the tokenizer can split
9399
+ // it off the selector — consistent with Turkish's other spaced case markers.
9400
+ tr: { type: 'particle', marker: 'ın' },
8370
9401
  id: { type: 'preposition', marker: 'dari' },
8371
- qu: { type: 'suffix', marker: '-pa' },
9402
+ // Latin-script genitive: must be a *spaced* particle (`#picker pa`), since a
9403
+ // glued `#pickerpa` can't be split from the selector by the tokenizer the
9404
+ // way a non-Latin suffix (の/의/র) can.
9405
+ qu: { type: 'particle', marker: 'pa' },
9406
+ // Bengali SOV postposition genitive, like ja/ko — a spaced suffix the
9407
+ // tokenizer splits off as a particle. Previously absent, so it fell back to
9408
+ // the English `'s` marker and its possessive property paths never parsed.
9409
+ // (Hindi `का` is intentionally omitted: its `bind` lacks a verb-final
9410
+ // grammar rule, so fixing its possessive alone yields a wrong `on` parse —
9411
+ // tracked as separate follow-up.)
9412
+ bn: { type: 'suffix', marker: 'র' },
8372
9413
  sw: { type: 'preposition', marker: 'ya' },
8373
9414
  };
8374
9415
  /**
@@ -8405,6 +9446,10 @@ function translatePossessive(token, sourceLocale, targetLocale) {
8405
9446
  case 'suffix':
8406
9447
  // Japanese/Korean/Chinese: owner + marker (e.g., #buttonの, #button의)
8407
9448
  return `${translatedOwner}${targetMarker.marker}`;
9449
+ case 'particle':
9450
+ // Latin-script spaced genitive (Quechua `pa`): owner + space + marker
9451
+ // so the tokenizer can separate it from the selector.
9452
+ return `${translatedOwner} ${targetMarker.marker}`;
8408
9453
  case 'preposition':
8409
9454
  // Will be handled by caller - return marker + owner format
8410
9455
  // Store as special format to be processed later
@@ -8473,6 +9518,20 @@ function translatePossessiveDotNotation(value, sourceLocale, targetLocale) {
8473
9518
  * Also handles possessive dot notation like "my.textContent" → "mi.textContent".
8474
9519
  */
8475
9520
  function translateMultiWordValue(value, sourceLocale, targetLocale) {
9521
+ // Mask event-guard / attribute brackets (`[key is 'Escape']`): their contents
9522
+ // are expression syntax, not translatable keywords — translating `is` -> `ni`
9523
+ // etc. inside them breaks the guard. Restore verbatim after translation.
9524
+ if (value.includes('[')) {
9525
+ const guards = [];
9526
+ const masked = value.replace(/\[[^\]]*\]/g, match => {
9527
+ guards.push(match);
9528
+ return `${guards.length - 1}`;
9529
+ });
9530
+ if (guards.length > 0) {
9531
+ const translated = translateMultiWordValue(masked, sourceLocale, targetLocale);
9532
+ return translated.replace(/(\d+)/g, (_, n) => guards[Number(n)]);
9533
+ }
9534
+ }
8476
9535
  // If it's a single word, check for possessive then translate
8477
9536
  if (!value.includes(' ')) {
8478
9537
  // Check for possessive 's
@@ -8563,6 +9622,35 @@ function translateElements(parsed, sourceLocale, targetLocale) {
8563
9622
  }
8564
9623
  }
8565
9624
  // =============================================================================
9625
+ // Caret-scope masking (`^name on <selector>`)
9626
+ // =============================================================================
9627
+ /** Private-use sentinels bracketing a masked caret-scope index. */
9628
+ const CARET_SCOPE_OPEN = '\uE000';
9629
+ const CARET_SCOPE_CLOSE = '\uE001';
9630
+ /**
9631
+ * Match `^name on <selector>` and the scope's selector form (#id, .class,
9632
+ * <tag/>, [attr]). The `^name` is kept; only the ` on <selector>` scope is masked.
9633
+ */
9634
+ const CARET_SCOPE_RE = /(\^[A-Za-z_][\w-]*)(\s+on\s+(?:[#.][\w-]+|<[^>]*\/>|\[[^\]]+\]))/g;
9635
+ /**
9636
+ * Mask the ` on <selector>` scope of every `^name on <selector>` read behind an
9637
+ * opaque token attached to `^name`, so the overloaded `on` doesn't reach the
9638
+ * splitter / event-handler parser. Returns null when there's nothing to mask.
9639
+ */
9640
+ function maskCaretScopes(input) {
9641
+ const scopes = [];
9642
+ const masked = input.replace(CARET_SCOPE_RE, (_m, varTok, scope) => {
9643
+ const idx = scopes.length;
9644
+ scopes.push(scope);
9645
+ return `${varTok}${CARET_SCOPE_OPEN}${idx}${CARET_SCOPE_CLOSE}`;
9646
+ });
9647
+ return scopes.length > 0 ? { masked, scopes } : null;
9648
+ }
9649
+ /** Restore masked caret-scope tokens to their verbatim ` on <selector>` form. */
9650
+ function restoreCaretScopes(input, scopes) {
9651
+ return input.replace(new RegExp(`${CARET_SCOPE_OPEN}(\\d+)${CARET_SCOPE_CLOSE}`, 'g'), (_m, n) => scopes[Number(n)] ?? '');
9652
+ }
9653
+ // =============================================================================
8566
9654
  // Main Transformer
8567
9655
  // =============================================================================
8568
9656
  class GrammarTransformer {
@@ -8584,7 +9672,54 @@ class GrammarTransformer {
8584
9672
  * For multi-line input, preserves line structure (indentation, blank lines).
8585
9673
  */
8586
9674
  transform(input) {
9675
+ const out = this.transformInternal(input);
9676
+ // Hebrew: repair a fronted accusative marker the body-split heuristics can emit
9677
+ // (`… את הוסף .x …` → `… הוסף את .x …`). Applied to the assembled output; idempotent
9678
+ // across the internal recursion. See repairHebrewFrontedAccusative.
9679
+ return this.targetProfile.code === 'he' ? repairHebrewFrontedAccusative(out) : out;
9680
+ }
9681
+ transformInternal(input) {
9682
+ // Caret-scoped variable reads (`^name on <selector>`) carry a second,
9683
+ // overloaded `on` that the splitter/event-parser would mistake for an event
9684
+ // or command boundary — mangling `put ^count on #host into me`. Mask the
9685
+ // ` on <selector>` scope behind an opaque token attached to `^name` so the
9686
+ // command reorders as if the patient were a single value, then restore it.
9687
+ // `on` is kept verbatim (the semantic caret-scope matcher accepts it by raw
9688
+ // value across languages — passthrough-alignment).
9689
+ const caret = maskCaretScopes(input);
9690
+ if (caret) {
9691
+ return restoreCaretScopes(this.transform(caret.masked), caret.scopes);
9692
+ }
8587
9693
  const targetThen = getTargetThenKeyword(this.targetProfile.code);
9694
+ // Inline JS blocks (`... js <raw js> end`) must be masked BEFORE any
9695
+ // splitting/reordering: the body is raw JavaScript, not hyperscript, so it
9696
+ // must never be tokenized, translated, or word-order reordered. (Single-line
9697
+ // only here; multi-line js bodies are handled with the behavior work.)
9698
+ if (!input.includes('\n')) {
9699
+ const jsBlock = this.tryTransformJsBlock(input);
9700
+ if (jsBlock !== null)
9701
+ return jsBlock;
9702
+ // Event handler whose body leads with a command-modifier
9703
+ // (`on click async fetch …`, `on click once add …`): lift the modifier out
9704
+ // so the real verb isn't mistaken for the action and the SOV reorder keeps
9705
+ // the body patient-first (recoverable by the parser's SOV event extraction).
9706
+ const eventModifier = this.tryTransformEventWithModifierBody(input);
9707
+ if (eventModifier !== null)
9708
+ return eventModifier;
9709
+ // Event handler whose body is a block (`on <event> if/repeat/unless … end`):
9710
+ // the block body must be reordered as a self-contained unit, not shredded
9711
+ // across the event handler's role soup.
9712
+ const eventBlock = this.tryTransformEventWithBlockBody(input);
9713
+ if (eventBlock !== null)
9714
+ return eventBlock;
9715
+ // Event handler whose body is an un-terminated inline `unless` guard
9716
+ // (`on click unless I match .disabled toggle .selected`): route the guard
9717
+ // through the standalone block path so Hebrew's accusative marker lands on
9718
+ // the body command, not the condition. Hebrew-only; null elsewhere.
9719
+ const eventGuard = this.tryTransformEventWithUnlessGuard(input);
9720
+ if (eventGuard !== null)
9721
+ return eventGuard;
9722
+ }
8588
9723
  // Check if input has multi-line structure worth preserving
8589
9724
  const hasMultiLineStructure = input.includes('\n');
8590
9725
  if (hasMultiLineStructure) {
@@ -8614,11 +9749,22 @@ class GrammarTransformer {
8614
9749
  if (block) {
8615
9750
  return this.transformBlock(block);
8616
9751
  }
9752
+ // 0b. `set @attr to V on <scope>` (S1 tabs-aria): strip the trailing
9753
+ // `on <scope>`, transform the scope-less set normally, then re-insert
9754
+ // `on <scope>` where the semantic set patterns expect it.
9755
+ const setScope = this.transformSetWithScope(input);
9756
+ if (setScope !== null) {
9757
+ return setScope;
9758
+ }
8617
9759
  // 1. Parse into semantic roles
8618
9760
  const parsed = parseStatement(input, this.sourceProfile.code);
8619
9761
  if (!parsed) {
8620
9762
  return input; // Return unchanged if parsing fails
8621
9763
  }
9764
+ // 1b. Re-assign a mis-marked primary argument (e.g. `wait`'s duration) off the
9765
+ // default `patient` role so the target doesn't emit a spurious object-marker.
9766
+ // Runs before translation while `action` is still the English keyword.
9767
+ applyPrimaryRole(parsed, this.targetProfile);
8622
9768
  // 2. Translate individual words
8623
9769
  translateElements(parsed, this.sourceProfile.code, this.targetProfile.code);
8624
9770
  // 3. Find applicable rule
@@ -8640,6 +9786,411 @@ class GrammarTransformer {
8640
9786
  // 7. Join without markers (still use joinTokens for consistency)
8641
9787
  return joinTokens(reordered.map(e => e.translated || e.value));
8642
9788
  }
9789
+ /**
9790
+ * Detect and transform an inline JS block (`[on <event>] js <raw js> end`).
9791
+ *
9792
+ * The `js ... end` body is raw JavaScript: it must not be tokenized,
9793
+ * translated, or word-order reordered. We mask the whole block with a single
9794
+ * opaque placeholder, run the surrounding statement (the event-handler head,
9795
+ * if any) through the normal reorder pipeline so the placeholder lands in the
9796
+ * correct action position, then substitute the translated `js`/`end` keywords
9797
+ * around the verbatim body.
9798
+ *
9799
+ * Returns `null` (fall through to the normal path) when there is no js block,
9800
+ * no matching `end`, or trailing content after `end` (kept tight on purpose).
9801
+ */
9802
+ tryTransformJsBlock(input) {
9803
+ const src = this.sourceProfile.code;
9804
+ const dst = this.targetProfile.code;
9805
+ // The `js` / `end` keyword forms in the SOURCE language.
9806
+ const sourceJs = translateWord('js', 'en', src);
9807
+ const sourceEnd = translateWord('end', 'en', src).toLowerCase();
9808
+ const tokens = input.split(/\s+/).filter(t => t.length > 0);
9809
+ // The js command token, optionally with a `(locals)` suffix: `js`, `js(me)`.
9810
+ const escapedJs = sourceJs.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9811
+ const jsRe = new RegExp(`^${escapedJs}(\\(.*\\))?$`, 'i');
9812
+ const jsIdx = tokens.findIndex(t => jsRe.test(t));
9813
+ if (jsIdx === -1)
9814
+ return null;
9815
+ // First `end` after the js keyword closes the block (raw JS never contains a
9816
+ // bare hyperscript `end` token).
9817
+ let endIdx = -1;
9818
+ for (let i = jsIdx + 1; i < tokens.length; i++) {
9819
+ if (tokens[i].toLowerCase() === sourceEnd) {
9820
+ endIdx = i;
9821
+ break;
9822
+ }
9823
+ }
9824
+ if (endIdx === -1)
9825
+ return null;
9826
+ // Trailing content after `end` — leave for the normal path.
9827
+ if (endIdx !== tokens.length - 1)
9828
+ return null;
9829
+ const jsToken = tokens[jsIdx];
9830
+ const jsParen = jsToken.match(jsRe)?.[1] ?? '';
9831
+ const jsKeywordRaw = jsParen ? jsToken.slice(0, jsToken.length - jsParen.length) : jsToken;
9832
+ const body = tokens.slice(jsIdx + 1, endIdx).join(' ');
9833
+ const targetJs = translateWord(jsKeywordRaw, src, dst) + jsParen;
9834
+ const targetEnd = translateWord(tokens[endIdx], src, dst);
9835
+ const replacement = [targetJs, body, targetEnd].filter(s => s.length > 0).join(' ');
9836
+ const before = tokens.slice(0, jsIdx);
9837
+ // Bare `js ... end` with no leading event-handler head: emit directly.
9838
+ if (before.length === 0)
9839
+ return replacement;
9840
+ // Mask the block as one opaque action token, reorder the surrounding
9841
+ // statement, then restore the verbatim block.
9842
+ const placeholder = 'JSBLOCKPLACEHOLDER';
9843
+ const reordered = this.transformSingle([...before, placeholder].join(' '));
9844
+ if (!reordered.includes(placeholder))
9845
+ return null; // unexpected — fall through
9846
+ return reordered.replace(placeholder, replacement);
9847
+ }
9848
+ /**
9849
+ * Transform an event handler whose body is a block command
9850
+ * (`on <event> [from <src>] {if|repeat|unless|while|for} … end`).
9851
+ *
9852
+ * `parseEventHandler` would treat the block keyword as the action and sweep the
9853
+ * condition/body into role values, then reorder them — shredding the block
9854
+ * (`if event.shiftKey call submitAndContinue() end` → scattered tokens). Instead
9855
+ * we mask the whole block as an opaque action placeholder, reorder the event
9856
+ * head normally, transform the block as a self-contained unit, and restitch.
9857
+ *
9858
+ * Returns `null` (fall through) when the input isn't an event handler, has no
9859
+ * block-keyword body, or has no closing `end`.
9860
+ */
9861
+ tryTransformEventWithBlockBody(input) {
9862
+ const tokens = tokenize(input, this.sourceProfile);
9863
+ if (tokens.length === 0)
9864
+ return null;
9865
+ if (!EVENT_KEYWORDS.has(tokens[0]?.toLowerCase()))
9866
+ return null;
9867
+ let blockIdx = -1;
9868
+ for (let i = 1; i < tokens.length; i++) {
9869
+ if (BLOCK_BODY_KEYWORDS.has(tokens[i].toLowerCase())) {
9870
+ blockIdx = i;
9871
+ break;
9872
+ }
9873
+ }
9874
+ if (blockIdx <= 0)
9875
+ return null;
9876
+ // Only handle the explicitly-terminated form; un-terminated bodies
9877
+ // (`on click unless X toggle Y`) keep the existing path.
9878
+ if (tokens[tokens.length - 1].toLowerCase() !== 'end')
9879
+ return null;
9880
+ const eventHead = tokens.slice(0, blockIdx);
9881
+ const blockTokens = tokens.slice(blockIdx);
9882
+ // Event heads carrying a `from <source>` modifier (`on keydown[...] from
9883
+ // .modal if … end`): for SVO/SOV targets, route them through this block-body
9884
+ // path too — masking the block and emitting the event clause (incl. the
9885
+ // translated `from <source>`, which `transformSingle` → `parseEventHandler`
9886
+ // already reorders) first, then the transformed block. This keeps the if-block
9887
+ // body from being shredded across the event handler's roles and properly
9888
+ // translates its inner keywords (`in`/`focus`/`first`/positional); the old
9889
+ // exclusion left them English and mangled the order (this is what cleared
9890
+ // `focus-trap` in tr). VSO targets (ar, tl) are kept on the existing path:
9891
+ // there the event-first emission with a `from`-source reorders incorrectly and
9892
+ // regresses `focus-trap`/`window-keydown`, while the existing path already
9893
+ // parses them. So the `from`-source exclusion is scoped to VSO only.
9894
+ if (this.targetProfile.wordOrder === 'VSO' && eventHead.some(t => t.toLowerCase() === 'from')) {
9895
+ return null;
9896
+ }
9897
+ const placeholder = 'EVENTBLOCKPLACEHOLDER';
9898
+ const headOut = this.transformSingle([...eventHead, placeholder].join(' '));
9899
+ if (!headOut.includes(placeholder))
9900
+ return null;
9901
+ const blockOut = this.transformBlockBody(blockTokens);
9902
+ // Always emit the event clause first, then the block. An event handler's
9903
+ // event is a leading delimiter, and the semantic parser only matches a
9904
+ // block body when it follows the event — even in verb-first (VSO) languages
9905
+ // whose normal command order would push the event to the end. So strip the
9906
+ // placeholder out of the (possibly reordered) head and append the block,
9907
+ // rather than substituting in place.
9908
+ const eventClause = headOut.replace(placeholder, '').replace(/\s+/g, ' ').trim();
9909
+ return [eventClause, blockOut].filter(s => s.length > 0).join(' ');
9910
+ }
9911
+ /**
9912
+ * Transform an event handler whose body is an inline `unless` guard with NO
9913
+ * `end` (`on <event> unless <cond> <body>` — the `unless-condition` shape).
9914
+ *
9915
+ * Object-marking SVO targets (he, zh). `parseEventHandler` reads `unless` as the
9916
+ * action and sweeps the whole `<cond> <body>` tail into a single `patient` blob;
9917
+ * the target then prefixes that blob with its object marker — Hebrew's accusative
9918
+ * את (`… אלא את I match .disabled מתג .selected`) or Chinese's BA particle 把
9919
+ * (`… 除非 把 I match .disabled 切换 .selected`) — and the inner toggle loses its
9920
+ * own marker. The semantic parser can't recover the guard from that: the marker
9921
+ * ahead of the condition blocks the `unless` pattern AND the now-markerless body
9922
+ * command fails its object-marked toggle pattern, so the body collapses (`unless`
9923
+ * dropped). Marker-less languages (de/it/ar/pl) tolerate the same role-blob and
9924
+ * stay faithful, so this is an object-marker artifact, not a general parse gap.
9925
+ *
9926
+ * The standalone `unless <cond> <body>` path already produces the correct shape
9927
+ * (`extractBlockStructure` → `transformBlock`: condition kept marker-free, body
9928
+ * command keeps its marker — he `אלא I match .disabled מתג את .selected`, zh
9929
+ * `除非 I match .disabled 切换 把 .selected`). So we split the event head off,
9930
+ * transform the guard through that path, and emit the event clause first (he and
9931
+ * zh are both SVO — event leads). Returns `null` (fall through) when the input
9932
+ * isn't an object-marking event handler with an un-terminated inline `unless`
9933
+ * guard.
9934
+ */
9935
+ tryTransformEventWithUnlessGuard(input) {
9936
+ // SVO object-marking targets only — these front the unless tail with an object
9937
+ // marker (he את / zh 把) that breaks the parse. Event-leads emission below
9938
+ // assumes SVO, so SOV/VSO object-markers (ja/ko/tr/ar) are intentionally out.
9939
+ if (!UNLESS_GUARD_OBJECT_MARKING_LOCALES.has(this.targetProfile.code))
9940
+ return null;
9941
+ const tokens = tokenize(input, this.sourceProfile);
9942
+ if (tokens.length === 0)
9943
+ return null;
9944
+ if (!EVENT_KEYWORDS.has(tokens[0]?.toLowerCase()))
9945
+ return null;
9946
+ let guardIdx = -1;
9947
+ for (let i = 1; i < tokens.length; i++) {
9948
+ if (tokens[i].toLowerCase() === 'unless') {
9949
+ guardIdx = i;
9950
+ break;
9951
+ }
9952
+ }
9953
+ if (guardIdx <= 0)
9954
+ return null;
9955
+ // The terminated form (`… unless … end`) is handled by
9956
+ // tryTransformEventWithBlockBody's masking path; only take the inline guard.
9957
+ if (tokens[tokens.length - 1].toLowerCase() === 'end')
9958
+ return null;
9959
+ const eventHead = tokens.slice(0, guardIdx);
9960
+ const guard = tokens.slice(guardIdx).join(' ');
9961
+ // `unless` is a BLOCK_HEAD keyword, so transform() routes the guard through
9962
+ // extractBlockStructure → transformBlock (the standalone shape that parses).
9963
+ const guardOut = this.transform(guard);
9964
+ if (!guardOut)
9965
+ return null;
9966
+ const placeholder = 'EVENTGUARDPLACEHOLDER';
9967
+ const headOut = this.transformSingle([...eventHead, placeholder].join(' '));
9968
+ if (!headOut.includes(placeholder))
9969
+ return null;
9970
+ const eventClause = headOut.replace(placeholder, '').replace(/\s+/g, ' ').trim();
9971
+ return [eventClause, guardOut].filter(s => s.length > 0).join(' ');
9972
+ }
9973
+ /**
9974
+ * Transform an event handler whose body leads with a command-modifier
9975
+ * (`on <event> [from <src>] {async|once|debounced [at N]|throttled [at N]} <body>`).
9976
+ *
9977
+ * `parseEventHandler` reads the first token after the event as the **action**, so
9978
+ * a leading modifier is mistaken for the verb and the real verb (`fetch`/`add`) is
9979
+ * swept into the patient. For SOV targets the reorder then surfaces that verb
9980
+ * **first** (`取得 /api/data を クリック …`), and the semantic parser matches the
9981
+ * leading `<verb> <patient>` with the low-priority `*-generated-verb-first`
9982
+ * command pattern — returning a bare command and discarding the event + the rest
9983
+ * of the body (degenerate parse).
9984
+ *
9985
+ * Instead, lift the modifier out, transform the modifier-free handler through the
9986
+ * normal path (which keeps the body in canonical patient-first SOV order so the
9987
+ * event sits mid-stream and the existing SOV event-extraction recovers it), then
9988
+ * re-emit the modifier as a **leading English literal**. The semantic parser
9989
+ * strips a leading `once`/`debounced`/`throttled` (`extractStandaloneModifiers`)
9990
+ * and an `async` anywhere (`stripAsyncModifier`) before parsing, so the modifier
9991
+ * is consumed as handler metadata rather than shadowing the body.
9992
+ *
9993
+ * Returns `null` (fall through) when the input isn't an event handler or the body
9994
+ * doesn't lead with a modifier — leaving simple/Mode-B handlers byte-identical.
9995
+ */
9996
+ tryTransformEventWithModifierBody(input) {
9997
+ // The verb-first degenerate parse this works around is specific to SOV
9998
+ // reorder: only there does a leading modifier displace the patient-first
9999
+ // order and surface the verb first. SVO/VSO/V2/other targets keep the body
10000
+ // in an order the parser already handles, so leave them byte-identical.
10001
+ if (this.targetProfile.wordOrder !== 'SOV')
10002
+ return null;
10003
+ const tokens = tokenize(input, this.sourceProfile);
10004
+ if (tokens.length === 0)
10005
+ return null;
10006
+ if (!EVENT_KEYWORDS.has(tokens[0]?.toLowerCase()))
10007
+ return null;
10008
+ // Walk past the event clause head: event keyword, event token, any
10009
+ // `or`-conjoined events, and an optional `from <source>` modifier — mirroring
10010
+ // parseEventHandler's head parsing — to find where the body begins.
10011
+ let i = 1; // past the event keyword
10012
+ if (!tokens[i])
10013
+ return null;
10014
+ i++; // past the event token
10015
+ while (tokens[i] && EVENT_CONJUNCTIONS.has(tokens[i].toLowerCase()) && tokens[i + 1]) {
10016
+ i += 2;
10017
+ }
10018
+ if (tokens[i]?.toLowerCase() === 'from' && tokens[i + 1]) {
10019
+ i++; // skip 'from'
10020
+ // Collect source tokens until a command verb or a body modifier.
10021
+ while (tokens[i] &&
10022
+ !ENGLISH_COMMANDS.has(tokens[i].toLowerCase()) &&
10023
+ !BODY_MODIFIER_KEYWORDS.has(tokens[i].toLowerCase())) {
10024
+ i++;
10025
+ }
10026
+ }
10027
+ const modWord = tokens[i]?.toLowerCase();
10028
+ if (!modWord || !BODY_MODIFIER_KEYWORDS.has(modWord))
10029
+ return null;
10030
+ // Consume the modifier phrase. `debounced`/`throttled` may carry an optional
10031
+ // `at <duration>` (or a bare duration). `async`/`once` are single tokens.
10032
+ const modStart = i;
10033
+ let modEnd = i + 1;
10034
+ if (modWord !== 'async' && modWord !== 'once') {
10035
+ if (tokens[modEnd]?.toLowerCase() === 'at')
10036
+ modEnd++;
10037
+ if (tokens[modEnd] && /^\d+(ms|s|m)?$/.test(tokens[modEnd]))
10038
+ modEnd++;
10039
+ }
10040
+ const modifierPhrase = tokens.slice(modStart, modEnd).join(' ');
10041
+ const rebuilt = [...tokens.slice(0, modStart), ...tokens.slice(modEnd)].join(' ');
10042
+ // A handler with only a modifier and no body has nothing to keep patient-first.
10043
+ if (tokens.length - (modEnd - modStart) <= 2)
10044
+ return null;
10045
+ // Re-run the full transform on the modifier-free handler so then-chains and
10046
+ // juxtaposed bodies route through the existing (working) paths. The rebuilt
10047
+ // input no longer leads with a modifier, so this never re-enters here.
10048
+ const bodyOut = this.transform(rebuilt);
10049
+ return [modifierPhrase, bodyOut].filter(s => s.length > 0).join(' ');
10050
+ }
10051
+ /**
10052
+ * Transform a `set <stuff> on <scope>` clause (S1 tabs-aria). The trailing
10053
+ * `on <scope>` is the element(s) the attribute is set on — kept attached by
10054
+ * splitOnCommandBoundaries. The semantic parser captures it as a `scope` role
10055
+ * via the passthrough literal `on` (setSchema's scope markerOverride is `on`
10056
+ * in every language), so `on` is emitted verbatim and only the scope *value*
10057
+ * is translated (selectors pass through; `me`/`it`/`you` translate to the
10058
+ * native reference, which the parser also accepts).
10059
+ *
10060
+ * Positioning matches where the set patterns expect the scope: at the clause
10061
+ * end for verb-first orders (SVO/VSO), and immediately before the clause-final
10062
+ * verb for SOV (the generated SOV pattern is `{dest} {patient} on {scope}
10063
+ * {verb}`). Returns null (fall through) when there is no trailing `on <scope>`.
10064
+ *
10065
+ * Source is English in the sync-translations pipeline, so the `set` verb and
10066
+ * `on` marker are matched as English literals.
10067
+ */
10068
+ transformSetWithScope(input) {
10069
+ const src = this.sourceProfile.code;
10070
+ const dst = this.targetProfile.code;
10071
+ const m = input.match(/^(.*\bset\b.*\S)\s+on\s+([#.<@[]\S*|me|it|you)\s*$/i);
10072
+ if (!m)
10073
+ return null;
10074
+ const head = m[1];
10075
+ const scopeRaw = m[2];
10076
+ // Transform the scope-less clause via the normal path; `head` no longer ends
10077
+ // in `on <scope>`, so this never re-enters transformSetWithScope.
10078
+ const headOut = this.transformSingle(head);
10079
+ const scopeT = /^[#.<@[]/.test(scopeRaw) ? scopeRaw : translateWord(scopeRaw, src, dst);
10080
+ // SOV needs the scope positioned per how the semantic set patterns match:
10081
+ // - Event-handler set (`on <event> set …`): the dest-first SOV event-handler
10082
+ // pattern is verb-MEDIAL and carries an optional trailing `[on {scope}]`,
10083
+ // so append the scope at the clause end.
10084
+ // - Standalone then-clause set: SOV emits verb-MEDIAL (`{dest} {verb}
10085
+ // {patient}`), but the only command set pattern with scope is verb-LAST
10086
+ // (`{dest} {patient} on {scope} {verb}`). Move the medial verb to the end
10087
+ // and place `on {scope}` before it, so the generated command pattern matches.
10088
+ if (this.targetProfile.wordOrder === 'SOV') {
10089
+ const verb = translateWord('set', 'en', dst);
10090
+ const firstTok = input.trim().split(/\s+/)[0]?.toLowerCase();
10091
+ const isEventHandler = !!firstTok && EVENT_KEYWORDS.has(firstTok);
10092
+ const toks = headOut.split(/\s+/).filter(Boolean);
10093
+ if (!isEventHandler) {
10094
+ // Standalone then-clause set: SOV emits verb-MEDIAL; move the verb to the
10095
+ // end and place `on {scope}` before it so the verb-last command set
10096
+ // pattern (scope before verb) matches.
10097
+ const vIdx = toks.indexOf(verb);
10098
+ if (vIdx >= 0) {
10099
+ toks.splice(vIdx, 1);
10100
+ toks.push('on', scopeT, verb);
10101
+ return toks.join(' ');
10102
+ }
10103
+ }
10104
+ else if (toks.length > 0 && toks[toks.length - 1] === verb) {
10105
+ // Event-handler set whose verb is clause-final (e.g. qu
10106
+ // `{dest} ta {patient} man {event} pi {verb}`): the parser extracts the
10107
+ // event and matches the body as a verb-last command, so the scope must
10108
+ // sit before the verb. Verb-MEDIAL SOV event handlers (ja/ko/tr/bn/hi)
10109
+ // fall through to the append branch, where their fused event-handler set
10110
+ // pattern carries the trailing `[on {scope}]` group.
10111
+ toks.splice(toks.length - 1, 0, 'on', scopeT);
10112
+ return toks.join(' ');
10113
+ }
10114
+ return `${headOut} on ${scopeT}`;
10115
+ }
10116
+ // Verb-first (SVO/VSO/V2): append `on <scope>` at the clause end, which the
10117
+ // trailing `[on {scope}]` group on the set patterns matches.
10118
+ return `${headOut} on ${scopeT}`;
10119
+ }
10120
+ /**
10121
+ * Transform a self-contained block command (`{head} {clause?} {body} {end}`),
10122
+ * where head ∈ {if, repeat, unless, …}. The clause (condition / `until event …`)
10123
+ * runs up to the first command verb and is translated word-by-word; the body is
10124
+ * recursively transformed (so its inner commands reorder for the target); the
10125
+ * head/tail keywords are translated. The block is never word-order reordered as
10126
+ * a whole — delimiters stay at the edges regardless of target word order.
10127
+ */
10128
+ transformBlockBody(blockTokens) {
10129
+ const src = this.sourceProfile.code;
10130
+ const dst = this.targetProfile.code;
10131
+ const head = blockTokens[0];
10132
+ const hasEnd = blockTokens[blockTokens.length - 1]?.toLowerCase() === 'end';
10133
+ const tail = hasEnd ? blockTokens[blockTokens.length - 1] : '';
10134
+ const inner = blockTokens.slice(1, hasEnd ? -1 : undefined);
10135
+ // Skip predicate-adjective positions (`… is empty`): the adjective is
10136
+ // part of the condition clause, not the body's first verb — cutting there
10137
+ // displaced it into the next command's argument zone (empty ×8 bn/hi/tr).
10138
+ const commands = getCommandKeywordsForLocale(src);
10139
+ const copulas = getCopulasForLocale(src);
10140
+ let bodyStart = inner.findIndex((t, i) => commands.has(t.toLowerCase()) && !isPredicateAdjectivePosition(inner, i, copulas));
10141
+ if (bodyStart < 0)
10142
+ bodyStart = inner.length;
10143
+ const clause = inner.slice(0, bodyStart).join(' ');
10144
+ const bodyTokens = inner.slice(bodyStart);
10145
+ const headT = translateWord(head, src, dst);
10146
+ const tailT = tail ? translateWord(tail, src, dst) : '';
10147
+ const clauseT = clause ? translateMultiWordValue(clause, src, dst) : '';
10148
+ const bodyT = this.transformConditionalBody(bodyTokens);
10149
+ return [headT, clauseT, bodyT, tailT].filter(s => s.length > 0).join(' ');
10150
+ }
10151
+ /**
10152
+ * Transform an `if`/`unless` block body, splitting it at a top-level `else` into
10153
+ * a then-branch and an else-branch so each is reordered as a self-contained unit
10154
+ * and the `else` keyword itself is translated. Without this, the body is reordered
10155
+ * as one stream: `else` rides along glued to the preceding clause (and, when that
10156
+ * clause begins with a selector, is marked a selector and left *untranslated*),
10157
+ * and a spurious `then` is inserted around it — both of which break the target
10158
+ * text and the downstream parse. The split is depth-aware so an `else` belonging
10159
+ * to a nested block is not mistaken for this block's separator. Bodies without an
10160
+ * `else` transform exactly as before.
10161
+ */
10162
+ transformConditionalBody(bodyTokens) {
10163
+ const src = this.sourceProfile.code;
10164
+ const dst = this.targetProfile.code;
10165
+ const sourceElse = translateWord('else', 'en', src).toLowerCase();
10166
+ let depth = 0;
10167
+ let elseIdx = -1;
10168
+ for (let i = 0; i < bodyTokens.length; i++) {
10169
+ const t = bodyTokens[i].toLowerCase();
10170
+ if (BLOCK_BODY_KEYWORDS.has(t))
10171
+ depth++;
10172
+ else if (t === 'end' && depth > 0)
10173
+ depth--;
10174
+ else if (t === sourceElse && depth === 0) {
10175
+ elseIdx = i;
10176
+ break;
10177
+ }
10178
+ }
10179
+ if (elseIdx === -1) {
10180
+ const body = bodyTokens.join(' ');
10181
+ return body ? this.transform(body) : '';
10182
+ }
10183
+ const thenBranch = bodyTokens.slice(0, elseIdx).join(' ');
10184
+ const elseBranch = bodyTokens.slice(elseIdx + 1).join(' ');
10185
+ const elseT = translateWord(bodyTokens[elseIdx], src, dst);
10186
+ return [
10187
+ thenBranch ? this.transform(thenBranch) : '',
10188
+ elseT,
10189
+ elseBranch ? this.transform(elseBranch) : '',
10190
+ ]
10191
+ .filter(s => s.length > 0)
10192
+ .join(' ');
10193
+ }
8643
10194
  /**
8644
10195
  * Translate a reactive block by translating the head/tail/connector
8645
10196
  * via the dictionary, recursively transforming the body through the
@@ -8806,5 +10357,5 @@ const examples = {
8806
10357
  },
8807
10358
  };
8808
10359
 
8809
- export { ENGLISH_COMMANDS, ENGLISH_KEYWORDS, GrammarTransformer, LANGUAGE_FAMILY_DEFAULTS, LocaleManager, UNIVERSAL_ENGLISH_KEYWORDS, UNIVERSAL_PATTERNS, ar$1 as ar, ar$1 as arDictionary, arKeywords, arabicProfile, bn$1 as bn, bn$1 as bnDictionary, bnKeywords, chineseProfile, createEnglishProvider, createKeywordProvider, de$1 as de, de$1 as deDictionary, deKeywords, detectBrowserLocale, directMappings, en$1 as en, englishProfile, es$1 as es, es$1 as esDictionary, esKeywords, fr$1 as fr, fr$1 as frDictionary, frKeywords, frenchProfile, germanProfile, getDirectMapping, getProfile, getSupportedDirectPairs, getSupportedLocales, examples as grammarExamples, hasDirectMapping, he$1 as he, he$1 as heDictionary, heKeywords, hebrewProfile, hindiDictionary as hiDictionary, hiKeywords, hindiDictionary, id$1 as id, id$1 as idDictionary, idKeywords, indonesianProfile, insertMarkers, it$1 as it, it$1 as itDictionary, itKeywords, ja$1 as ja, ja$1 as jaDictionary, jaKeywords, japaneseProfile, joinTokens, ko$1 as ko, ko$1 as koDictionary, koKeywords, koreanProfile, ms$1 as ms, ms$1 as msDictionary, msKeywords, parseStatement, pl$1 as pl, pl$1 as plDictionary, plKeywords, portugueseProfile, profiles, pt$1 as pt, pt$1 as ptDictionary, ptKeywords, qu$1 as qu, qu$1 as quDictionary, quKeywords, quechuaProfile, reorderRoles, russianDictionary as ruDictionary, ruKeywords, russianDictionary, spanishProfile, sw$1 as sw, sw$1 as swDictionary, swKeywords, swahiliProfile, th$1 as th, th$1 as thDictionary, thKeywords, tl$1 as tl, tl$1 as tlDictionary, tlKeywords, toEnglish, toLocale, tr$1 as tr, tr$1 as trDictionary, trKeywords, transformStatement, translate, translateWordDirect, turkishProfile, ukrainianDictionary as ukDictionary, ukKeywords, ukrainianDictionary, vi$1 as vi, vi$1 as viDictionary, viKeywords, zh$1 as zh, zh$1 as zhDictionary, zhKeywords };
10360
+ export { ENGLISH_COMMANDS, ENGLISH_KEYWORDS, GrammarTransformer, LANGUAGE_FAMILY_DEFAULTS, LocaleManager, UNIVERSAL_ENGLISH_KEYWORDS, UNIVERSAL_PATTERNS, ar$1 as ar, ar$1 as arDictionary, arKeywords, arabicProfile, bn$1 as bn, bn$1 as bnDictionary, bnKeywords, chineseProfile, createEnglishProvider, createKeywordProvider, de$1 as de, de$1 as deDictionary, deKeywords, detectBrowserLocale, directMappings, en$1 as en, englishProfile, es$1 as es, es$1 as esDictionary, esKeywords, fr$1 as fr, fr$1 as frDictionary, frKeywords, frenchProfile, germanProfile, getDirectMapping, getProfile, getSupportedDirectPairs, getSupportedLocales, examples as grammarExamples, hasDirectMapping, he$1 as he, he$1 as heDictionary, heKeywords, hebrewProfile, hindiDictionary as hiDictionary, hiKeywords, hindiDictionary, id$1 as id, id$1 as idDictionary, idKeywords, indonesianProfile, insertMarkers, it$1 as it, it$1 as itDictionary, itKeywords, ja$1 as ja, ja$1 as jaDictionary, jaKeywords, japaneseProfile, joinTokens, ko$1 as ko, ko$1 as koDictionary, koKeywords, koreanProfile, malayProfile, ms$1 as ms, ms$1 as msDictionary, msKeywords, parseStatement, pl$1 as pl, pl$1 as plDictionary, plKeywords, portugueseProfile, profiles, pt$1 as pt, pt$1 as ptDictionary, ptKeywords, qu$1 as qu, qu$1 as quDictionary, quKeywords, quechuaProfile, reorderRoles, russianDictionary as ruDictionary, ruKeywords, russianDictionary, spanishProfile, sw$1 as sw, sw$1 as swDictionary, swKeywords, swahiliProfile, th$1 as th, th$1 as thDictionary, thKeywords, tl$1 as tl, tl$1 as tlDictionary, tlKeywords, toEnglish, toLocale, tr$1 as tr, tr$1 as trDictionary, trKeywords, transformStatement, translate, translateWordDirect, turkishProfile, ukrainianDictionary as ukDictionary, ukKeywords, ukrainianDictionary, vi$1 as vi, vi$1 as viDictionary, viKeywords, zh$1 as zh, zh$1 as zhDictionary, zhKeywords };
8810
10361
  //# sourceMappingURL=lokascript-i18n.mjs.map