@lokascript/hyperscript-adapter 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/dist/hyperscript-i18n-ar.global.js +4 -0
  4. package/dist/hyperscript-i18n-bn.global.js +4 -0
  5. package/dist/hyperscript-i18n-de.global.js +4 -0
  6. package/dist/hyperscript-i18n-east-asian.global.js +4 -0
  7. package/dist/hyperscript-i18n-en.global.js +6 -0
  8. package/dist/hyperscript-i18n-es.global.js +4 -0
  9. package/dist/hyperscript-i18n-fr.global.js +4 -0
  10. package/dist/hyperscript-i18n-he.global.js +4 -0
  11. package/dist/hyperscript-i18n-hi.global.js +4 -0
  12. package/dist/hyperscript-i18n-id.global.js +4 -0
  13. package/dist/hyperscript-i18n-it.global.js +4 -0
  14. package/dist/hyperscript-i18n-ja.global.js +4 -0
  15. package/dist/hyperscript-i18n-ko.global.js +4 -0
  16. package/dist/hyperscript-i18n-lite.global.js +1 -0
  17. package/dist/hyperscript-i18n-ms.global.js +4 -0
  18. package/dist/hyperscript-i18n-pl.global.js +4 -0
  19. package/dist/hyperscript-i18n-pt.global.js +4 -0
  20. package/dist/hyperscript-i18n-qu.global.js +4 -0
  21. package/dist/hyperscript-i18n-ru.global.js +4 -0
  22. package/dist/hyperscript-i18n-slavic.global.js +4 -0
  23. package/dist/hyperscript-i18n-south-asian.global.js +4 -0
  24. package/dist/hyperscript-i18n-southeast-asian.global.js +4 -0
  25. package/dist/hyperscript-i18n-sw.global.js +4 -0
  26. package/dist/hyperscript-i18n-th.global.js +4 -0
  27. package/dist/hyperscript-i18n-tl.global.js +4 -0
  28. package/dist/hyperscript-i18n-tr.global.js +4 -0
  29. package/dist/hyperscript-i18n-uk.global.js +4 -0
  30. package/dist/hyperscript-i18n-vi.global.js +4 -0
  31. package/dist/hyperscript-i18n-western.global.js +4 -0
  32. package/dist/hyperscript-i18n-zh.global.js +4 -0
  33. package/dist/hyperscript-i18n.global.js +8 -0
  34. package/dist/hyperscript-i18n.global.js.map +1 -0
  35. package/dist/index.cjs +203 -0
  36. package/dist/index.cjs.map +1 -0
  37. package/dist/index.d.cts +99 -0
  38. package/dist/index.d.ts +99 -0
  39. package/dist/index.js +179 -0
  40. package/dist/index.js.map +1 -0
  41. package/package.json +169 -0
  42. package/src/browser-lite.ts +159 -0
  43. package/src/browser.ts +74 -0
  44. package/src/bundles/ar.ts +6 -0
  45. package/src/bundles/bn.ts +6 -0
  46. package/src/bundles/de.ts +6 -0
  47. package/src/bundles/east-asian.ts +8 -0
  48. package/src/bundles/en.ts +6 -0
  49. package/src/bundles/es.ts +6 -0
  50. package/src/bundles/fr.ts +6 -0
  51. package/src/bundles/he.ts +6 -0
  52. package/src/bundles/hi.ts +6 -0
  53. package/src/bundles/id.ts +6 -0
  54. package/src/bundles/it.ts +6 -0
  55. package/src/bundles/ja.ts +6 -0
  56. package/src/bundles/ko.ts +6 -0
  57. package/src/bundles/ms.ts +6 -0
  58. package/src/bundles/pl.ts +6 -0
  59. package/src/bundles/pt.ts +6 -0
  60. package/src/bundles/qu.ts +6 -0
  61. package/src/bundles/ru.ts +6 -0
  62. package/src/bundles/shared.ts +65 -0
  63. package/src/bundles/slavic.ts +8 -0
  64. package/src/bundles/south-asian.ts +7 -0
  65. package/src/bundles/southeast-asian.ts +10 -0
  66. package/src/bundles/sw.ts +6 -0
  67. package/src/bundles/th.ts +6 -0
  68. package/src/bundles/tl.ts +6 -0
  69. package/src/bundles/tr.ts +6 -0
  70. package/src/bundles/uk.ts +6 -0
  71. package/src/bundles/vi.ts +6 -0
  72. package/src/bundles/western.ts +9 -0
  73. package/src/bundles/zh.ts +6 -0
  74. package/src/derive-syntax.ts +126 -0
  75. package/src/generated/syntax-table.ts +61 -0
  76. package/src/hyperscript-renderer.ts +150 -0
  77. package/src/index.ts +27 -0
  78. package/src/language-resolver.ts +41 -0
  79. package/src/plugin.ts +119 -0
  80. package/src/preprocessor.ts +251 -0
  81. package/src/slim-plugin.ts +60 -0
  82. package/src/slim-preprocessor.ts +190 -0
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Preprocessor
3
+ *
4
+ * Translates non-English hyperscript to English using the semantic parser
5
+ * (with optional i18n fallback), so the original _hyperscript can parse it.
6
+ */
7
+
8
+ import {
9
+ translate,
10
+ render,
11
+ createSemanticAnalyzer,
12
+ shouldUseSemanticResult,
13
+ tryGetProfile,
14
+ type SemanticAnalyzer,
15
+ } from '@lokascript/semantic';
16
+
17
+ export interface PreprocessorConfig {
18
+ /**
19
+ * Minimum confidence threshold for semantic parsing (0-1). Default: 0.5.
20
+ * Can be a single number (applies to all languages) or a per-language map.
21
+ * Per-language thresholds are useful because SOV languages (ja, ko, tr) produce
22
+ * inherently lower confidence scores than SVO languages (es, fr, de).
23
+ *
24
+ * @example
25
+ * // Single threshold
26
+ * { confidenceThreshold: 0.5 }
27
+ *
28
+ * @example
29
+ * // Per-language thresholds
30
+ * { confidenceThreshold: { es: 0.7, ja: 0.1, ko: 0.05, '*': 0.5 } }
31
+ */
32
+ confidenceThreshold: number | Record<string, number>;
33
+ /** Strategy: 'semantic' (default), 'i18n', or 'auto' (semantic then i18n) */
34
+ strategy: 'semantic' | 'i18n' | 'auto';
35
+ /** Whether to return original text on failure. Default: true */
36
+ fallbackToOriginal: boolean;
37
+ /** Optional i18n toEnglish function (loaded dynamically if available) */
38
+ i18nToEnglish?: (input: string, locale: string) => string;
39
+ }
40
+
41
+ const DEFAULT_THRESHOLD = 0.5;
42
+
43
+ const DEFAULT_CONFIG: PreprocessorConfig = {
44
+ confidenceThreshold: DEFAULT_THRESHOLD,
45
+ strategy: 'semantic',
46
+ fallbackToOriginal: true,
47
+ };
48
+
49
+ /**
50
+ * Resolve the confidence threshold for a specific language.
51
+ * Supports both a single number and a per-language map with '*' as default.
52
+ */
53
+ function resolveThreshold(threshold: number | Record<string, number>, lang: string): number {
54
+ if (typeof threshold === 'number') return threshold;
55
+ return threshold[lang] ?? threshold['*'] ?? DEFAULT_THRESHOLD;
56
+ }
57
+
58
+ let analyzer: SemanticAnalyzer | null = null;
59
+
60
+ function getAnalyzer(): SemanticAnalyzer {
61
+ if (!analyzer) {
62
+ analyzer = createSemanticAnalyzer();
63
+ }
64
+ return analyzer;
65
+ }
66
+
67
+ /**
68
+ * Preprocess non-English hyperscript into English.
69
+ *
70
+ * Uses the semantic parser to parse the input in the source language,
71
+ * then renders back to English. Falls through to i18n if configured.
72
+ *
73
+ * Handles _hyperscript feature prefixes (e.g. "on click", "on every keyup")
74
+ * by stripping them, translating only the command portion, then reassembling.
75
+ */
76
+ export function preprocessToEnglish(
77
+ src: string,
78
+ lang: string,
79
+ config: Partial<PreprocessorConfig> = {}
80
+ ): string {
81
+ // English→English is identity; skip semantic parsing which may mangle the input
82
+ if (lang === 'en') return src;
83
+
84
+ const cfg = { ...DEFAULT_CONFIG, ...config };
85
+
86
+ // Try translating the full string first
87
+ const fullResult = tryTranslateWithStrategies(src, lang, cfg);
88
+ if (fullResult !== null) return fullResult;
89
+
90
+ // If full translation failed, try stripping event/feature prefix.
91
+ // _hyperscript attributes often contain "on <event> <commands>" — the semantic
92
+ // parser only understands command syntax, not event declarations.
93
+ const stripped = stripEventPrefix(src);
94
+ if (stripped) {
95
+ const translated = tryTranslateWithStrategies(stripped.commands, lang, cfg);
96
+ if (translated !== null) return stripped.prefix + translated;
97
+ }
98
+
99
+ // Fallback: return original
100
+ return cfg.fallbackToOriginal ? src : src;
101
+ }
102
+
103
+ /**
104
+ * Try all configured translation strategies on the input.
105
+ * Returns null if none succeed.
106
+ */
107
+ function tryTranslateWithStrategies(
108
+ src: string,
109
+ lang: string,
110
+ cfg: PreprocessorConfig
111
+ ): string | null {
112
+ // Strategy: semantic-only
113
+ if (cfg.strategy === 'semantic' || cfg.strategy === 'auto') {
114
+ const threshold = resolveThreshold(cfg.confidenceThreshold, lang);
115
+ const result = trySemanticTranslation(src, lang, threshold);
116
+ if (result !== null) return result;
117
+ }
118
+
119
+ // Strategy: i18n fallback (auto mode or i18n-only)
120
+ if ((cfg.strategy === 'auto' || cfg.strategy === 'i18n') && cfg.i18nToEnglish) {
121
+ const result = tryI18nTranslation(src, lang, cfg.i18nToEnglish);
122
+ if (result !== null) return result;
123
+ }
124
+
125
+ return null;
126
+ }
127
+
128
+ /**
129
+ * Match _hyperscript event handler prefix: "on [every] <event>[filter][.modifiers] "
130
+ *
131
+ * Examples:
132
+ * "on click toggle .active" → prefix: "on click ", commands: "toggle .active"
133
+ * "on every click toggle .active" → prefix: "on every click ", commands: "toggle .active"
134
+ * "on click.debounce(300) toggle .x" → prefix: "on click.debounce(300) ", commands: "toggle .x"
135
+ * "on keyup[key=='Enter'] set x to 1" → prefix: "on keyup[key=='Enter'] ", commands: "set x to 1"
136
+ * "on click from body toggle .active" → prefix: "on click from body ", commands: "toggle .active"
137
+ */
138
+ const EVENT_PREFIX_RE =
139
+ /^(on\s+(?:every\s+)?[\w-]+(?:\[.*?\])?(?:\.[\w-]+(?:\([^)]*\))?)*(?:\s+from\s+\S+)?(?:\s+queue\s+\w+)?\s+)/;
140
+
141
+ function stripEventPrefix(src: string): { prefix: string; commands: string } | null {
142
+ const match = src.match(EVENT_PREFIX_RE);
143
+ if (!match) return null;
144
+ const prefix = match[1];
145
+ const commands = src.slice(prefix.length);
146
+ if (!commands) return null;
147
+ return { prefix, commands };
148
+ }
149
+
150
+ /**
151
+ * Try semantic translation: parse in source language, render to English.
152
+ * Returns null if confidence is below threshold.
153
+ */
154
+ function trySemanticTranslation(src: string, lang: string, threshold: number): string | null {
155
+ try {
156
+ // Handle compound statements (split on "then" boundaries and newlines)
157
+ const statements = splitStatements(src, lang);
158
+ if (statements.length > 1) {
159
+ return translateCompound(statements, lang, threshold);
160
+ }
161
+
162
+ // Check if the semantic parser can handle this
163
+ const sem = getAnalyzer();
164
+ if (!sem.supportsLanguage(lang)) return null;
165
+
166
+ const result = sem.analyze(src, lang);
167
+ if (!shouldUseSemanticResult(result, threshold)) return null;
168
+
169
+ // Render the semantic node to English
170
+ if (result.node) {
171
+ return render(result.node, 'en');
172
+ }
173
+
174
+ // Fallback: use translate() which combines parse+render
175
+ return translate(src, lang, 'en');
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Try i18n grammar transformation to English.
183
+ * Returns null if the result is identical to input (no translation happened).
184
+ */
185
+ function tryI18nTranslation(
186
+ src: string,
187
+ lang: string,
188
+ toEnglish: (input: string, locale: string) => string
189
+ ): string | null {
190
+ try {
191
+ const result = toEnglish(src, lang);
192
+ return result !== src ? result : null;
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Get all "then" keyword forms for a language (primary + alternatives).
200
+ * Falls back to English "then" if profile is unavailable.
201
+ */
202
+ function getThenKeywords(lang: string): string[] {
203
+ const keywords = ['then']; // Always include English
204
+ const profile = tryGetProfile(lang);
205
+ if (profile?.keywords?.then) {
206
+ const kw = profile.keywords.then;
207
+ if (kw.primary && kw.primary !== 'then') keywords.push(kw.primary);
208
+ if (kw.alternatives) {
209
+ for (const alt of kw.alternatives) {
210
+ if (!keywords.includes(alt)) keywords.push(alt);
211
+ }
212
+ }
213
+ }
214
+ return keywords;
215
+ }
216
+
217
+ /**
218
+ * Split a hyperscript source string into individual statements.
219
+ * Splits on language-specific "then" keywords and newlines.
220
+ */
221
+ function splitStatements(src: string, lang: string): string[] {
222
+ const thenWords = getThenKeywords(lang);
223
+ // Escape regex special chars and build pattern
224
+ const escaped = thenWords.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
225
+ const pattern = new RegExp(`\\s+(?:${escaped.join('|')})\\s+`, 'i');
226
+
227
+ // Split on newlines first
228
+ const lines = src
229
+ .split('\n')
230
+ .map(l => l.trim())
231
+ .filter(l => l.length > 0);
232
+ const result: string[] = [];
233
+ for (const line of lines) {
234
+ const parts = line.split(pattern);
235
+ result.push(...parts);
236
+ }
237
+ return result;
238
+ }
239
+
240
+ /**
241
+ * Translate compound statements individually and rejoin with " then ".
242
+ */
243
+ function translateCompound(statements: string[], lang: string, threshold: number): string | null {
244
+ const translated: string[] = [];
245
+ for (const stmt of statements) {
246
+ const result = trySemanticTranslation(stmt.trim(), lang, threshold);
247
+ if (result === null) return null; // If any part fails, fail the whole compound
248
+ translated.push(result);
249
+ }
250
+ return translated.join(' then ');
251
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Slim Plugin
3
+ *
4
+ * Same plugin logic as plugin.ts but uses slim-preprocessor
5
+ * (does not trigger all-language registration).
6
+ */
7
+
8
+ import { resolveLanguage } from './language-resolver';
9
+ import { preprocessToEnglish } from './slim-preprocessor';
10
+ import type { PreprocessorConfig } from './preprocessor';
11
+ import type { PluginOptions } from './plugin';
12
+
13
+ export type { PluginOptions };
14
+
15
+ export function hyperscriptI18n(options: PluginOptions = {}) {
16
+ return function plugin(hs: unknown): void {
17
+ const { internals } = hs as {
18
+ internals: { runtime: { getScript: (elt: Element) => string | null } };
19
+ };
20
+ const runtime = internals.runtime;
21
+ const originalGetScript = runtime.getScript.bind(runtime);
22
+
23
+ runtime.getScript = function (elt: Element): string | null {
24
+ const src = originalGetScript(elt);
25
+ if (!src) return null;
26
+
27
+ const lang = resolveLanguageWithOptions(elt, options);
28
+ if (!lang || lang === 'en') return src;
29
+
30
+ const english = preprocessToEnglish(src, lang, options);
31
+
32
+ if (options.debug && english !== src) {
33
+ console.log(`[hyperscript-i18n] ${lang}: "${src}" → "${english}"`);
34
+ }
35
+
36
+ return english;
37
+ };
38
+ };
39
+ }
40
+
41
+ function resolveLanguageWithOptions(elt: Element, options: PluginOptions): string | null {
42
+ if (options.languageAttribute) {
43
+ const custom = elt.getAttribute(options.languageAttribute);
44
+ if (custom) return custom.split('-')[0].toLowerCase();
45
+ }
46
+
47
+ const resolved = resolveLanguage(elt);
48
+ if (resolved) return resolved;
49
+
50
+ return options.defaultLanguage ?? null;
51
+ }
52
+
53
+ export function preprocess(
54
+ src: string,
55
+ lang: string,
56
+ config: Partial<PreprocessorConfig> = {}
57
+ ): string {
58
+ if (lang === 'en') return src;
59
+ return preprocessToEnglish(src, lang, config);
60
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Slim Preprocessor
3
+ *
4
+ * Same logic as preprocessor.ts but imports from @lokascript/semantic/core
5
+ * and uses a custom hyperscript renderer instead of the semantic package's
6
+ * render(). This avoids importing English language data (tokenizer, patterns,
7
+ * profile), saving ~35 KB per per-language bundle.
8
+ *
9
+ * Used by per-language browser bundles for tree-shaking.
10
+ */
11
+
12
+ // Import from /core — does NOT trigger all-language registration.
13
+ // Languages are registered separately via side-effect imports in bundle entries.
14
+ import {
15
+ createSemanticAnalyzer,
16
+ shouldUseSemanticResult,
17
+ parse,
18
+ tryGetProfile,
19
+ type SemanticAnalyzer,
20
+ } from '@lokascript/semantic/core';
21
+
22
+ import { renderToHyperscript } from './hyperscript-renderer';
23
+ import type { PreprocessorConfig } from './preprocessor';
24
+
25
+ const DEFAULT_THRESHOLD = 0.5;
26
+
27
+ const DEFAULT_CONFIG: PreprocessorConfig = {
28
+ confidenceThreshold: DEFAULT_THRESHOLD,
29
+ strategy: 'semantic',
30
+ fallbackToOriginal: true,
31
+ };
32
+
33
+ function resolveThreshold(threshold: number | Record<string, number>, lang: string): number {
34
+ if (typeof threshold === 'number') return threshold;
35
+ return threshold[lang] ?? threshold['*'] ?? DEFAULT_THRESHOLD;
36
+ }
37
+
38
+ let analyzer: SemanticAnalyzer | null = null;
39
+
40
+ function getAnalyzer(): SemanticAnalyzer {
41
+ if (!analyzer) {
42
+ analyzer = createSemanticAnalyzer();
43
+ }
44
+ return analyzer;
45
+ }
46
+
47
+ /**
48
+ * Preprocess non-English hyperscript into English.
49
+ * Identical to preprocessToEnglish in preprocessor.ts but with slim imports.
50
+ *
51
+ * Handles _hyperscript feature prefixes (e.g. "on click", "on every keyup")
52
+ * by stripping them, translating only the command portion, then reassembling.
53
+ */
54
+ export function preprocessToEnglish(
55
+ src: string,
56
+ lang: string,
57
+ config: Partial<PreprocessorConfig> = {}
58
+ ): string {
59
+ const cfg = { ...DEFAULT_CONFIG, ...config };
60
+
61
+ // Try translating the full string first
62
+ const fullResult = tryTranslateWithStrategies(src, lang, cfg);
63
+ if (fullResult !== null) return fullResult;
64
+
65
+ // If full translation failed, try stripping event/feature prefix
66
+ const stripped = stripEventPrefix(src);
67
+ if (stripped) {
68
+ const translated = tryTranslateWithStrategies(stripped.commands, lang, cfg);
69
+ if (translated !== null) return stripped.prefix + translated;
70
+ }
71
+
72
+ return src;
73
+ }
74
+
75
+ function tryTranslateWithStrategies(
76
+ src: string,
77
+ lang: string,
78
+ cfg: PreprocessorConfig
79
+ ): string | null {
80
+ if (cfg.strategy === 'semantic' || cfg.strategy === 'auto') {
81
+ const threshold = resolveThreshold(cfg.confidenceThreshold, lang);
82
+ const result = trySemanticTranslation(src, lang, threshold);
83
+ if (result !== null) return result;
84
+ }
85
+
86
+ if ((cfg.strategy === 'auto' || cfg.strategy === 'i18n') && cfg.i18nToEnglish) {
87
+ const result = tryI18nTranslation(src, lang, cfg.i18nToEnglish);
88
+ if (result !== null) return result;
89
+ }
90
+
91
+ return null;
92
+ }
93
+
94
+ /** Match _hyperscript event handler prefix. */
95
+ const EVENT_PREFIX_RE =
96
+ /^(on\s+(?:every\s+)?[\w-]+(?:\[.*?\])?(?:\.[\w-]+(?:\([^)]*\))?)*(?:\s+from\s+\S+)?(?:\s+queue\s+\w+)?\s+)/;
97
+
98
+ function stripEventPrefix(src: string): { prefix: string; commands: string } | null {
99
+ const match = src.match(EVENT_PREFIX_RE);
100
+ if (!match) return null;
101
+ const prefix = match[1];
102
+ const commands = src.slice(prefix.length);
103
+ if (!commands) return null;
104
+ return { prefix, commands };
105
+ }
106
+
107
+ function trySemanticTranslation(src: string, lang: string, threshold: number): string | null {
108
+ try {
109
+ const statements = splitStatements(src, lang);
110
+ if (statements.length > 1) {
111
+ return translateCompound(statements, lang, threshold);
112
+ }
113
+
114
+ const sem = getAnalyzer();
115
+ if (!sem.supportsLanguage(lang)) return null;
116
+
117
+ const result = sem.analyze(src, lang);
118
+ if (!shouldUseSemanticResult(result, threshold)) return null;
119
+
120
+ if (result.node) {
121
+ return renderToHyperscript(result.node);
122
+ }
123
+
124
+ // Fallback: parse then render via custom renderer
125
+ const node = parse(src, lang);
126
+ return renderToHyperscript(node);
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ function tryI18nTranslation(
133
+ src: string,
134
+ lang: string,
135
+ toEnglish: (input: string, locale: string) => string
136
+ ): string | null {
137
+ try {
138
+ const result = toEnglish(src, lang);
139
+ return result !== src ? result : null;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Get all "then" keyword forms for a language (primary + alternatives).
147
+ * Falls back to English "then" if profile is unavailable.
148
+ */
149
+ function getThenKeywords(lang: string): string[] {
150
+ const keywords = ['then']; // Always include English
151
+ const profile = tryGetProfile(lang);
152
+ if (profile?.keywords?.then) {
153
+ const kw = profile.keywords.then;
154
+ if (kw.primary && kw.primary !== 'then') keywords.push(kw.primary);
155
+ if (kw.alternatives) {
156
+ for (const alt of kw.alternatives) {
157
+ if (!keywords.includes(alt)) keywords.push(alt);
158
+ }
159
+ }
160
+ }
161
+ return keywords;
162
+ }
163
+
164
+ function splitStatements(src: string, lang: string): string[] {
165
+ const thenWords = getThenKeywords(lang);
166
+ // Escape regex special chars and build pattern
167
+ const escaped = thenWords.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
168
+ const pattern = new RegExp(`\\s+(?:${escaped.join('|')})\\s+`, 'i');
169
+
170
+ const lines = src
171
+ .split('\n')
172
+ .map(l => l.trim())
173
+ .filter(l => l.length > 0);
174
+ const result: string[] = [];
175
+ for (const line of lines) {
176
+ const parts = line.split(pattern);
177
+ result.push(...parts);
178
+ }
179
+ return result;
180
+ }
181
+
182
+ function translateCompound(statements: string[], lang: string, threshold: number): string | null {
183
+ const translated: string[] = [];
184
+ for (const stmt of statements) {
185
+ const result = trySemanticTranslation(stmt.trim(), lang, threshold);
186
+ if (result === null) return null;
187
+ translated.push(result);
188
+ }
189
+ return translated.join(' then ');
190
+ }