@lokascript/i18n 2.3.0 → 2.4.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 (71) hide show
  1. package/dist/browser.cjs +626 -473
  2. package/dist/browser.cjs.map +1 -1
  3. package/dist/browser.d.cts +25 -3
  4. package/dist/browser.d.ts +25 -3
  5. package/dist/browser.js +623 -474
  6. package/dist/browser.js.map +1 -1
  7. package/dist/dictionaries/index.cjs +489 -472
  8. package/dist/dictionaries/index.cjs.map +1 -1
  9. package/dist/dictionaries/index.d.cts +5 -3
  10. package/dist/dictionaries/index.d.ts +5 -3
  11. package/dist/dictionaries/index.js +489 -473
  12. package/dist/dictionaries/index.js.map +1 -1
  13. package/dist/index.cjs +617 -473
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +37 -37
  16. package/dist/index.d.ts +37 -37
  17. package/dist/index.js +617 -473
  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 +730 -478
  22. package/dist/lokascript-i18n.mjs.map +1 -1
  23. package/dist/plugins/vite.cjs +488 -472
  24. package/dist/plugins/vite.cjs.map +1 -1
  25. package/dist/plugins/vite.js +488 -472
  26. package/dist/plugins/vite.js.map +1 -1
  27. package/dist/plugins/webpack.cjs +488 -472
  28. package/dist/plugins/webpack.cjs.map +1 -1
  29. package/dist/plugins/webpack.js +488 -472
  30. package/dist/plugins/webpack.js.map +1 -1
  31. package/dist/{transformer-B0zy-kti.d.ts → transformer-BLv389qz.d.ts} +12 -47
  32. package/dist/{transformer-CzY_vHU0.d.cts → transformer-PFsYELrc.d.cts} +12 -47
  33. package/dist/{types-BYtpqGq3.d.cts → types-BcALAO6h.d.cts} +1 -1
  34. package/dist/{types-BYtpqGq3.d.ts → types-BcALAO6h.d.ts} +1 -1
  35. package/package.json +5 -5
  36. package/src/browser.ts +3 -0
  37. package/src/constants.ts +9 -0
  38. package/src/dictionaries/ar.ts +22 -43
  39. package/src/dictionaries/bn.ts +17 -0
  40. package/src/dictionaries/de.ts +19 -40
  41. package/src/dictionaries/en.ts +26 -0
  42. package/src/dictionaries/es.ts +19 -40
  43. package/src/dictionaries/fr.ts +19 -40
  44. package/src/dictionaries/he.ts +2 -0
  45. package/src/dictionaries/hi.ts +18 -48
  46. package/src/dictionaries/id.ts +22 -43
  47. package/src/dictionaries/index.ts +5 -1
  48. package/src/dictionaries/it.ts +18 -39
  49. package/src/dictionaries/ja.ts +18 -39
  50. package/src/dictionaries/ko.ts +22 -43
  51. package/src/dictionaries/ms.ts +16 -50
  52. package/src/dictionaries/pl.ts +18 -45
  53. package/src/dictionaries/pt.ts +23 -48
  54. package/src/dictionaries/qu.ts +22 -43
  55. package/src/dictionaries/ru.ts +18 -48
  56. package/src/dictionaries/sw.ts +22 -43
  57. package/src/dictionaries/th.ts +17 -0
  58. package/src/dictionaries/tl.ts +16 -50
  59. package/src/dictionaries/tr.ts +24 -45
  60. package/src/dictionaries/uk.ts +18 -48
  61. package/src/dictionaries/vi.ts +17 -0
  62. package/src/dictionaries/zh.ts +22 -43
  63. package/src/grammar/grammar.test.ts +143 -24
  64. package/src/grammar/index.ts +1 -0
  65. package/src/grammar/profiles/index.ts +31 -0
  66. package/src/grammar/transformer.ts +161 -0
  67. package/src/parser/he.ts +29 -0
  68. package/src/parser/index.ts +1 -0
  69. package/src/schema-alignment.test.ts +154 -0
  70. package/src/test-utils/complete-languages.ts +40 -0
  71. package/src/translation-validation.test.ts +1 -26
@@ -1,21 +1,14 @@
1
- /**
2
- * Ukrainian Dictionary
3
- *
4
- * Translations for Ukrainian language support.
5
- * Uses infinitive verbs (common in software UIs).
6
- */
1
+ // Generated/merged from semantic profiles — hand-written entries are preserved
2
+ // To add derived entries, update the semantic profile and run: npm run generate:language-assets
7
3
 
8
4
  import { Dictionary } from '../types';
9
5
 
10
6
  export const ukrainianDictionary: Dictionary = {
11
7
  commands: {
12
- // Event handling
13
8
  on: 'при',
14
9
  tell: 'сказати',
15
10
  trigger: 'викликати',
16
11
  send: 'надіслати',
17
-
18
- // DOM manipulation
19
12
  take: 'взяти',
20
13
  put: 'покласти',
21
14
  set: 'встановити',
@@ -25,8 +18,6 @@ export const ukrainianDictionary: Dictionary = {
25
18
  toggle: 'перемкнути',
26
19
  hide: 'сховати',
27
20
  show: 'показати',
28
-
29
- // Control flow
30
21
  if: 'якщо',
31
22
  unless: 'якщо_не',
32
23
  repeat: 'повторити',
@@ -36,65 +27,46 @@ export const ukrainianDictionary: Dictionary = {
36
27
  continue: 'продовжити',
37
28
  break: 'перервати',
38
29
  halt: 'зупинити',
39
-
40
- // Async
41
30
  wait: 'чекати',
42
31
  fetch: 'завантажити',
43
32
  call: 'викликати',
44
33
  return: 'повернути',
45
-
46
- // Other commands
47
34
  make: 'створити',
48
35
  log: 'записати',
49
36
  throw: 'кинути',
50
37
  catch: 'зловити',
51
38
  measure: 'виміряти',
52
39
  transition: 'анімувати',
53
-
54
- // Data Commands
55
40
  increment: 'збільшити',
56
41
  decrement: 'зменшити',
57
42
  default: 'за_замовчуванням',
58
-
59
- // Navigation Commands
60
43
  go: 'перейти',
61
44
  pushUrl: 'додати_url',
62
45
  replaceUrl: 'замінити_url',
63
-
64
- // Utility Commands
65
46
  copy: 'копіювати',
66
47
  pick: 'вибрати',
67
48
  beep: 'звук',
68
-
69
- // Advanced Commands
70
49
  js: 'js',
71
50
  async: 'асинхронно',
72
51
  render: 'відобразити',
73
-
74
- // Animation Commands
75
52
  swap: 'поміняти',
76
53
  morph: 'трансформувати',
77
54
  settle: 'стабілізувати',
78
-
79
- // Content Commands
80
55
  append: 'додати_в_кінець',
81
-
82
- // Control Flow
83
56
  exit: 'вийти',
84
57
  else: 'інакше',
85
-
86
- // Behaviors
87
58
  install: 'встановити',
88
59
  init: 'ініціалізувати',
89
60
  behavior: 'поведінка',
90
-
91
- // Focus
92
61
  focus: 'сфокусувати',
93
62
  blur: 'розфокусувати',
94
-
95
- // Clone
96
63
  clone: 'клонувати',
97
64
  prepend: 'додати_на_початок',
65
+ breakpoint: 'точка-зупинки',
66
+ clear: 'очистити',
67
+ close: 'закрити',
68
+ open: 'відкрити',
69
+ select: 'вибрати',
98
70
  },
99
71
 
100
72
  modifiers: {
@@ -127,23 +99,19 @@ export const ukrainianDictionary: Dictionary = {
127
99
  mouseover: 'наведення',
128
100
  mouseout: 'відведення',
129
101
  mousemove: 'рух_миші',
130
-
131
102
  keydown: 'клавіша_вниз',
132
103
  keyup: 'клавіша_вгору',
133
104
  keypress: 'натискання_клавіші',
134
-
135
105
  focus: 'фокус',
136
106
  blur: 'розфокус',
137
107
  change: 'зміна',
138
108
  input: 'введення',
139
109
  submit: 'надсилання',
140
110
  reset: 'скидання',
141
-
142
111
  load: 'завантаження',
143
112
  unload: 'вивантаження',
144
113
  resize: 'зміна_розміру',
145
114
  scroll: 'прокрутка',
146
-
147
115
  touchstart: 'дотик_початок',
148
116
  touchend: 'дотик_кінець',
149
117
  touchmove: 'дотик_рух',
@@ -166,8 +134,10 @@ export const ukrainianDictionary: Dictionary = {
166
134
  else: 'інакше',
167
135
  otherwise: 'в_іншому_випадку',
168
136
  end: 'кінець',
169
- has: 'має', // third-person: він/вона має
170
- have: 'маю', // first-person: я маю
137
+ live: 'живий',
138
+ changes: 'змінюється',
139
+ has: 'має',
140
+ have: 'маю',
171
141
  isNot: 'не_є',
172
142
  empty: 'порожній',
173
143
  },
@@ -241,7 +211,6 @@ export const ukrainianDictionary: Dictionary = {
241
211
  },
242
212
 
243
213
  expressions: {
244
- // Positional
245
214
  first: 'перший',
246
215
  last: 'останній',
247
216
  next: 'наступний',
@@ -249,20 +218,21 @@ export const ukrainianDictionary: Dictionary = {
249
218
  prev: 'попер',
250
219
  at: 'в',
251
220
  random: 'випадковий',
252
-
253
- // DOM Traversal
254
221
  closest: 'найближчий',
255
222
  parent: 'батьківський',
256
223
  children: 'діти',
257
224
  within: 'всередині',
258
-
259
- // Emptiness/Existence
260
225
  no: 'ні',
261
226
  empty: 'порожній',
262
227
  some: 'деякі',
263
-
264
- // String operations
265
228
  'starts with': 'починається_з',
266
229
  'ends with': 'закінчується_на',
230
+ 'ignoring case': 'без_урахування_регістру',
231
+ 'sorted by': 'відсортовано_за',
232
+ 'mapped to': 'перетворено_на',
233
+ 'split by': 'розділено_за',
234
+ 'joined by': "об'єднано_через",
267
235
  },
268
236
  };
237
+
238
+ export const uk = ukrainianDictionary;
@@ -62,6 +62,11 @@ export const vi: Dictionary = {
62
62
  clone: 'sao chép',
63
63
  prepend: 'thêm đầu',
64
64
  else: 'không thì',
65
+ breakpoint: 'điểm-dừng',
66
+ clear: 'xóa',
67
+ close: 'đóng',
68
+ open: 'mở',
69
+ select: 'chọn',
65
70
  },
66
71
 
67
72
  modifiers: {
@@ -79,6 +84,7 @@ export const vi: Dictionary = {
79
84
  in: 'trong',
80
85
  at: 'tại',
81
86
  of: 'của',
87
+ between: 'giữa',
82
88
  the: '',
83
89
  times: 'lần',
84
90
  },
@@ -98,6 +104,7 @@ export const vi: Dictionary = {
98
104
  mouseleave: 'chuột rời',
99
105
  focus: 'tập trung',
100
106
  blur: 'mất tập trung',
107
+ reset: 'đặt-lại',
101
108
  load: 'tải',
102
109
  scroll: 'cuộn',
103
110
  resize: 'đổi kích thước',
@@ -123,6 +130,8 @@ export const vi: Dictionary = {
123
130
  else: 'không thì',
124
131
  end: 'kết thúc',
125
132
  then: 'rồi',
133
+ live: 'trực tiếp',
134
+ changes: 'thay đổi',
126
135
  },
127
136
 
128
137
  temporal: {
@@ -191,5 +200,13 @@ export const vi: Dictionary = {
191
200
  parent: 'cha',
192
201
  children: 'con',
193
202
  closest: 'gần nhất',
203
+ empty: 'làm-rỗng',
204
+ 'starts with': 'bắt đầu bằng',
205
+ 'ends with': 'kết thúc bằng',
206
+ 'ignoring case': 'không phân biệt hoa thường',
207
+ 'sorted by': 'sắp xếp theo',
208
+ 'mapped to': 'ánh xạ thành',
209
+ 'split by': 'tách bởi',
210
+ 'joined by': 'nối bởi',
194
211
  },
195
212
  };
@@ -1,16 +1,14 @@
1
- // packages/i18n/src/dictionaries/zh.ts
1
+ // Generated/merged from semantic profiles — hand-written entries are preserved
2
+ // To add derived entries, update the semantic profile and run: npm run generate:language-assets
2
3
 
3
4
  import { Dictionary } from '../types';
4
5
 
5
6
  export const zh: Dictionary = {
6
7
  commands: {
7
- // Event handling
8
8
  on: '当',
9
9
  tell: '告诉',
10
10
  trigger: '触发',
11
11
  send: '发送',
12
-
13
- // DOM manipulation
14
12
  take: '获取',
15
13
  put: '放置',
16
14
  set: '设置',
@@ -20,8 +18,6 @@ export const zh: Dictionary = {
20
18
  toggle: '切换',
21
19
  hide: '隐藏',
22
20
  show: '显示',
23
-
24
- // Control flow
25
21
  if: '如果',
26
22
  unless: '除非',
27
23
  repeat: '重复',
@@ -31,54 +27,41 @@ export const zh: Dictionary = {
31
27
  continue: '继续',
32
28
  break: '中断',
33
29
  halt: '停止',
34
-
35
- // Async
36
30
  wait: '等待',
37
31
  fetch: '获取',
38
32
  call: '调用',
39
33
  return: '返回',
40
-
41
- // Other commands
42
34
  make: '制作',
43
35
  log: '日志',
44
36
  throw: '抛出',
45
37
  catch: '捕获',
46
38
  measure: '测量',
47
39
  transition: '过渡',
48
-
49
- // Data Commands
50
40
  increment: '增加',
51
41
  decrement: '减少',
52
42
  default: '默认',
53
-
54
- // Navigation Commands
55
43
  go: '前往',
56
44
  pushUrl: '推送网址',
57
45
  replaceUrl: '替换网址',
58
-
59
- // Utility Commands
60
46
  copy: '复制',
61
47
  pick: '选取',
62
48
  beep: '蜂鸣',
63
-
64
- // Advanced Commands
65
49
  js: 'JS执行',
66
50
  async: '异步',
67
51
  render: '渲染',
68
-
69
- // Animation Commands
70
52
  swap: '交换',
71
53
  morph: '变形',
72
54
  settle: '稳定',
73
-
74
- // Content Commands
75
55
  append: '追加',
76
-
77
- // Control Flow
78
56
  exit: '退出',
79
-
80
- // Behaviors
81
57
  install: '安装',
58
+ breakpoint: '断点',
59
+ clear: '清除',
60
+ close: '关闭',
61
+ open: '打开',
62
+ select: '选择',
63
+ clone: '克隆',
64
+ prepend: '前置',
82
65
  },
83
66
 
84
67
  modifiers: {
@@ -110,23 +93,19 @@ export const zh: Dictionary = {
110
93
  mouseover: '鼠标悬停',
111
94
  mouseout: '鼠标移出',
112
95
  mousemove: '鼠标移动',
113
-
114
96
  keydown: '按键按下',
115
97
  keyup: '按键抬起',
116
98
  keypress: '按键',
117
-
118
99
  focus: '聚焦',
119
100
  blur: '失焦',
120
101
  change: '改变',
121
102
  input: '输入',
122
103
  submit: '提交',
123
104
  reset: '重置',
124
-
125
105
  load: '加载',
126
106
  unload: '卸载',
127
107
  resize: '调整大小',
128
108
  scroll: '滚动',
129
-
130
109
  touchstart: '触摸开始',
131
110
  touchend: '触摸结束',
132
111
  touchmove: '触摸移动',
@@ -145,12 +124,14 @@ export const zh: Dictionary = {
145
124
  contains: '包含',
146
125
  includes: '包括',
147
126
  equals: '等于',
148
- has: '有', // universal "have"
149
- have: '有', // same character for first/third person
127
+ has: '有',
128
+ have: '有',
150
129
  then: '那么',
151
130
  else: '否则',
152
131
  otherwise: '要不然',
153
132
  end: '结束',
133
+ live: '实时',
134
+ changes: '改变时',
154
135
  },
155
136
 
156
137
  temporal: {
@@ -174,13 +155,13 @@ export const zh: Dictionary = {
174
155
  null: '空',
175
156
  undefined: '未定义',
176
157
  it: '它',
177
- its: '它的', // REVIEW: native speaker
158
+ its: '它的',
178
159
  me: '我',
179
160
  my: '我的',
180
161
  myself: '我自己',
181
- you: '你', // REVIEW: native speaker
182
- your: '你的', // REVIEW: native speaker
183
- yourself: '你自己', // REVIEW: native speaker
162
+ you: '你',
163
+ your: '你的',
164
+ yourself: '你自己',
184
165
  element: '元素',
185
166
  target: '目标',
186
167
  detail: '详情',
@@ -204,7 +185,6 @@ export const zh: Dictionary = {
204
185
  },
205
186
 
206
187
  expressions: {
207
- // Positional
208
188
  first: '第一个',
209
189
  last: '最后一个',
210
190
  next: '下一个',
@@ -212,20 +192,19 @@ export const zh: Dictionary = {
212
192
  prev: '上个',
213
193
  at: '在',
214
194
  random: '随机',
215
-
216
- // DOM Traversal
217
195
  closest: '最近的',
218
196
  parent: '父级',
219
197
  children: '子级',
220
198
  within: '之内',
221
-
222
- // Emptiness/Existence
223
199
  no: '没有',
224
200
  empty: '空的',
225
201
  some: '一些',
226
-
227
- // String operations
228
202
  'starts with': '以开头',
229
203
  'ends with': '以结尾',
204
+ 'ignoring case': '忽略大小写',
205
+ 'sorted by': '按_排序',
206
+ 'mapped to': '映射为',
207
+ 'split by': '按_分割',
208
+ 'joined by': '用_连接',
230
209
  },
231
210
  };
@@ -40,30 +40,37 @@ import type { ParsedElement, SemanticRole } from './types';
40
40
  describe('Language Profiles', () => {
41
41
  it('should have profiles for all supported locales', () => {
42
42
  const locales = getSupportedLocales();
43
- expect(locales).toContain('en');
44
- expect(locales).toContain('ja');
45
- expect(locales).toContain('ko');
46
- expect(locales).toContain('zh');
47
- expect(locales).toContain('ar');
48
- expect(locales).toContain('tr');
49
- expect(locales).toContain('es');
50
- expect(locales).toContain('de');
51
- expect(locales).toContain('fr');
52
- expect(locales).toContain('pt');
53
- expect(locales).toContain('id');
54
- expect(locales).toContain('qu');
55
- expect(locales).toContain('sw');
56
- expect(locales).toContain('bn');
57
- // New languages added
58
- expect(locales).toContain('it');
59
- expect(locales).toContain('ru');
60
- expect(locales).toContain('uk');
61
- expect(locales).toContain('vi');
62
- expect(locales).toContain('hi');
63
- expect(locales).toContain('tl');
64
- expect(locales).toContain('th');
65
- expect(locales).toContain('pl');
66
- expect(locales.length).toBe(22);
43
+ // Explicit expected list — when adding a profile, append it here so the
44
+ // count assertion stays meaningful without becoming a magic number.
45
+ const expectedLocales = [
46
+ 'en',
47
+ 'ja',
48
+ 'ko',
49
+ 'zh',
50
+ 'ar',
51
+ 'tr',
52
+ 'es',
53
+ 'de',
54
+ 'fr',
55
+ 'pt',
56
+ 'id',
57
+ 'qu',
58
+ 'sw',
59
+ 'bn',
60
+ 'it',
61
+ 'ru',
62
+ 'uk',
63
+ 'vi',
64
+ 'hi',
65
+ 'tl',
66
+ 'th',
67
+ 'pl',
68
+ 'he',
69
+ ];
70
+ for (const code of expectedLocales) {
71
+ expect(locales, `missing profile: ${code}`).toContain(code);
72
+ }
73
+ expect(locales.length).toBe(expectedLocales.length);
67
74
  });
68
75
 
69
76
  it('should return undefined for unknown locales', () => {
@@ -1149,4 +1156,116 @@ describe('Possessive Dot Notation Translation', () => {
1149
1156
  expect(result).not.toMatch(/\bmy\b/);
1150
1157
  });
1151
1158
  });
1159
+
1160
+ // ──── Reactive `live` block scope ────
1161
+ // Before this fix, the splitter cut between `live` and the body's
1162
+ // first command keyword, then the join logic re-inserted the target
1163
+ // language's `then` keyword between them. E.g.
1164
+ // `live put X into me end` (en→de) became
1165
+ // `live dann setzen X zu ich ende` (spurious `dann`).
1166
+ // The fix tracks block depth from `live` to its matching `end` and
1167
+ // suppresses splits inside that scope.
1168
+ describe('live block does not spuriously inject "then"', () => {
1169
+ const cases: Array<[string, string, RegExp]> = [
1170
+ // [target lang, input, banned pattern]
1171
+ ['de', 'live put $count into me end', /\bdann\b/i],
1172
+ ['es', 'live put $count into me end', /\bentonces\b/i],
1173
+ ['ja', 'live put $count into me end', /それから/],
1174
+ ['tr', 'live put $count into me end', /\bsonra\b/i],
1175
+ ['it', 'live put $count into me end', /\ballora\b/i],
1176
+ ['ru', 'live put $count into me end', /\bзатем\b/i],
1177
+ ['vi', 'live put $count into me end', /\brồi\b/i],
1178
+ ];
1179
+
1180
+ for (const [target, input, banned] of cases) {
1181
+ it(`(${target}) does not inject "then" inside the body`, () => {
1182
+ const transformer = new GrammarTransformer('en', target);
1183
+ const result = transformer.transform(input);
1184
+ expect(result, `unexpected ${banned} in: ${result}`).not.toMatch(banned);
1185
+ });
1186
+ }
1187
+
1188
+ it('keeps live body as one unit — no `live then` artifact', () => {
1189
+ const transformer = new GrammarTransformer('en', 'en');
1190
+ const result = transformer.transform('live put $count into me end');
1191
+ expect(result).not.toMatch(/live\s+then\b/i);
1192
+ expect(result).toMatch(/^live\b/);
1193
+ expect(result).toMatch(/\bend$/);
1194
+ });
1195
+
1196
+ it('still splits on explicit "then" OUTSIDE the live block', () => {
1197
+ // Legitimate `then`-based chains MUST still split.
1198
+ const transformer = new GrammarTransformer('en', 'de');
1199
+ const result = transformer.transform('live put $x into me end then toggle .active');
1200
+ // German "then" = "dann" — appears exactly once (between block + toggle).
1201
+ const occurrences = (result.match(/\bdann\b/gi) || []).length;
1202
+ expect(occurrences).toBe(1);
1203
+ });
1204
+ });
1205
+
1206
+ // ──── Block extraction for `when`, `unless`, and SOV `live` ────
1207
+ // Block-syntactic tokens are pulled out before parseStatement so
1208
+ // they don't end up as command verbs (`live` → action role) or get
1209
+ // swept into role values (`end` → destination tail). The body
1210
+ // recurses through the regular pipeline, which keeps SOV reorder
1211
+ // working *inside* the body without disturbing the block frame.
1212
+ describe('reactive blocks — when/unless/live block extraction', () => {
1213
+ it('`when X changes Y` does not truncate — body is translated (en→de)', () => {
1214
+ const t = new GrammarTransformer('en', 'de');
1215
+ const result = t.transform('when $count changes log $count');
1216
+ // Pre-fix output was just `wenn` (parseConditional returned only
1217
+ // the action role). Now we should see head + body translated.
1218
+ expect(result.length).toBeGreaterThan('wenn'.length + 5);
1219
+ expect(result).toMatch(/wenn/i);
1220
+ expect(result).toMatch(/protokolliere|log/i);
1221
+ expect(result).toMatch(/ändert|changes/i);
1222
+ });
1223
+
1224
+ it('`unless X Y` does not truncate — body is translated (en→de)', () => {
1225
+ const t = new GrammarTransformer('en', 'de');
1226
+ const result = t.transform('unless $disabled add .ready to me');
1227
+ expect(result.length).toBeGreaterThan('wennnicht'.length + 5);
1228
+ expect(result).toMatch(/wennnicht|unless/i);
1229
+ expect(result).toMatch(/hinzufüg|add/i);
1230
+ });
1231
+
1232
+ it('`live X end` keeps live at start and end at end (SOV: ja)', () => {
1233
+ const t = new GrammarTransformer('en', 'ja');
1234
+ const result = t.transform('live put $count into me end');
1235
+ // Block frame: head first, tail last. Pre-fix the SOV reorder
1236
+ // dragged `live` to the end as if it were the action verb.
1237
+ expect(result).toMatch(/^(live|ライブ)/);
1238
+ expect(result).toMatch(/(end|終わり)$/);
1239
+ // SOV reorder applies inside the body — destination marker に
1240
+ // should appear, indicating `me` was reordered with its postposition.
1241
+ expect(result).toMatch(/に/);
1242
+ });
1243
+
1244
+ it('`live X end` keeps live at start and end at end (SOV: tr)', () => {
1245
+ const t = new GrammarTransformer('en', 'tr');
1246
+ const result = t.transform('live put $count into me end');
1247
+ expect(result).toMatch(/^(live|canlı)/i);
1248
+ expect(result).toMatch(/(end|son)$/i);
1249
+ });
1250
+
1251
+ it('regression: `if X then Y end` still works (no block extraction)', () => {
1252
+ // `if` is intentionally not a BLOCK_HEAD_KEYWORD — splitOnThen +
1253
+ // parseConditional already handle it. Verify we didn't regress.
1254
+ const t = new GrammarTransformer('en', 'de');
1255
+ const result = t.transform('if $x then increment $count end');
1256
+ expect(result).toMatch(/wenn|if/i);
1257
+ expect(result).toMatch(/dann|then/i);
1258
+ expect(result).toMatch(/erhöh|increment/i);
1259
+ });
1260
+
1261
+ it('regression: `live X end then toggle .y` — exactly one "then" connector', () => {
1262
+ // Splitter must still recognize the `then` *outside* the live
1263
+ // block as a statement boundary, even now that we route blocks
1264
+ // around parseStatement.
1265
+ const t = new GrammarTransformer('en', 'de');
1266
+ const result = t.transform('live put $x into me end then toggle .active');
1267
+ const danns = (result.match(/\bdann\b/gi) || []).length;
1268
+ expect(danns).toBe(1);
1269
+ });
1270
+ });
1152
1271
  });
@@ -46,6 +46,7 @@ export {
46
46
  tagalogProfile,
47
47
  thaiProfile,
48
48
  polishProfile,
49
+ hebrewProfile,
49
50
  } from './profiles';
50
51
 
51
52
  // Direct language-pair translation
@@ -826,6 +826,36 @@ export const polishProfile: LanguageProfile = {
826
826
  ],
827
827
  };
828
828
 
829
+ // =============================================================================
830
+ // Hebrew (SVO, RTL, Prepositions)
831
+ // =============================================================================
832
+
833
+ export const hebrewProfile: LanguageProfile = {
834
+ code: 'he',
835
+ name: 'עברית',
836
+
837
+ wordOrder: 'SVO', // Modern Hebrew is predominantly SVO (Biblical Hebrew was VSO)
838
+ adpositionType: 'preposition',
839
+ morphology: 'fusional', // Root-and-pattern (Semitic), like Arabic
840
+ direction: 'rtl',
841
+
842
+ canonicalOrder: ['event', 'action', 'patient', 'destination'],
843
+
844
+ // Hebrew uses single-letter prefix prepositions (ב, ל, מ, כ) that attach to
845
+ // the following word. For tokenization we treat them as spaced forms.
846
+ markers: [
847
+ { form: 'ב', role: 'event', position: 'preposition', required: true },
848
+ { form: 'על', role: 'destination', position: 'preposition', required: false },
849
+ { form: 'אל', role: 'destination', position: 'preposition', required: false },
850
+ { form: 'ל', role: 'destination', position: 'preposition', required: false },
851
+ { form: 'את', role: 'patient', position: 'preposition', required: false }, // Direct object marker
852
+ { form: 'מ', role: 'source', position: 'preposition', required: false },
853
+ { form: 'מן', role: 'source', position: 'preposition', required: false },
854
+ { form: 'עם', role: 'style', position: 'preposition', required: false },
855
+ { form: 'כ', role: 'method', position: 'preposition', required: false },
856
+ ],
857
+ };
858
+
829
859
  // =============================================================================
830
860
  // Profile Registry
831
861
  // =============================================================================
@@ -854,6 +884,7 @@ export const profiles: Record<string, LanguageProfile> = {
854
884
  tl: tagalogProfile,
855
885
  th: thaiProfile,
856
886
  pl: polishProfile,
887
+ he: hebrewProfile,
857
888
  };
858
889
 
859
890
  export function getProfile(locale: string): LanguageProfile | undefined {