@lokascript/hyperscript-adapter 3.1.1 → 3.3.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 (43) hide show
  1. package/CHANGELOG.md +186 -1
  2. package/README.md +30 -25
  3. package/dist/hyperscript-i18n-ar.global.js +6 -5
  4. package/dist/hyperscript-i18n-bn.global.js +6 -5
  5. package/dist/hyperscript-i18n-de.global.js +6 -5
  6. package/dist/hyperscript-i18n-east-asian.global.js +6 -5
  7. package/dist/hyperscript-i18n-en.global.js +8 -7
  8. package/dist/hyperscript-i18n-es.global.js +6 -5
  9. package/dist/hyperscript-i18n-fr.global.js +6 -5
  10. package/dist/hyperscript-i18n-he.global.js +6 -5
  11. package/dist/hyperscript-i18n-hi.global.js +6 -5
  12. package/dist/hyperscript-i18n-id.global.js +6 -5
  13. package/dist/hyperscript-i18n-it.global.js +6 -5
  14. package/dist/hyperscript-i18n-ja.global.js +6 -5
  15. package/dist/hyperscript-i18n-ko.global.js +6 -5
  16. package/dist/hyperscript-i18n-lite.global.js +2 -2
  17. package/dist/hyperscript-i18n-ms.global.js +6 -5
  18. package/dist/hyperscript-i18n-pl.global.js +6 -5
  19. package/dist/hyperscript-i18n-pt.global.js +6 -5
  20. package/dist/hyperscript-i18n-qu.global.js +6 -5
  21. package/dist/hyperscript-i18n-ru.global.js +6 -5
  22. package/dist/hyperscript-i18n-slavic.global.js +6 -5
  23. package/dist/hyperscript-i18n-south-asian.global.js +6 -5
  24. package/dist/hyperscript-i18n-southeast-asian.global.js +6 -5
  25. package/dist/hyperscript-i18n-sw.global.js +6 -5
  26. package/dist/hyperscript-i18n-th.global.js +6 -5
  27. package/dist/hyperscript-i18n-tl.global.js +6 -5
  28. package/dist/hyperscript-i18n-tr.global.js +6 -5
  29. package/dist/hyperscript-i18n-uk.global.js +6 -5
  30. package/dist/hyperscript-i18n-vi.global.js +6 -5
  31. package/dist/hyperscript-i18n-western.global.js +6 -5
  32. package/dist/hyperscript-i18n-zh.global.js +6 -5
  33. package/dist/hyperscript-i18n.global.js +28 -27
  34. package/dist/hyperscript-i18n.global.js.map +1 -1
  35. package/dist/index.cjs +17 -2
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.js +17 -2
  38. package/dist/index.js.map +1 -1
  39. package/package.json +7 -4
  40. package/src/attribute-translator.ts +18 -2
  41. package/src/generated/syntax-table.ts +1 -1
  42. package/src/host-validate.ts +39 -7
  43. package/src/hyperscript-renderer.ts +21 -0
package/dist/index.cjs CHANGED
@@ -137,6 +137,10 @@ function findScriptAttribute(elt, attrNames) {
137
137
  return null;
138
138
  }
139
139
  function installAttributeTranslator(hs, translate2) {
140
+ if (typeof hs.addSourceTransform === "function") {
141
+ hs.addSourceTransform(translate2);
142
+ return;
143
+ }
140
144
  if (typeof hs.addBeforeProcessHook !== "function") {
141
145
  console.warn(
142
146
  "[hyperscript-i18n] _hyperscript.addBeforeProcessHook is unavailable \u2014 this build of _hyperscript.org is not supported. Load a current version from https://unpkg.com/hyperscript.org."
@@ -171,11 +175,22 @@ function installAttributeTranslator(hs, translate2) {
171
175
  }
172
176
 
173
177
  // src/host-validate.ts
178
+ var REFERENCE_NAMES = /* @__PURE__ */ new Set(["me", "my", "i", "it", "its", "you", "your"]);
179
+ function referenceReadAsEvent(result) {
180
+ for (const feature of result?.features ?? []) {
181
+ for (const event of feature.events ?? []) {
182
+ if (typeof event.on === "string" && REFERENCE_NAMES.has(event.on.toLowerCase()))
183
+ return event.on;
184
+ }
185
+ }
186
+ return void 0;
187
+ }
174
188
  function acceptedByHost(hs, src) {
175
189
  if (typeof hs.parse !== "function") return true;
176
190
  try {
177
191
  const result = hs.parse(src);
178
- return !result?.errors || result.errors.length === 0;
192
+ if (result?.errors && result.errors.length > 0) return false;
193
+ return referenceReadAsEvent(result) === void 0;
179
194
  } catch {
180
195
  return false;
181
196
  }
@@ -188,7 +203,7 @@ function warnRejectedOnce(lang, src, english) {
188
203
  if (warnedRejectedLang.has(lang)) return;
189
204
  warnedRejectedLang.add(lang);
190
205
  console.warn(
191
- `[hyperscript-i18n] Translation for lang="${lang}" rendered hyperscript the host parser rejects \u2014 falling back to the original text. Source: "${src.length > 60 ? src.slice(0, 60) + "\u2026" : src}" \u2192 "${english.length > 60 ? english.slice(0, 60) + "\u2026" : english}". Further elements in this language stay quiet \u2014 enable { debug: true } for per-element detail.`
206
+ `[hyperscript-i18n] Translation for lang="${lang}" rendered hyperscript the host parser rejects (or reads as a handler for an event named like me/it/you) \u2014 falling back to the original text. Source: "${src.length > 60 ? src.slice(0, 60) + "\u2026" : src}" \u2192 "${english.length > 60 ? english.slice(0, 60) + "\u2026" : english}". Further elements in this language stay quiet \u2014 enable { debug: true } for per-element detail.`
192
207
  );
193
208
  }
194
209
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/language-resolver.ts","../src/preprocessor.ts","../src/preprocessor-core.ts","../src/attribute-translator.ts","../src/host-validate.ts","../src/plugin.ts"],"sourcesContent":["/**\n * @lokascript/hyperscript-adapter\n *\n * Multilingual adapter plugin for the original _hyperscript.\n * Enables writing hyperscript in any of 24 supported languages.\n *\n * @example\n * import { hyperscriptI18n } from '@lokascript/hyperscript-adapter';\n * _hyperscript.use(hyperscriptI18n());\n *\n * @example\n * // With options\n * _hyperscript.use(hyperscriptI18n({\n * defaultLanguage: 'ja',\n * debug: true,\n * }));\n *\n * @example\n * // Standalone preprocessing (for _hyperscript.evaluate() calls)\n * import { preprocess } from '@lokascript/hyperscript-adapter';\n * const english = preprocess('トグル .active', 'ja');\n * _hyperscript(english);\n */\n\nexport {\n hyperscriptI18n,\n preprocess,\n resetTranslationWarnings,\n type PluginOptions,\n} from './plugin';\nexport { preprocessToEnglish, type PreprocessorConfig } from './preprocessor';\nexport { resolveLanguage } from './language-resolver';\n","/**\n * Language Resolver\n *\n * Determines the language of hyperscript code on a given element\n * using a cascading resolution strategy.\n */\n\n/**\n * Resolve the language for a hyperscript attribute on an element.\n *\n * Resolution order:\n * 1. `data-lang` attribute on the element itself\n * 2. `data-hyperscript-lang` attribute on the element or closest ancestor\n * 3. `lang` attribute on the element or closest ancestor (the HTML-standard\n * cascade — a `<section lang=\"es\">` localizes everything inside it, and a\n * nested `lang=\"en\"` opts back out). This is how the paired htmx-adapter\n * (`langOf()`) and loka-js resolve language, so `hx-*` and `_` attributes\n * on the same element agree.\n * 4. `lang` on `<html>` via `document.documentElement` — only reachable when\n * the element is DETACHED (step 3's ancestor walk covers `<html>` for\n * attached elements): a hook processing a not-yet-inserted fragment still\n * picks up the page default.\n * 5. null (assume English, no preprocessing needed)\n */\nexport function resolveLanguage(elt: Element): string | null {\n // 1. Explicit per-element\n const dataLang = elt.getAttribute('data-lang');\n if (dataLang) return normalizeLangCode(dataLang);\n\n // 2. Inherited from ancestor\n const hsLang =\n elt.getAttribute('data-hyperscript-lang') ??\n elt.closest?.('[data-hyperscript-lang]')?.getAttribute('data-hyperscript-lang');\n if (hsLang) return normalizeLangCode(hsLang);\n\n // 3. Standard lang cascade (nearest ancestor wins)\n const closestLang = elt.closest?.('[lang]')?.getAttribute('lang');\n if (closestLang) return normalizeLangCode(closestLang);\n\n // 4. Document-level lang (detached elements only — see doc comment)\n const htmlLang = typeof document !== 'undefined' ? document.documentElement?.lang : null;\n if (htmlLang && htmlLang !== 'en') return normalizeLangCode(htmlLang);\n\n return null;\n}\n\n/**\n * Normalize a BCP-47 language tag to an ISO 639-1 code.\n * e.g., \"ja-JP\" → \"ja\", \"zh-Hans\" → \"zh\", \"pt-BR\" → \"pt\"\n */\nfunction normalizeLangCode(lang: string): string {\n return lang.split('-')[0].toLowerCase();\n}\n","/**\n * Preprocessor (full path)\n *\n * Translates non-English hyperscript to English using the semantic parser\n * (with optional i18n fallback), so the original _hyperscript can parse it.\n *\n * The strategy/split/strip skeleton lives in preprocessor-core.ts, shared\n * with the slim path — only HOW one statement is parsed and rendered\n * differs here: semantic's parseSemantic + render('en'), plus a\n * translate() rescue when the parse is confident but yields no node.\n */\n\nimport { translate, render, parseSemantic, isLanguageRegistered } from '@lokascript/semantic';\n\nimport { createPreprocessToEnglish, type PreprocessorConfig } from './preprocessor-core';\n\nexport type { PreprocessorConfig };\n\n/**\n * Preprocess non-English hyperscript into English.\n *\n * Uses the semantic parser to parse the input in the source language,\n * then renders back to English. Falls through to i18n if configured.\n */\nexport const preprocessToEnglish = createPreprocessToEnglish({\n isLanguageRegistered,\n translateSingle(src, lang, threshold) {\n const result = parseSemantic(src, lang);\n if (result.confidence < threshold || !result.node) {\n // If we got SOME confidence but no node, fall back to translate()\n if (result.confidence >= threshold) {\n return translate(src, lang, 'en');\n }\n return null;\n }\n\n // Render the semantic node to English\n return render(result.node, 'en');\n },\n});\n","/**\n * Preprocessor core — the shared skeleton of the full and slim\n * preprocessors.\n *\n * Everything that is NOT genuinely different between the two paths lives\n * here exactly once: config defaults/threshold resolution, strategy\n * ordering (semantic → optional i18n), event-prefix stripping, and the\n * return-original fallback. The two real differences stay with each path,\n * injected as hooks:\n *\n * - HOW one statement is parsed and rendered to English\n * (`translateSingle`): the full path uses semantic's parseSemantic +\n * render + a translate() rescue for confident-but-nodeless parses;\n * the slim path uses parseWithConfidence + the custom\n * hyperscript-renderer (no English language data).\n * - WHERE the registry lookup comes from (`isLanguageRegistered`):\n * `@lokascript/semantic` vs `…/core` — under tsup's split dist these\n * are separate registry instances, so each path must bring its own.\n *\n * Behavior is pinned by the parity ratchet\n * (test/preprocessor-parity.*.test.ts + the committed fixture): both paths\n * are byte-identical to their pre-extraction outputs over the corpus.\n *\n * HISTORY: the skeleton used to carry a compound-splitting fallback\n * (`splitStatements` on localized `then` keywords + newlines,\n * `translateCompound` rejoining pieces with a hardcoded ` then `), run when\n * the whole-string parse declined. Deleted 2026-08-07 after measurement:\n * over all 3703 patterns.db corpus translations plus the parity corpus, on\n * BOTH paths, the split arm produced ZERO final outputs (full: 115\n * invocations, all returned null; slim: 32, all null) — post-whole-string-\n * first (#899), an input whose pieces each parse would have parsed whole,\n * so the arm's success condition is self-defeating. It was also defective\n * twice over: the regex split was string-literal-blind (`say 'now then\n * later'` split inside the literal) and the ` then ` rejoin is invalid\n * immediately after block headers. Deleting it erased both defect classes\n * wholesale; the parity fixture regenerated byte-identical.\n */\n\nexport interface PreprocessorConfig {\n /**\n * Minimum confidence threshold for semantic parsing (0-1). Default: 0.5.\n * Can be a single number (applies to all languages) or a per-language map.\n * Per-language thresholds are useful because SOV languages (ja, ko, tr) produce\n * inherently lower confidence scores than SVO languages (es, fr, de).\n *\n * @example\n * // Single threshold\n * { confidenceThreshold: 0.5 }\n *\n * @example\n * // Per-language thresholds\n * { confidenceThreshold: { es: 0.7, ja: 0.1, ko: 0.05, '*': 0.5 } }\n */\n confidenceThreshold: number | Record<string, number>;\n /** Strategy: 'semantic' (default), 'i18n', or 'auto' (semantic then i18n) */\n strategy: 'semantic' | 'i18n' | 'auto';\n /**\n * @deprecated Never implemented — `preprocessToEnglish` always returns a\n * string, and on translation failure that string is the original source\n * (there is nothing else it could return). The option has had no effect in\n * any released version and is ignored; it will be removed in a future major.\n */\n fallbackToOriginal?: boolean;\n /** Optional i18n toEnglish function (loaded dynamically if available) */\n i18nToEnglish?: (input: string, locale: string) => string;\n}\n\n/** The per-path seam — see module doc. */\nexport interface PreprocessorHooks {\n isLanguageRegistered(lang: string): boolean;\n /**\n * Parse ONE statement in `lang` and render it to English. Return null\n * when it cannot be translated (below-threshold confidence, no node, …);\n * the skeleton then tries the remaining strategies / falls back.\n */\n translateSingle(src: string, lang: string, threshold: number): string | null;\n}\n\nconst DEFAULT_THRESHOLD = 0.5;\n\nconst DEFAULT_CONFIG: PreprocessorConfig = {\n confidenceThreshold: DEFAULT_THRESHOLD,\n strategy: 'semantic',\n};\n\n/**\n * Resolve the confidence threshold for a specific language.\n * Supports both a single number and a per-language map with '*' as default.\n */\nfunction resolveThreshold(threshold: number | Record<string, number>, lang: string): number {\n if (typeof threshold === 'number') return threshold;\n return threshold[lang] ?? threshold['*'] ?? DEFAULT_THRESHOLD;\n}\n\n/**\n * Match _hyperscript event handler prefix: \"on [every] <event>[filter][.modifiers] \"\n *\n * Examples:\n * \"on click toggle .active\" → prefix: \"on click \", commands: \"toggle .active\"\n * \"on every click toggle .active\" → prefix: \"on every click \", commands: \"toggle .active\"\n * \"on click.debounce(300) toggle .x\" → prefix: \"on click.debounce(300) \", commands: \"toggle .x\"\n * \"on keyup[key=='Enter'] set x to 1\" → prefix: \"on keyup[key=='Enter'] \", commands: \"set x to 1\"\n * \"on click from body toggle .active\" → prefix: \"on click from body \", commands: \"toggle .active\"\n */\nconst EVENT_PREFIX_RE =\n /^(on\\s+(?:every\\s+)?[\\w-]+(?:\\[.*?\\])?(?:\\.[\\w-]+(?:\\([^)]*\\))?)*(?:\\s+from\\s+\\S+)?(?:\\s+queue\\s+\\w+)?\\s+)/;\n\nfunction stripEventPrefix(src: string): { prefix: string; commands: string } | null {\n const match = src.match(EVENT_PREFIX_RE);\n if (!match) return null;\n const prefix = match[1];\n const commands = src.slice(prefix.length);\n if (!commands) return null;\n return { prefix, commands };\n}\n\n/**\n * Build a `preprocessToEnglish` function from the per-path hooks.\n *\n * Handles _hyperscript feature prefixes (e.g. \"on click\", \"on every keyup\")\n * by stripping them, translating only the command portion, then reassembling.\n */\nexport function createPreprocessToEnglish(\n hooks: PreprocessorHooks\n): (src: string, lang: string, config?: Partial<PreprocessorConfig>) => string {\n /**\n * Try semantic translation: parse the WHOLE string in the source\n * language, render it to English. Returns null if confidence is below\n * threshold. The semantic parser handles `then`-sequences, newlines,\n * loop/tell bodies and behavior blocks natively (top-level\n * command-sequence support landed with the multiset-recall arc), so\n * there is deliberately no pre-splitting here — see the module doc's\n * HISTORY note for the measured deletion of the old split fallback.\n */\n function trySemanticTranslation(src: string, lang: string, threshold: number): string | null {\n try {\n // Check if the semantic parser can handle this\n if (!hooks.isLanguageRegistered(lang)) return null;\n\n return hooks.translateSingle(src, lang, threshold);\n } catch {\n return null;\n }\n }\n\n /**\n * Try i18n grammar transformation to English.\n * Returns null if the result is identical to input (no translation happened).\n */\n function tryI18nTranslation(\n src: string,\n lang: string,\n toEnglish: (input: string, locale: string) => string\n ): string | null {\n try {\n const result = toEnglish(src, lang);\n return result !== src ? result : null;\n } catch {\n return null;\n }\n }\n\n /**\n * Try all configured translation strategies on the input.\n * Returns null if none succeed.\n */\n function tryTranslateWithStrategies(\n src: string,\n lang: string,\n cfg: PreprocessorConfig\n ): string | null {\n // Strategy: semantic-only\n if (cfg.strategy === 'semantic' || cfg.strategy === 'auto') {\n const threshold = resolveThreshold(cfg.confidenceThreshold, lang);\n const result = trySemanticTranslation(src, lang, threshold);\n if (result !== null) return result;\n }\n\n // Strategy: i18n fallback (auto mode or i18n-only)\n if ((cfg.strategy === 'auto' || cfg.strategy === 'i18n') && cfg.i18nToEnglish) {\n const result = tryI18nTranslation(src, lang, cfg.i18nToEnglish);\n if (result !== null) return result;\n }\n\n return null;\n }\n\n return function preprocessToEnglish(\n src: string,\n lang: string,\n config: Partial<PreprocessorConfig> = {}\n ): string {\n // English→English is identity; skip semantic parsing which may mangle\n // the input. (Historically full-path only — the slim path gained it in\n // the shared-skeleton extraction, deliberately: it is a mangle guard.)\n if (lang === 'en') return src;\n\n const cfg = { ...DEFAULT_CONFIG, ...config };\n\n // Try translating the full string first\n const fullResult = tryTranslateWithStrategies(src, lang, cfg);\n if (fullResult !== null) return fullResult;\n\n // If full translation failed, try stripping event/feature prefix.\n // _hyperscript attributes often contain \"on <event> <commands>\" — the semantic\n // parser only understands command syntax, not event declarations.\n const stripped = stripEventPrefix(src);\n if (stripped) {\n const translated = tryTranslateWithStrategies(stripped.commands, lang, cfg);\n if (translated !== null) return stripped.prefix + translated;\n }\n\n // Fallback: return original (unconditional — see fallbackToOriginal's\n // deprecation note; the string contract leaves nothing else to return)\n return src;\n };\n}\n","/**\n * Attribute translator\n *\n * _hyperscript.org's Runtime#getScript is a private class field (`#getScript`),\n * not a property reachable through `internals.runtime` — every plugin variant\n * in this package used to monkey-patch `internals.runtime.getScript`, which\n * silently no-ops against current _hyperscript.org builds (the assignment\n * creates a stray own-property that the runtime's internal `#getScript()`\n * calls never read, so translation never happens and errors are swallowed by\n * `runtime.getScript.bind` throwing before any hyperscript even parses).\n *\n * `addBeforeProcessHook` is the supported, public extension point instead: it\n * fires on the subtree root passed to `processNode()` before the runtime reads\n * whichever configured attribute (`_`, `script`, `data-script` by default) or\n * `<script type=\"text/hyperscript\">` body holds the source. Rewriting that\n * attribute/body in place — before the runtime's own scan reaches it — gets\n * the same \"translate before parse\" effect through a mechanism the runtime\n * actually calls.\n */\n\nexport interface HyperscriptHost {\n addBeforeProcessHook?: (fn: (elt: Element) => void) => void;\n config?: { attributes?: string };\n}\n\n/** Elements already processed, so a later `processNode()` call over the same\n * subtree (e.g. a sibling swap re-scanning a shared ancestor) doesn't\n * re-translate already-English text as if it were still the original language.\n * A WeakSet instead of a marker attribute: the same idempotency with zero DOM\n * mutation (devtools/serialization show exactly what the author wrote).\n * Serialize→reparse (e.g. an innerHTML round-trip) produces NEW elements that\n * are re-processed — safe, because re-translating already-English text is\n * confidence-gated into a no-op. */\nconst processed = new WeakSet<Element>();\n\nfunction scriptAttributeNames(hs: HyperscriptHost): string[] {\n const raw = hs.config?.attributes ?? '_, script, data-script';\n return raw\n .split(',')\n .map(s => s.trim())\n .filter(Boolean);\n}\n\nfunction findScriptAttribute(elt: Element, attrNames: string[]): string | null {\n for (const name of attrNames) {\n if (elt.hasAttribute(name)) return name;\n }\n return null;\n}\n\n/**\n * Install a translator that rewrites non-English script attributes to English\n * in place, before `_hyperscript.org` parses them.\n *\n * @param translate Given the raw source and its element, return the English\n * translation, or `null`/the same string to leave the element untouched\n * (English input, unresolved language, translation failure, etc).\n */\nexport function installAttributeTranslator(\n hs: HyperscriptHost,\n translate: (src: string, elt: Element) => string | null\n): void {\n if (typeof hs.addBeforeProcessHook !== 'function') {\n console.warn(\n '[hyperscript-i18n] _hyperscript.addBeforeProcessHook is unavailable — ' +\n 'this build of _hyperscript.org is not supported. Load a current version ' +\n 'from https://unpkg.com/hyperscript.org.'\n );\n return;\n }\n\n const attrNames = scriptAttributeNames(hs);\n const selector = [...attrNames.map(a => `[${a}]`), 'script[type=\"text/hyperscript\"]'].join(', ');\n\n const translateOne = (elt: Element): void => {\n if (processed.has(elt)) return;\n\n if (elt instanceof HTMLScriptElement && elt.type === 'text/hyperscript') {\n const src = elt.textContent ?? '';\n if (!src) return;\n const english = translate(src, elt);\n processed.add(elt);\n if (english != null && english !== src) elt.textContent = english;\n return;\n }\n\n const attr = findScriptAttribute(elt, attrNames);\n if (!attr) return;\n const src = elt.getAttribute(attr);\n if (!src) return;\n const english = translate(src, elt);\n processed.add(elt);\n if (english != null && english !== src) elt.setAttribute(attr, english);\n };\n\n hs.addBeforeProcessHook((root: Element) => {\n if (!root || typeof root.querySelectorAll !== 'function') return;\n if (typeof root.matches === 'function' && root.matches(selector)) translateOne(root);\n root.querySelectorAll(selector).forEach(translateOne);\n });\n}\n","/**\n * Host-parser validity gate (review item F8).\n *\n * After the preprocessor rewrites an attribute to English, the plugin asks\n * the HOST _hyperscript runtime — the same parser that will consume the\n * rewrite moments later — whether the result actually parses. On rejection\n * the plugin falls back to the author's original text, so any parse error\n * the author then sees names code they wrote, not invisible generated\n * English. This is the runtime analog of the offline R4 canonical-validity\n * gate, and the F5 arc measured its failure class shipping in practice:\n * until the whole-string-first reorder (#899), 256 corpus rows rendered\n * English the engine rejects, with no warning anywhere.\n *\n * The engine has two failure channels — `parse().errors` collects grammar\n * errors, and the tokenizer THROWS on an unknown character — folded here\n * the same way test/whole-string-first.test.ts folds them.\n *\n * Zero-dependency module: shared by the full, slim, and lite plugin\n * variants, which must not share heavier import chains (the slim/lite\n * bundles exclude the full semantic package by construction).\n */\n\nexport interface HyperscriptParseHost {\n parse?: (src: string) => { errors?: unknown[] } | null | undefined;\n}\n\n/**\n * True when the host's parser accepts `src`. Also true when the host\n * exposes no `parse()` — with nothing to validate against, the gate\n * degrades to a no-op rather than suppressing translation on unusual\n * builds (same graceful posture as the `addBeforeProcessHook` check).\n */\nexport function acceptedByHost(hs: HyperscriptParseHost, src: string): boolean {\n if (typeof hs.parse !== 'function') return true;\n try {\n const result = hs.parse(src);\n return !result?.errors || result.errors.length === 0;\n } catch {\n return false;\n }\n}\n\n/** Languages already warned about a rejected translation this page load —\n * same warn-once-per-lang convention as the unchanged-translation warning\n * (and htmx-adapter's warnMissingLangOnce). */\nconst warnedRejectedLang = new Set<string>();\n\n/** Reset the warn-once state. Mainly for tests. */\nexport function resetHostValidationWarnings(): void {\n warnedRejectedLang.clear();\n}\n\nexport function warnRejectedOnce(lang: string, src: string, english: string): void {\n if (warnedRejectedLang.has(lang)) return;\n warnedRejectedLang.add(lang);\n console.warn(\n `[hyperscript-i18n] Translation for lang=\"${lang}\" rendered hyperscript the host parser ` +\n `rejects — falling back to the original text. ` +\n `Source: \"${src.length > 60 ? src.slice(0, 60) + '…' : src}\" → ` +\n `\"${english.length > 60 ? english.slice(0, 60) + '…' : english}\". ` +\n 'Further elements in this language stay quiet — enable { debug: true } for per-element detail.'\n );\n}\n","/**\n * _hyperscript Plugin\n *\n * Registers with _hyperscript.use() to rewrite non-English hyperscript\n * attributes into English before _hyperscript.org parses them.\n */\n\nimport { resolveLanguage } from './language-resolver';\nimport { preprocessToEnglish, type PreprocessorConfig } from './preprocessor';\nimport { installAttributeTranslator, type HyperscriptHost } from './attribute-translator';\nimport {\n acceptedByHost,\n warnRejectedOnce,\n resetHostValidationWarnings,\n type HyperscriptParseHost,\n} from './host-validate';\n\nexport interface PluginOptions extends Partial<PreprocessorConfig> {\n /** Default language for all elements (overridable per-element). */\n defaultLanguage?: string;\n /** Custom attribute name for per-element language. Default: \"data-lang\" */\n languageAttribute?: string;\n /** Enable debug logging to console. Default: false */\n debug?: boolean;\n /**\n * Validate rendered English on the HOST parser before committing the\n * rewrite; on rejection, fall back to the original text (so parse errors\n * name the author's code, not generated English). Default: true.\n * No-op on host builds that expose no `parse()`.\n */\n validateWithHost?: boolean;\n}\n\n/** Languages already warned about an unchanged translation this page load.\n * Unchanged output is common and often legitimate (canonical-English\n * hyperscript under a non-en lang scope), so warning per element per\n * processNode was pure noise — mirror htmx-adapter's warn-once-per-lang\n * convention and leave per-element detail to `debug: true`. */\nconst warnedUnchangedLang = new Set<string>();\n\n/** Reset the warn-once state (unchanged + host-rejected). Mainly for tests. */\nexport function resetTranslationWarnings(): void {\n warnedUnchangedLang.clear();\n resetHostValidationWarnings();\n}\n\nfunction warnUnchangedOnce(lang: string, src: string): void {\n if (warnedUnchangedLang.has(lang)) return;\n warnedUnchangedLang.add(lang);\n console.warn(\n `[hyperscript-i18n] Translation unchanged for lang=\"${lang}\": \"${src.length > 60 ? src.slice(0, 60) + '…' : src}\". ` +\n 'This is fine if the source is already canonical English; otherwise the input may not match ' +\n 'any known pattern, or the language may not be registered. Original text is passed to ' +\n '_hyperscript as-is. Further elements in this language stay quiet — enable { debug: true } ' +\n 'for per-element detail.'\n );\n}\n\n/**\n * Create a _hyperscript plugin that enables multilingual hyperscript.\n *\n * @example\n * // Basic usage\n * _hyperscript.use(hyperscriptI18n());\n *\n * @example\n * // With options\n * _hyperscript.use(hyperscriptI18n({\n * defaultLanguage: 'ja',\n * confidenceThreshold: 0.6,\n * debug: true,\n * }));\n */\nexport function hyperscriptI18n(options: PluginOptions = {}) {\n return function plugin(hs: unknown): void {\n const host = hs as HyperscriptHost & HyperscriptParseHost;\n installAttributeTranslator(host, (src, elt) => {\n // Resolve language\n const lang = resolveLanguageWithOptions(elt, options);\n\n // If English or no language detected, pass through\n if (!lang || lang === 'en') return src;\n\n // Preprocess to English\n const english = preprocessToEnglish(src, lang, options);\n\n if (english !== src) {\n // Validity gate: the host parser is the consumer of this rewrite —\n // if it rejects the English, committing it would only trade a\n // translation gap for a parse error naming code the author never\n // wrote. Fall back to the original text instead.\n if (options.validateWithHost !== false && !acceptedByHost(host, english)) {\n if (options.debug) {\n console.log(\n `[hyperscript-i18n] ${lang}: host rejected \"${english}\" — keeping \"${src}\"`\n );\n } else {\n warnRejectedOnce(lang, src, english);\n }\n return src;\n }\n if (options.debug) {\n console.log(`[hyperscript-i18n] ${lang}: \"${src}\" → \"${english}\"`);\n }\n } else if (options.debug) {\n console.log(`[hyperscript-i18n] ${lang}: unchanged \"${src}\"`);\n } else {\n warnUnchangedOnce(lang, src);\n }\n\n return english;\n });\n };\n}\n\n/**\n * Resolve language with plugin options factored in.\n */\nfunction resolveLanguageWithOptions(elt: Element, options: PluginOptions): string | null {\n // Check custom attribute name\n if (options.languageAttribute) {\n const custom = elt.getAttribute(options.languageAttribute);\n if (custom) return custom.split('-')[0].toLowerCase();\n }\n\n // Standard resolution\n const resolved = resolveLanguage(elt);\n if (resolved) return resolved;\n\n // Fall back to default language\n return options.defaultLanguage ?? null;\n}\n\n/**\n * Standalone preprocessing function for programmatic use.\n * Call this when using _hyperscript.evaluate() or _hyperscript(\"code\") directly.\n *\n * @example\n * const english = preprocess(\"トグル .active\", \"ja\");\n * _hyperscript(english);\n */\nexport function preprocess(\n src: string,\n lang: string,\n config: Partial<PreprocessorConfig> = {}\n): string {\n if (lang === 'en') return src;\n return preprocessToEnglish(src, lang, config);\n}\n"],"mappings":"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","names":["preprocessToEnglish","translate","src","english"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/language-resolver.ts","../src/preprocessor.ts","../src/preprocessor-core.ts","../src/attribute-translator.ts","../src/host-validate.ts","../src/plugin.ts"],"sourcesContent":["/**\n * @lokascript/hyperscript-adapter\n *\n * Multilingual adapter plugin for the original _hyperscript.\n * Enables writing hyperscript in any of 24 supported languages.\n *\n * @example\n * import { hyperscriptI18n } from '@lokascript/hyperscript-adapter';\n * _hyperscript.use(hyperscriptI18n());\n *\n * @example\n * // With options\n * _hyperscript.use(hyperscriptI18n({\n * defaultLanguage: 'ja',\n * debug: true,\n * }));\n *\n * @example\n * // Standalone preprocessing (for _hyperscript.evaluate() calls)\n * import { preprocess } from '@lokascript/hyperscript-adapter';\n * const english = preprocess('トグル .active', 'ja');\n * _hyperscript(english);\n */\n\nexport {\n hyperscriptI18n,\n preprocess,\n resetTranslationWarnings,\n type PluginOptions,\n} from './plugin';\nexport { preprocessToEnglish, type PreprocessorConfig } from './preprocessor';\nexport { resolveLanguage } from './language-resolver';\n","/**\n * Language Resolver\n *\n * Determines the language of hyperscript code on a given element\n * using a cascading resolution strategy.\n */\n\n/**\n * Resolve the language for a hyperscript attribute on an element.\n *\n * Resolution order:\n * 1. `data-lang` attribute on the element itself\n * 2. `data-hyperscript-lang` attribute on the element or closest ancestor\n * 3. `lang` attribute on the element or closest ancestor (the HTML-standard\n * cascade — a `<section lang=\"es\">` localizes everything inside it, and a\n * nested `lang=\"en\"` opts back out). This is how the paired htmx-adapter\n * (`langOf()`) and loka-js resolve language, so `hx-*` and `_` attributes\n * on the same element agree.\n * 4. `lang` on `<html>` via `document.documentElement` — only reachable when\n * the element is DETACHED (step 3's ancestor walk covers `<html>` for\n * attached elements): a hook processing a not-yet-inserted fragment still\n * picks up the page default.\n * 5. null (assume English, no preprocessing needed)\n */\nexport function resolveLanguage(elt: Element): string | null {\n // 1. Explicit per-element\n const dataLang = elt.getAttribute('data-lang');\n if (dataLang) return normalizeLangCode(dataLang);\n\n // 2. Inherited from ancestor\n const hsLang =\n elt.getAttribute('data-hyperscript-lang') ??\n elt.closest?.('[data-hyperscript-lang]')?.getAttribute('data-hyperscript-lang');\n if (hsLang) return normalizeLangCode(hsLang);\n\n // 3. Standard lang cascade (nearest ancestor wins)\n const closestLang = elt.closest?.('[lang]')?.getAttribute('lang');\n if (closestLang) return normalizeLangCode(closestLang);\n\n // 4. Document-level lang (detached elements only — see doc comment)\n const htmlLang = typeof document !== 'undefined' ? document.documentElement?.lang : null;\n if (htmlLang && htmlLang !== 'en') return normalizeLangCode(htmlLang);\n\n return null;\n}\n\n/**\n * Normalize a BCP-47 language tag to an ISO 639-1 code.\n * e.g., \"ja-JP\" → \"ja\", \"zh-Hans\" → \"zh\", \"pt-BR\" → \"pt\"\n */\nfunction normalizeLangCode(lang: string): string {\n return lang.split('-')[0].toLowerCase();\n}\n","/**\n * Preprocessor (full path)\n *\n * Translates non-English hyperscript to English using the semantic parser\n * (with optional i18n fallback), so the original _hyperscript can parse it.\n *\n * The strategy/split/strip skeleton lives in preprocessor-core.ts, shared\n * with the slim path — only HOW one statement is parsed and rendered\n * differs here: semantic's parseSemantic + render('en'), plus a\n * translate() rescue when the parse is confident but yields no node.\n */\n\nimport { translate, render, parseSemantic, isLanguageRegistered } from '@lokascript/semantic';\n\nimport { createPreprocessToEnglish, type PreprocessorConfig } from './preprocessor-core';\n\nexport type { PreprocessorConfig };\n\n/**\n * Preprocess non-English hyperscript into English.\n *\n * Uses the semantic parser to parse the input in the source language,\n * then renders back to English. Falls through to i18n if configured.\n */\nexport const preprocessToEnglish = createPreprocessToEnglish({\n isLanguageRegistered,\n translateSingle(src, lang, threshold) {\n const result = parseSemantic(src, lang);\n if (result.confidence < threshold || !result.node) {\n // If we got SOME confidence but no node, fall back to translate()\n if (result.confidence >= threshold) {\n return translate(src, lang, 'en');\n }\n return null;\n }\n\n // Render the semantic node to English\n return render(result.node, 'en');\n },\n});\n","/**\n * Preprocessor core — the shared skeleton of the full and slim\n * preprocessors.\n *\n * Everything that is NOT genuinely different between the two paths lives\n * here exactly once: config defaults/threshold resolution, strategy\n * ordering (semantic → optional i18n), event-prefix stripping, and the\n * return-original fallback. The two real differences stay with each path,\n * injected as hooks:\n *\n * - HOW one statement is parsed and rendered to English\n * (`translateSingle`): the full path uses semantic's parseSemantic +\n * render + a translate() rescue for confident-but-nodeless parses;\n * the slim path uses parseWithConfidence + the custom\n * hyperscript-renderer (no English language data).\n * - WHERE the registry lookup comes from (`isLanguageRegistered`):\n * `@lokascript/semantic` vs `…/core` — under tsup's split dist these\n * are separate registry instances, so each path must bring its own.\n *\n * Behavior is pinned by the parity ratchet\n * (test/preprocessor-parity.*.test.ts + the committed fixture): both paths\n * are byte-identical to their pre-extraction outputs over the corpus.\n *\n * HISTORY: the skeleton used to carry a compound-splitting fallback\n * (`splitStatements` on localized `then` keywords + newlines,\n * `translateCompound` rejoining pieces with a hardcoded ` then `), run when\n * the whole-string parse declined. Deleted 2026-08-07 after measurement:\n * over all 3703 patterns.db corpus translations plus the parity corpus, on\n * BOTH paths, the split arm produced ZERO final outputs (full: 115\n * invocations, all returned null; slim: 32, all null) — post-whole-string-\n * first (#899), an input whose pieces each parse would have parsed whole,\n * so the arm's success condition is self-defeating. It was also defective\n * twice over: the regex split was string-literal-blind (`say 'now then\n * later'` split inside the literal) and the ` then ` rejoin is invalid\n * immediately after block headers. Deleting it erased both defect classes\n * wholesale; the parity fixture regenerated byte-identical.\n */\n\nexport interface PreprocessorConfig {\n /**\n * Minimum confidence threshold for semantic parsing (0-1). Default: 0.5.\n * Can be a single number (applies to all languages) or a per-language map.\n * Per-language thresholds are useful because SOV languages (ja, ko, tr) produce\n * inherently lower confidence scores than SVO languages (es, fr, de).\n *\n * @example\n * // Single threshold\n * { confidenceThreshold: 0.5 }\n *\n * @example\n * // Per-language thresholds\n * { confidenceThreshold: { es: 0.7, ja: 0.1, ko: 0.05, '*': 0.5 } }\n */\n confidenceThreshold: number | Record<string, number>;\n /** Strategy: 'semantic' (default), 'i18n', or 'auto' (semantic then i18n) */\n strategy: 'semantic' | 'i18n' | 'auto';\n /**\n * @deprecated Never implemented — `preprocessToEnglish` always returns a\n * string, and on translation failure that string is the original source\n * (there is nothing else it could return). The option has had no effect in\n * any released version and is ignored; it will be removed in a future major.\n */\n fallbackToOriginal?: boolean;\n /** Optional i18n toEnglish function (loaded dynamically if available) */\n i18nToEnglish?: (input: string, locale: string) => string;\n}\n\n/** The per-path seam — see module doc. */\nexport interface PreprocessorHooks {\n isLanguageRegistered(lang: string): boolean;\n /**\n * Parse ONE statement in `lang` and render it to English. Return null\n * when it cannot be translated (below-threshold confidence, no node, …);\n * the skeleton then tries the remaining strategies / falls back.\n */\n translateSingle(src: string, lang: string, threshold: number): string | null;\n}\n\nconst DEFAULT_THRESHOLD = 0.5;\n\nconst DEFAULT_CONFIG: PreprocessorConfig = {\n confidenceThreshold: DEFAULT_THRESHOLD,\n strategy: 'semantic',\n};\n\n/**\n * Resolve the confidence threshold for a specific language.\n * Supports both a single number and a per-language map with '*' as default.\n */\nfunction resolveThreshold(threshold: number | Record<string, number>, lang: string): number {\n if (typeof threshold === 'number') return threshold;\n return threshold[lang] ?? threshold['*'] ?? DEFAULT_THRESHOLD;\n}\n\n/**\n * Match _hyperscript event handler prefix: \"on [every] <event>[filter][.modifiers] \"\n *\n * Examples:\n * \"on click toggle .active\" → prefix: \"on click \", commands: \"toggle .active\"\n * \"on every click toggle .active\" → prefix: \"on every click \", commands: \"toggle .active\"\n * \"on click.debounce(300) toggle .x\" → prefix: \"on click.debounce(300) \", commands: \"toggle .x\"\n * \"on keyup[key=='Enter'] set x to 1\" → prefix: \"on keyup[key=='Enter'] \", commands: \"set x to 1\"\n * \"on click from body toggle .active\" → prefix: \"on click from body \", commands: \"toggle .active\"\n */\nconst EVENT_PREFIX_RE =\n /^(on\\s+(?:every\\s+)?[\\w-]+(?:\\[.*?\\])?(?:\\.[\\w-]+(?:\\([^)]*\\))?)*(?:\\s+from\\s+\\S+)?(?:\\s+queue\\s+\\w+)?\\s+)/;\n\nfunction stripEventPrefix(src: string): { prefix: string; commands: string } | null {\n const match = src.match(EVENT_PREFIX_RE);\n if (!match) return null;\n const prefix = match[1];\n const commands = src.slice(prefix.length);\n if (!commands) return null;\n return { prefix, commands };\n}\n\n/**\n * Build a `preprocessToEnglish` function from the per-path hooks.\n *\n * Handles _hyperscript feature prefixes (e.g. \"on click\", \"on every keyup\")\n * by stripping them, translating only the command portion, then reassembling.\n */\nexport function createPreprocessToEnglish(\n hooks: PreprocessorHooks\n): (src: string, lang: string, config?: Partial<PreprocessorConfig>) => string {\n /**\n * Try semantic translation: parse the WHOLE string in the source\n * language, render it to English. Returns null if confidence is below\n * threshold. The semantic parser handles `then`-sequences, newlines,\n * loop/tell bodies and behavior blocks natively (top-level\n * command-sequence support landed with the multiset-recall arc), so\n * there is deliberately no pre-splitting here — see the module doc's\n * HISTORY note for the measured deletion of the old split fallback.\n */\n function trySemanticTranslation(src: string, lang: string, threshold: number): string | null {\n try {\n // Check if the semantic parser can handle this\n if (!hooks.isLanguageRegistered(lang)) return null;\n\n return hooks.translateSingle(src, lang, threshold);\n } catch {\n return null;\n }\n }\n\n /**\n * Try i18n grammar transformation to English.\n * Returns null if the result is identical to input (no translation happened).\n */\n function tryI18nTranslation(\n src: string,\n lang: string,\n toEnglish: (input: string, locale: string) => string\n ): string | null {\n try {\n const result = toEnglish(src, lang);\n return result !== src ? result : null;\n } catch {\n return null;\n }\n }\n\n /**\n * Try all configured translation strategies on the input.\n * Returns null if none succeed.\n */\n function tryTranslateWithStrategies(\n src: string,\n lang: string,\n cfg: PreprocessorConfig\n ): string | null {\n // Strategy: semantic-only\n if (cfg.strategy === 'semantic' || cfg.strategy === 'auto') {\n const threshold = resolveThreshold(cfg.confidenceThreshold, lang);\n const result = trySemanticTranslation(src, lang, threshold);\n if (result !== null) return result;\n }\n\n // Strategy: i18n fallback (auto mode or i18n-only)\n if ((cfg.strategy === 'auto' || cfg.strategy === 'i18n') && cfg.i18nToEnglish) {\n const result = tryI18nTranslation(src, lang, cfg.i18nToEnglish);\n if (result !== null) return result;\n }\n\n return null;\n }\n\n return function preprocessToEnglish(\n src: string,\n lang: string,\n config: Partial<PreprocessorConfig> = {}\n ): string {\n // English→English is identity; skip semantic parsing which may mangle\n // the input. (Historically full-path only — the slim path gained it in\n // the shared-skeleton extraction, deliberately: it is a mangle guard.)\n if (lang === 'en') return src;\n\n const cfg = { ...DEFAULT_CONFIG, ...config };\n\n // Try translating the full string first\n const fullResult = tryTranslateWithStrategies(src, lang, cfg);\n if (fullResult !== null) return fullResult;\n\n // If full translation failed, try stripping event/feature prefix.\n // _hyperscript attributes often contain \"on <event> <commands>\" — the semantic\n // parser only understands command syntax, not event declarations.\n const stripped = stripEventPrefix(src);\n if (stripped) {\n const translated = tryTranslateWithStrategies(stripped.commands, lang, cfg);\n if (translated !== null) return stripped.prefix + translated;\n }\n\n // Fallback: return original (unconditional — see fallbackToOriginal's\n // deprecation note; the string contract leaves nothing else to return)\n return src;\n };\n}\n","/**\n * Attribute translator\n *\n * _hyperscript.org's Runtime#getScript is a private class field (`#getScript`),\n * not a property reachable through `internals.runtime` — every plugin variant\n * in this package used to monkey-patch `internals.runtime.getScript`, which\n * silently no-ops against current _hyperscript.org builds (the assignment\n * creates a stray own-property that the runtime's internal `#getScript()`\n * calls never read, so translation never happens and errors are swallowed by\n * `runtime.getScript.bind` throwing before any hyperscript even parses).\n *\n * `addBeforeProcessHook` is the supported, public extension point instead: it\n * fires on the subtree root passed to `processNode()` before the runtime reads\n * whichever configured attribute (`_`, `script`, `data-script` by default) or\n * `<script type=\"text/hyperscript\">` body holds the source. Rewriting that\n * attribute/body in place — before the runtime's own scan reaches it — gets\n * the same \"translate before parse\" effect through a mechanism the runtime\n * actually calls.\n *\n * A host may offer `addSourceTransform` instead (`@hyperfixi/engine`\n * does; _hyperscript.org does not). The host then\n * hands each script to the translator as it reads it, and the attribute keeps\n * the author's text: nothing in the DOM is rewritten, and a parse error can\n * say what was written.\n */\n\nexport interface HyperscriptHost {\n addBeforeProcessHook?: (fn: (elt: Element) => void) => void;\n /** Rewrite a script as the host reads it; the element is left as written. */\n addSourceTransform?: (fn: (src: string, elt: Element) => string | null) => void;\n config?: { attributes?: string };\n}\n\n/** Elements already processed, so a later `processNode()` call over the same\n * subtree (e.g. a sibling swap re-scanning a shared ancestor) doesn't\n * re-translate already-English text as if it were still the original language.\n * A WeakSet instead of a marker attribute: the same idempotency with zero DOM\n * mutation (devtools/serialization show exactly what the author wrote).\n * Serialize→reparse (e.g. an innerHTML round-trip) produces NEW elements that\n * are re-processed — safe, because re-translating already-English text is\n * confidence-gated into a no-op. */\nconst processed = new WeakSet<Element>();\n\nfunction scriptAttributeNames(hs: HyperscriptHost): string[] {\n const raw = hs.config?.attributes ?? '_, script, data-script';\n return raw\n .split(',')\n .map(s => s.trim())\n .filter(Boolean);\n}\n\nfunction findScriptAttribute(elt: Element, attrNames: string[]): string | null {\n for (const name of attrNames) {\n if (elt.hasAttribute(name)) return name;\n }\n return null;\n}\n\n/**\n * Install a translator that turns non-English scripts into English before the\n * host parses them: through the host's source transform when it has one,\n * otherwise by rewriting the script attributes in place.\n *\n * @param translate Given the raw source and its element, return the English\n * translation, or `null`/the same string to leave the element untouched\n * (English input, unresolved language, translation failure, etc).\n */\nexport function installAttributeTranslator(\n hs: HyperscriptHost,\n translate: (src: string, elt: Element) => string | null\n): void {\n if (typeof hs.addSourceTransform === 'function') {\n // The host asks only for an element it is about to initialise, so the\n // processed-set below is not needed on this path.\n hs.addSourceTransform(translate);\n return;\n }\n\n if (typeof hs.addBeforeProcessHook !== 'function') {\n console.warn(\n '[hyperscript-i18n] _hyperscript.addBeforeProcessHook is unavailable — ' +\n 'this build of _hyperscript.org is not supported. Load a current version ' +\n 'from https://unpkg.com/hyperscript.org.'\n );\n return;\n }\n\n const attrNames = scriptAttributeNames(hs);\n const selector = [...attrNames.map(a => `[${a}]`), 'script[type=\"text/hyperscript\"]'].join(', ');\n\n const translateOne = (elt: Element): void => {\n if (processed.has(elt)) return;\n\n if (elt instanceof HTMLScriptElement && elt.type === 'text/hyperscript') {\n const src = elt.textContent ?? '';\n if (!src) return;\n const english = translate(src, elt);\n processed.add(elt);\n if (english != null && english !== src) elt.textContent = english;\n return;\n }\n\n const attr = findScriptAttribute(elt, attrNames);\n if (!attr) return;\n const src = elt.getAttribute(attr);\n if (!src) return;\n const english = translate(src, elt);\n processed.add(elt);\n if (english != null && english !== src) elt.setAttribute(attr, english);\n };\n\n hs.addBeforeProcessHook((root: Element) => {\n if (!root || typeof root.querySelectorAll !== 'function') return;\n if (typeof root.matches === 'function' && root.matches(selector)) translateOne(root);\n root.querySelectorAll(selector).forEach(translateOne);\n });\n}\n","/**\n * Host-parser validity gate (review item F8).\n *\n * After the preprocessor rewrites an attribute to English, the plugin asks\n * the HOST _hyperscript runtime — the same parser that will consume the\n * rewrite moments later — whether the result actually parses. On rejection\n * the plugin falls back to the author's original text, so any parse error\n * the author then sees names code they wrote, not invisible generated\n * English. This is the runtime analog of the offline R4 canonical-validity\n * gate, and the F5 arc measured its failure class shipping in practice:\n * until the whole-string-first reorder (#899), 256 corpus rows rendered\n * English the engine rejects, with no warning anywhere.\n *\n * The engine has two failure channels — `parse().errors` collects grammar\n * errors, and the tokenizer THROWS on an unknown character — folded here\n * the same way test/whole-string-first.test.ts folds them.\n *\n * Zero-dependency module: shared by the full, slim, and lite plugin\n * variants, which must not share heavier import chains (the slim/lite\n * bundles exclude the full semantic package by construction).\n */\n\n/** The part of a host parse result the gate reads: grammar errors, and each handler's events. */\nexport interface HostParseResult {\n errors?: unknown[];\n features?: Array<{ events?: Array<{ on?: unknown }> }>;\n}\n\nexport interface HyperscriptParseHost {\n parse?: (src: string) => HostParseResult | null | undefined;\n}\n\n/**\n * Names that are references, never events. A translation that reads one as a\n * handler's event parses cleanly and does nothing: the README's Japanese\n * example `on click .active を me で 切り替え` became `on click on me toggle\n * .active`, which the engine reads as an empty click handler plus a handler for\n * an event named `me`, and the button stayed dead. The host accepts that, so\n * the gate asks for it by name.\n */\nconst REFERENCE_NAMES = new Set(['me', 'my', 'i', 'it', 'its', 'you', 'your']);\n\n/** The reference a parse result listens for as an event, if any. */\nexport function referenceReadAsEvent(\n result: HostParseResult | null | undefined\n): string | undefined {\n for (const feature of result?.features ?? []) {\n for (const event of feature.events ?? []) {\n if (typeof event.on === 'string' && REFERENCE_NAMES.has(event.on.toLowerCase()))\n return event.on;\n }\n }\n return undefined;\n}\n\n/**\n * True when the host's parser accepts `src` and reads no reference as a\n * handler's event (see REFERENCE_NAMES). Also true when the host exposes no\n * `parse()` — with nothing to validate against, the gate degrades to a no-op\n * rather than suppressing translation on unusual builds (same graceful\n * posture as the `addBeforeProcessHook` check).\n */\nexport function acceptedByHost(hs: HyperscriptParseHost, src: string): boolean {\n if (typeof hs.parse !== 'function') return true;\n try {\n const result = hs.parse(src);\n if (result?.errors && result.errors.length > 0) return false;\n return referenceReadAsEvent(result) === undefined;\n } catch {\n return false;\n }\n}\n\n/** Languages already warned about a rejected translation this page load —\n * same warn-once-per-lang convention as the unchanged-translation warning\n * (and htmx-adapter's warnMissingLangOnce). */\nconst warnedRejectedLang = new Set<string>();\n\n/** Reset the warn-once state. Mainly for tests. */\nexport function resetHostValidationWarnings(): void {\n warnedRejectedLang.clear();\n}\n\nexport function warnRejectedOnce(lang: string, src: string, english: string): void {\n if (warnedRejectedLang.has(lang)) return;\n warnedRejectedLang.add(lang);\n console.warn(\n `[hyperscript-i18n] Translation for lang=\"${lang}\" rendered hyperscript the host parser ` +\n `rejects (or reads as a handler for an event named like me/it/you) — falling back to the ` +\n `original text. ` +\n `Source: \"${src.length > 60 ? src.slice(0, 60) + '…' : src}\" → ` +\n `\"${english.length > 60 ? english.slice(0, 60) + '…' : english}\". ` +\n 'Further elements in this language stay quiet — enable { debug: true } for per-element detail.'\n );\n}\n","/**\n * _hyperscript Plugin\n *\n * Registers with _hyperscript.use() to rewrite non-English hyperscript\n * attributes into English before _hyperscript.org parses them.\n */\n\nimport { resolveLanguage } from './language-resolver';\nimport { preprocessToEnglish, type PreprocessorConfig } from './preprocessor';\nimport { installAttributeTranslator, type HyperscriptHost } from './attribute-translator';\nimport {\n acceptedByHost,\n warnRejectedOnce,\n resetHostValidationWarnings,\n type HyperscriptParseHost,\n} from './host-validate';\n\nexport interface PluginOptions extends Partial<PreprocessorConfig> {\n /** Default language for all elements (overridable per-element). */\n defaultLanguage?: string;\n /** Custom attribute name for per-element language. Default: \"data-lang\" */\n languageAttribute?: string;\n /** Enable debug logging to console. Default: false */\n debug?: boolean;\n /**\n * Validate rendered English on the HOST parser before committing the\n * rewrite; on rejection, fall back to the original text (so parse errors\n * name the author's code, not generated English). Default: true.\n * No-op on host builds that expose no `parse()`.\n */\n validateWithHost?: boolean;\n}\n\n/** Languages already warned about an unchanged translation this page load.\n * Unchanged output is common and often legitimate (canonical-English\n * hyperscript under a non-en lang scope), so warning per element per\n * processNode was pure noise — mirror htmx-adapter's warn-once-per-lang\n * convention and leave per-element detail to `debug: true`. */\nconst warnedUnchangedLang = new Set<string>();\n\n/** Reset the warn-once state (unchanged + host-rejected). Mainly for tests. */\nexport function resetTranslationWarnings(): void {\n warnedUnchangedLang.clear();\n resetHostValidationWarnings();\n}\n\nfunction warnUnchangedOnce(lang: string, src: string): void {\n if (warnedUnchangedLang.has(lang)) return;\n warnedUnchangedLang.add(lang);\n console.warn(\n `[hyperscript-i18n] Translation unchanged for lang=\"${lang}\": \"${src.length > 60 ? src.slice(0, 60) + '…' : src}\". ` +\n 'This is fine if the source is already canonical English; otherwise the input may not match ' +\n 'any known pattern, or the language may not be registered. Original text is passed to ' +\n '_hyperscript as-is. Further elements in this language stay quiet — enable { debug: true } ' +\n 'for per-element detail.'\n );\n}\n\n/**\n * Create a _hyperscript plugin that enables multilingual hyperscript.\n *\n * @example\n * // Basic usage\n * _hyperscript.use(hyperscriptI18n());\n *\n * @example\n * // With options\n * _hyperscript.use(hyperscriptI18n({\n * defaultLanguage: 'ja',\n * confidenceThreshold: 0.6,\n * debug: true,\n * }));\n */\nexport function hyperscriptI18n(options: PluginOptions = {}) {\n return function plugin(hs: unknown): void {\n const host = hs as HyperscriptHost & HyperscriptParseHost;\n installAttributeTranslator(host, (src, elt) => {\n // Resolve language\n const lang = resolveLanguageWithOptions(elt, options);\n\n // If English or no language detected, pass through\n if (!lang || lang === 'en') return src;\n\n // Preprocess to English\n const english = preprocessToEnglish(src, lang, options);\n\n if (english !== src) {\n // Validity gate: the host parser is the consumer of this rewrite —\n // if it rejects the English, committing it would only trade a\n // translation gap for a parse error naming code the author never\n // wrote. Fall back to the original text instead.\n if (options.validateWithHost !== false && !acceptedByHost(host, english)) {\n if (options.debug) {\n console.log(\n `[hyperscript-i18n] ${lang}: host rejected \"${english}\" — keeping \"${src}\"`\n );\n } else {\n warnRejectedOnce(lang, src, english);\n }\n return src;\n }\n if (options.debug) {\n console.log(`[hyperscript-i18n] ${lang}: \"${src}\" → \"${english}\"`);\n }\n } else if (options.debug) {\n console.log(`[hyperscript-i18n] ${lang}: unchanged \"${src}\"`);\n } else {\n warnUnchangedOnce(lang, src);\n }\n\n return english;\n });\n };\n}\n\n/**\n * Resolve language with plugin options factored in.\n */\nfunction resolveLanguageWithOptions(elt: Element, options: PluginOptions): string | null {\n // Check custom attribute name\n if (options.languageAttribute) {\n const custom = elt.getAttribute(options.languageAttribute);\n if (custom) return custom.split('-')[0].toLowerCase();\n }\n\n // Standard resolution\n const resolved = resolveLanguage(elt);\n if (resolved) return resolved;\n\n // Fall back to default language\n return options.defaultLanguage ?? null;\n}\n\n/**\n * Standalone preprocessing function for programmatic use.\n * Call this when using _hyperscript.evaluate() or _hyperscript(\"code\") directly.\n *\n * @example\n * const english = preprocess(\"トグル .active\", \"ja\");\n * _hyperscript(english);\n */\nexport function preprocess(\n src: string,\n lang: string,\n config: Partial<PreprocessorConfig> = {}\n): string {\n if (lang === 'en') return src;\n return preprocessToEnglish(src, lang, config);\n}\n"],"mappings":"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","names":["preprocessToEnglish","translate","src","english"]}
package/dist/index.js CHANGED
@@ -107,6 +107,10 @@ function findScriptAttribute(elt, attrNames) {
107
107
  return null;
108
108
  }
109
109
  function installAttributeTranslator(hs, translate2) {
110
+ if (typeof hs.addSourceTransform === "function") {
111
+ hs.addSourceTransform(translate2);
112
+ return;
113
+ }
110
114
  if (typeof hs.addBeforeProcessHook !== "function") {
111
115
  console.warn(
112
116
  "[hyperscript-i18n] _hyperscript.addBeforeProcessHook is unavailable \u2014 this build of _hyperscript.org is not supported. Load a current version from https://unpkg.com/hyperscript.org."
@@ -141,11 +145,22 @@ function installAttributeTranslator(hs, translate2) {
141
145
  }
142
146
 
143
147
  // src/host-validate.ts
148
+ var REFERENCE_NAMES = /* @__PURE__ */ new Set(["me", "my", "i", "it", "its", "you", "your"]);
149
+ function referenceReadAsEvent(result) {
150
+ for (const feature of result?.features ?? []) {
151
+ for (const event of feature.events ?? []) {
152
+ if (typeof event.on === "string" && REFERENCE_NAMES.has(event.on.toLowerCase()))
153
+ return event.on;
154
+ }
155
+ }
156
+ return void 0;
157
+ }
144
158
  function acceptedByHost(hs, src) {
145
159
  if (typeof hs.parse !== "function") return true;
146
160
  try {
147
161
  const result = hs.parse(src);
148
- return !result?.errors || result.errors.length === 0;
162
+ if (result?.errors && result.errors.length > 0) return false;
163
+ return referenceReadAsEvent(result) === void 0;
149
164
  } catch {
150
165
  return false;
151
166
  }
@@ -158,7 +173,7 @@ function warnRejectedOnce(lang, src, english) {
158
173
  if (warnedRejectedLang.has(lang)) return;
159
174
  warnedRejectedLang.add(lang);
160
175
  console.warn(
161
- `[hyperscript-i18n] Translation for lang="${lang}" rendered hyperscript the host parser rejects \u2014 falling back to the original text. Source: "${src.length > 60 ? src.slice(0, 60) + "\u2026" : src}" \u2192 "${english.length > 60 ? english.slice(0, 60) + "\u2026" : english}". Further elements in this language stay quiet \u2014 enable { debug: true } for per-element detail.`
176
+ `[hyperscript-i18n] Translation for lang="${lang}" rendered hyperscript the host parser rejects (or reads as a handler for an event named like me/it/you) \u2014 falling back to the original text. Source: "${src.length > 60 ? src.slice(0, 60) + "\u2026" : src}" \u2192 "${english.length > 60 ? english.slice(0, 60) + "\u2026" : english}". Further elements in this language stay quiet \u2014 enable { debug: true } for per-element detail.`
162
177
  );
163
178
  }
164
179
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/language-resolver.ts","../src/preprocessor.ts","../src/preprocessor-core.ts","../src/attribute-translator.ts","../src/host-validate.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Language Resolver\n *\n * Determines the language of hyperscript code on a given element\n * using a cascading resolution strategy.\n */\n\n/**\n * Resolve the language for a hyperscript attribute on an element.\n *\n * Resolution order:\n * 1. `data-lang` attribute on the element itself\n * 2. `data-hyperscript-lang` attribute on the element or closest ancestor\n * 3. `lang` attribute on the element or closest ancestor (the HTML-standard\n * cascade — a `<section lang=\"es\">` localizes everything inside it, and a\n * nested `lang=\"en\"` opts back out). This is how the paired htmx-adapter\n * (`langOf()`) and loka-js resolve language, so `hx-*` and `_` attributes\n * on the same element agree.\n * 4. `lang` on `<html>` via `document.documentElement` — only reachable when\n * the element is DETACHED (step 3's ancestor walk covers `<html>` for\n * attached elements): a hook processing a not-yet-inserted fragment still\n * picks up the page default.\n * 5. null (assume English, no preprocessing needed)\n */\nexport function resolveLanguage(elt: Element): string | null {\n // 1. Explicit per-element\n const dataLang = elt.getAttribute('data-lang');\n if (dataLang) return normalizeLangCode(dataLang);\n\n // 2. Inherited from ancestor\n const hsLang =\n elt.getAttribute('data-hyperscript-lang') ??\n elt.closest?.('[data-hyperscript-lang]')?.getAttribute('data-hyperscript-lang');\n if (hsLang) return normalizeLangCode(hsLang);\n\n // 3. Standard lang cascade (nearest ancestor wins)\n const closestLang = elt.closest?.('[lang]')?.getAttribute('lang');\n if (closestLang) return normalizeLangCode(closestLang);\n\n // 4. Document-level lang (detached elements only — see doc comment)\n const htmlLang = typeof document !== 'undefined' ? document.documentElement?.lang : null;\n if (htmlLang && htmlLang !== 'en') return normalizeLangCode(htmlLang);\n\n return null;\n}\n\n/**\n * Normalize a BCP-47 language tag to an ISO 639-1 code.\n * e.g., \"ja-JP\" → \"ja\", \"zh-Hans\" → \"zh\", \"pt-BR\" → \"pt\"\n */\nfunction normalizeLangCode(lang: string): string {\n return lang.split('-')[0].toLowerCase();\n}\n","/**\n * Preprocessor (full path)\n *\n * Translates non-English hyperscript to English using the semantic parser\n * (with optional i18n fallback), so the original _hyperscript can parse it.\n *\n * The strategy/split/strip skeleton lives in preprocessor-core.ts, shared\n * with the slim path — only HOW one statement is parsed and rendered\n * differs here: semantic's parseSemantic + render('en'), plus a\n * translate() rescue when the parse is confident but yields no node.\n */\n\nimport { translate, render, parseSemantic, isLanguageRegistered } from '@lokascript/semantic';\n\nimport { createPreprocessToEnglish, type PreprocessorConfig } from './preprocessor-core';\n\nexport type { PreprocessorConfig };\n\n/**\n * Preprocess non-English hyperscript into English.\n *\n * Uses the semantic parser to parse the input in the source language,\n * then renders back to English. Falls through to i18n if configured.\n */\nexport const preprocessToEnglish = createPreprocessToEnglish({\n isLanguageRegistered,\n translateSingle(src, lang, threshold) {\n const result = parseSemantic(src, lang);\n if (result.confidence < threshold || !result.node) {\n // If we got SOME confidence but no node, fall back to translate()\n if (result.confidence >= threshold) {\n return translate(src, lang, 'en');\n }\n return null;\n }\n\n // Render the semantic node to English\n return render(result.node, 'en');\n },\n});\n","/**\n * Preprocessor core — the shared skeleton of the full and slim\n * preprocessors.\n *\n * Everything that is NOT genuinely different between the two paths lives\n * here exactly once: config defaults/threshold resolution, strategy\n * ordering (semantic → optional i18n), event-prefix stripping, and the\n * return-original fallback. The two real differences stay with each path,\n * injected as hooks:\n *\n * - HOW one statement is parsed and rendered to English\n * (`translateSingle`): the full path uses semantic's parseSemantic +\n * render + a translate() rescue for confident-but-nodeless parses;\n * the slim path uses parseWithConfidence + the custom\n * hyperscript-renderer (no English language data).\n * - WHERE the registry lookup comes from (`isLanguageRegistered`):\n * `@lokascript/semantic` vs `…/core` — under tsup's split dist these\n * are separate registry instances, so each path must bring its own.\n *\n * Behavior is pinned by the parity ratchet\n * (test/preprocessor-parity.*.test.ts + the committed fixture): both paths\n * are byte-identical to their pre-extraction outputs over the corpus.\n *\n * HISTORY: the skeleton used to carry a compound-splitting fallback\n * (`splitStatements` on localized `then` keywords + newlines,\n * `translateCompound` rejoining pieces with a hardcoded ` then `), run when\n * the whole-string parse declined. Deleted 2026-08-07 after measurement:\n * over all 3703 patterns.db corpus translations plus the parity corpus, on\n * BOTH paths, the split arm produced ZERO final outputs (full: 115\n * invocations, all returned null; slim: 32, all null) — post-whole-string-\n * first (#899), an input whose pieces each parse would have parsed whole,\n * so the arm's success condition is self-defeating. It was also defective\n * twice over: the regex split was string-literal-blind (`say 'now then\n * later'` split inside the literal) and the ` then ` rejoin is invalid\n * immediately after block headers. Deleting it erased both defect classes\n * wholesale; the parity fixture regenerated byte-identical.\n */\n\nexport interface PreprocessorConfig {\n /**\n * Minimum confidence threshold for semantic parsing (0-1). Default: 0.5.\n * Can be a single number (applies to all languages) or a per-language map.\n * Per-language thresholds are useful because SOV languages (ja, ko, tr) produce\n * inherently lower confidence scores than SVO languages (es, fr, de).\n *\n * @example\n * // Single threshold\n * { confidenceThreshold: 0.5 }\n *\n * @example\n * // Per-language thresholds\n * { confidenceThreshold: { es: 0.7, ja: 0.1, ko: 0.05, '*': 0.5 } }\n */\n confidenceThreshold: number | Record<string, number>;\n /** Strategy: 'semantic' (default), 'i18n', or 'auto' (semantic then i18n) */\n strategy: 'semantic' | 'i18n' | 'auto';\n /**\n * @deprecated Never implemented — `preprocessToEnglish` always returns a\n * string, and on translation failure that string is the original source\n * (there is nothing else it could return). The option has had no effect in\n * any released version and is ignored; it will be removed in a future major.\n */\n fallbackToOriginal?: boolean;\n /** Optional i18n toEnglish function (loaded dynamically if available) */\n i18nToEnglish?: (input: string, locale: string) => string;\n}\n\n/** The per-path seam — see module doc. */\nexport interface PreprocessorHooks {\n isLanguageRegistered(lang: string): boolean;\n /**\n * Parse ONE statement in `lang` and render it to English. Return null\n * when it cannot be translated (below-threshold confidence, no node, …);\n * the skeleton then tries the remaining strategies / falls back.\n */\n translateSingle(src: string, lang: string, threshold: number): string | null;\n}\n\nconst DEFAULT_THRESHOLD = 0.5;\n\nconst DEFAULT_CONFIG: PreprocessorConfig = {\n confidenceThreshold: DEFAULT_THRESHOLD,\n strategy: 'semantic',\n};\n\n/**\n * Resolve the confidence threshold for a specific language.\n * Supports both a single number and a per-language map with '*' as default.\n */\nfunction resolveThreshold(threshold: number | Record<string, number>, lang: string): number {\n if (typeof threshold === 'number') return threshold;\n return threshold[lang] ?? threshold['*'] ?? DEFAULT_THRESHOLD;\n}\n\n/**\n * Match _hyperscript event handler prefix: \"on [every] <event>[filter][.modifiers] \"\n *\n * Examples:\n * \"on click toggle .active\" → prefix: \"on click \", commands: \"toggle .active\"\n * \"on every click toggle .active\" → prefix: \"on every click \", commands: \"toggle .active\"\n * \"on click.debounce(300) toggle .x\" → prefix: \"on click.debounce(300) \", commands: \"toggle .x\"\n * \"on keyup[key=='Enter'] set x to 1\" → prefix: \"on keyup[key=='Enter'] \", commands: \"set x to 1\"\n * \"on click from body toggle .active\" → prefix: \"on click from body \", commands: \"toggle .active\"\n */\nconst EVENT_PREFIX_RE =\n /^(on\\s+(?:every\\s+)?[\\w-]+(?:\\[.*?\\])?(?:\\.[\\w-]+(?:\\([^)]*\\))?)*(?:\\s+from\\s+\\S+)?(?:\\s+queue\\s+\\w+)?\\s+)/;\n\nfunction stripEventPrefix(src: string): { prefix: string; commands: string } | null {\n const match = src.match(EVENT_PREFIX_RE);\n if (!match) return null;\n const prefix = match[1];\n const commands = src.slice(prefix.length);\n if (!commands) return null;\n return { prefix, commands };\n}\n\n/**\n * Build a `preprocessToEnglish` function from the per-path hooks.\n *\n * Handles _hyperscript feature prefixes (e.g. \"on click\", \"on every keyup\")\n * by stripping them, translating only the command portion, then reassembling.\n */\nexport function createPreprocessToEnglish(\n hooks: PreprocessorHooks\n): (src: string, lang: string, config?: Partial<PreprocessorConfig>) => string {\n /**\n * Try semantic translation: parse the WHOLE string in the source\n * language, render it to English. Returns null if confidence is below\n * threshold. The semantic parser handles `then`-sequences, newlines,\n * loop/tell bodies and behavior blocks natively (top-level\n * command-sequence support landed with the multiset-recall arc), so\n * there is deliberately no pre-splitting here — see the module doc's\n * HISTORY note for the measured deletion of the old split fallback.\n */\n function trySemanticTranslation(src: string, lang: string, threshold: number): string | null {\n try {\n // Check if the semantic parser can handle this\n if (!hooks.isLanguageRegistered(lang)) return null;\n\n return hooks.translateSingle(src, lang, threshold);\n } catch {\n return null;\n }\n }\n\n /**\n * Try i18n grammar transformation to English.\n * Returns null if the result is identical to input (no translation happened).\n */\n function tryI18nTranslation(\n src: string,\n lang: string,\n toEnglish: (input: string, locale: string) => string\n ): string | null {\n try {\n const result = toEnglish(src, lang);\n return result !== src ? result : null;\n } catch {\n return null;\n }\n }\n\n /**\n * Try all configured translation strategies on the input.\n * Returns null if none succeed.\n */\n function tryTranslateWithStrategies(\n src: string,\n lang: string,\n cfg: PreprocessorConfig\n ): string | null {\n // Strategy: semantic-only\n if (cfg.strategy === 'semantic' || cfg.strategy === 'auto') {\n const threshold = resolveThreshold(cfg.confidenceThreshold, lang);\n const result = trySemanticTranslation(src, lang, threshold);\n if (result !== null) return result;\n }\n\n // Strategy: i18n fallback (auto mode or i18n-only)\n if ((cfg.strategy === 'auto' || cfg.strategy === 'i18n') && cfg.i18nToEnglish) {\n const result = tryI18nTranslation(src, lang, cfg.i18nToEnglish);\n if (result !== null) return result;\n }\n\n return null;\n }\n\n return function preprocessToEnglish(\n src: string,\n lang: string,\n config: Partial<PreprocessorConfig> = {}\n ): string {\n // English→English is identity; skip semantic parsing which may mangle\n // the input. (Historically full-path only — the slim path gained it in\n // the shared-skeleton extraction, deliberately: it is a mangle guard.)\n if (lang === 'en') return src;\n\n const cfg = { ...DEFAULT_CONFIG, ...config };\n\n // Try translating the full string first\n const fullResult = tryTranslateWithStrategies(src, lang, cfg);\n if (fullResult !== null) return fullResult;\n\n // If full translation failed, try stripping event/feature prefix.\n // _hyperscript attributes often contain \"on <event> <commands>\" — the semantic\n // parser only understands command syntax, not event declarations.\n const stripped = stripEventPrefix(src);\n if (stripped) {\n const translated = tryTranslateWithStrategies(stripped.commands, lang, cfg);\n if (translated !== null) return stripped.prefix + translated;\n }\n\n // Fallback: return original (unconditional — see fallbackToOriginal's\n // deprecation note; the string contract leaves nothing else to return)\n return src;\n };\n}\n","/**\n * Attribute translator\n *\n * _hyperscript.org's Runtime#getScript is a private class field (`#getScript`),\n * not a property reachable through `internals.runtime` — every plugin variant\n * in this package used to monkey-patch `internals.runtime.getScript`, which\n * silently no-ops against current _hyperscript.org builds (the assignment\n * creates a stray own-property that the runtime's internal `#getScript()`\n * calls never read, so translation never happens and errors are swallowed by\n * `runtime.getScript.bind` throwing before any hyperscript even parses).\n *\n * `addBeforeProcessHook` is the supported, public extension point instead: it\n * fires on the subtree root passed to `processNode()` before the runtime reads\n * whichever configured attribute (`_`, `script`, `data-script` by default) or\n * `<script type=\"text/hyperscript\">` body holds the source. Rewriting that\n * attribute/body in place — before the runtime's own scan reaches it — gets\n * the same \"translate before parse\" effect through a mechanism the runtime\n * actually calls.\n */\n\nexport interface HyperscriptHost {\n addBeforeProcessHook?: (fn: (elt: Element) => void) => void;\n config?: { attributes?: string };\n}\n\n/** Elements already processed, so a later `processNode()` call over the same\n * subtree (e.g. a sibling swap re-scanning a shared ancestor) doesn't\n * re-translate already-English text as if it were still the original language.\n * A WeakSet instead of a marker attribute: the same idempotency with zero DOM\n * mutation (devtools/serialization show exactly what the author wrote).\n * Serialize→reparse (e.g. an innerHTML round-trip) produces NEW elements that\n * are re-processed — safe, because re-translating already-English text is\n * confidence-gated into a no-op. */\nconst processed = new WeakSet<Element>();\n\nfunction scriptAttributeNames(hs: HyperscriptHost): string[] {\n const raw = hs.config?.attributes ?? '_, script, data-script';\n return raw\n .split(',')\n .map(s => s.trim())\n .filter(Boolean);\n}\n\nfunction findScriptAttribute(elt: Element, attrNames: string[]): string | null {\n for (const name of attrNames) {\n if (elt.hasAttribute(name)) return name;\n }\n return null;\n}\n\n/**\n * Install a translator that rewrites non-English script attributes to English\n * in place, before `_hyperscript.org` parses them.\n *\n * @param translate Given the raw source and its element, return the English\n * translation, or `null`/the same string to leave the element untouched\n * (English input, unresolved language, translation failure, etc).\n */\nexport function installAttributeTranslator(\n hs: HyperscriptHost,\n translate: (src: string, elt: Element) => string | null\n): void {\n if (typeof hs.addBeforeProcessHook !== 'function') {\n console.warn(\n '[hyperscript-i18n] _hyperscript.addBeforeProcessHook is unavailable — ' +\n 'this build of _hyperscript.org is not supported. Load a current version ' +\n 'from https://unpkg.com/hyperscript.org.'\n );\n return;\n }\n\n const attrNames = scriptAttributeNames(hs);\n const selector = [...attrNames.map(a => `[${a}]`), 'script[type=\"text/hyperscript\"]'].join(', ');\n\n const translateOne = (elt: Element): void => {\n if (processed.has(elt)) return;\n\n if (elt instanceof HTMLScriptElement && elt.type === 'text/hyperscript') {\n const src = elt.textContent ?? '';\n if (!src) return;\n const english = translate(src, elt);\n processed.add(elt);\n if (english != null && english !== src) elt.textContent = english;\n return;\n }\n\n const attr = findScriptAttribute(elt, attrNames);\n if (!attr) return;\n const src = elt.getAttribute(attr);\n if (!src) return;\n const english = translate(src, elt);\n processed.add(elt);\n if (english != null && english !== src) elt.setAttribute(attr, english);\n };\n\n hs.addBeforeProcessHook((root: Element) => {\n if (!root || typeof root.querySelectorAll !== 'function') return;\n if (typeof root.matches === 'function' && root.matches(selector)) translateOne(root);\n root.querySelectorAll(selector).forEach(translateOne);\n });\n}\n","/**\n * Host-parser validity gate (review item F8).\n *\n * After the preprocessor rewrites an attribute to English, the plugin asks\n * the HOST _hyperscript runtime — the same parser that will consume the\n * rewrite moments later — whether the result actually parses. On rejection\n * the plugin falls back to the author's original text, so any parse error\n * the author then sees names code they wrote, not invisible generated\n * English. This is the runtime analog of the offline R4 canonical-validity\n * gate, and the F5 arc measured its failure class shipping in practice:\n * until the whole-string-first reorder (#899), 256 corpus rows rendered\n * English the engine rejects, with no warning anywhere.\n *\n * The engine has two failure channels — `parse().errors` collects grammar\n * errors, and the tokenizer THROWS on an unknown character — folded here\n * the same way test/whole-string-first.test.ts folds them.\n *\n * Zero-dependency module: shared by the full, slim, and lite plugin\n * variants, which must not share heavier import chains (the slim/lite\n * bundles exclude the full semantic package by construction).\n */\n\nexport interface HyperscriptParseHost {\n parse?: (src: string) => { errors?: unknown[] } | null | undefined;\n}\n\n/**\n * True when the host's parser accepts `src`. Also true when the host\n * exposes no `parse()` — with nothing to validate against, the gate\n * degrades to a no-op rather than suppressing translation on unusual\n * builds (same graceful posture as the `addBeforeProcessHook` check).\n */\nexport function acceptedByHost(hs: HyperscriptParseHost, src: string): boolean {\n if (typeof hs.parse !== 'function') return true;\n try {\n const result = hs.parse(src);\n return !result?.errors || result.errors.length === 0;\n } catch {\n return false;\n }\n}\n\n/** Languages already warned about a rejected translation this page load —\n * same warn-once-per-lang convention as the unchanged-translation warning\n * (and htmx-adapter's warnMissingLangOnce). */\nconst warnedRejectedLang = new Set<string>();\n\n/** Reset the warn-once state. Mainly for tests. */\nexport function resetHostValidationWarnings(): void {\n warnedRejectedLang.clear();\n}\n\nexport function warnRejectedOnce(lang: string, src: string, english: string): void {\n if (warnedRejectedLang.has(lang)) return;\n warnedRejectedLang.add(lang);\n console.warn(\n `[hyperscript-i18n] Translation for lang=\"${lang}\" rendered hyperscript the host parser ` +\n `rejects — falling back to the original text. ` +\n `Source: \"${src.length > 60 ? src.slice(0, 60) + '…' : src}\" → ` +\n `\"${english.length > 60 ? english.slice(0, 60) + '…' : english}\". ` +\n 'Further elements in this language stay quiet — enable { debug: true } for per-element detail.'\n );\n}\n","/**\n * _hyperscript Plugin\n *\n * Registers with _hyperscript.use() to rewrite non-English hyperscript\n * attributes into English before _hyperscript.org parses them.\n */\n\nimport { resolveLanguage } from './language-resolver';\nimport { preprocessToEnglish, type PreprocessorConfig } from './preprocessor';\nimport { installAttributeTranslator, type HyperscriptHost } from './attribute-translator';\nimport {\n acceptedByHost,\n warnRejectedOnce,\n resetHostValidationWarnings,\n type HyperscriptParseHost,\n} from './host-validate';\n\nexport interface PluginOptions extends Partial<PreprocessorConfig> {\n /** Default language for all elements (overridable per-element). */\n defaultLanguage?: string;\n /** Custom attribute name for per-element language. Default: \"data-lang\" */\n languageAttribute?: string;\n /** Enable debug logging to console. Default: false */\n debug?: boolean;\n /**\n * Validate rendered English on the HOST parser before committing the\n * rewrite; on rejection, fall back to the original text (so parse errors\n * name the author's code, not generated English). Default: true.\n * No-op on host builds that expose no `parse()`.\n */\n validateWithHost?: boolean;\n}\n\n/** Languages already warned about an unchanged translation this page load.\n * Unchanged output is common and often legitimate (canonical-English\n * hyperscript under a non-en lang scope), so warning per element per\n * processNode was pure noise — mirror htmx-adapter's warn-once-per-lang\n * convention and leave per-element detail to `debug: true`. */\nconst warnedUnchangedLang = new Set<string>();\n\n/** Reset the warn-once state (unchanged + host-rejected). Mainly for tests. */\nexport function resetTranslationWarnings(): void {\n warnedUnchangedLang.clear();\n resetHostValidationWarnings();\n}\n\nfunction warnUnchangedOnce(lang: string, src: string): void {\n if (warnedUnchangedLang.has(lang)) return;\n warnedUnchangedLang.add(lang);\n console.warn(\n `[hyperscript-i18n] Translation unchanged for lang=\"${lang}\": \"${src.length > 60 ? src.slice(0, 60) + '…' : src}\". ` +\n 'This is fine if the source is already canonical English; otherwise the input may not match ' +\n 'any known pattern, or the language may not be registered. Original text is passed to ' +\n '_hyperscript as-is. Further elements in this language stay quiet — enable { debug: true } ' +\n 'for per-element detail.'\n );\n}\n\n/**\n * Create a _hyperscript plugin that enables multilingual hyperscript.\n *\n * @example\n * // Basic usage\n * _hyperscript.use(hyperscriptI18n());\n *\n * @example\n * // With options\n * _hyperscript.use(hyperscriptI18n({\n * defaultLanguage: 'ja',\n * confidenceThreshold: 0.6,\n * debug: true,\n * }));\n */\nexport function hyperscriptI18n(options: PluginOptions = {}) {\n return function plugin(hs: unknown): void {\n const host = hs as HyperscriptHost & HyperscriptParseHost;\n installAttributeTranslator(host, (src, elt) => {\n // Resolve language\n const lang = resolveLanguageWithOptions(elt, options);\n\n // If English or no language detected, pass through\n if (!lang || lang === 'en') return src;\n\n // Preprocess to English\n const english = preprocessToEnglish(src, lang, options);\n\n if (english !== src) {\n // Validity gate: the host parser is the consumer of this rewrite —\n // if it rejects the English, committing it would only trade a\n // translation gap for a parse error naming code the author never\n // wrote. Fall back to the original text instead.\n if (options.validateWithHost !== false && !acceptedByHost(host, english)) {\n if (options.debug) {\n console.log(\n `[hyperscript-i18n] ${lang}: host rejected \"${english}\" — keeping \"${src}\"`\n );\n } else {\n warnRejectedOnce(lang, src, english);\n }\n return src;\n }\n if (options.debug) {\n console.log(`[hyperscript-i18n] ${lang}: \"${src}\" → \"${english}\"`);\n }\n } else if (options.debug) {\n console.log(`[hyperscript-i18n] ${lang}: unchanged \"${src}\"`);\n } else {\n warnUnchangedOnce(lang, src);\n }\n\n return english;\n });\n };\n}\n\n/**\n * Resolve language with plugin options factored in.\n */\nfunction resolveLanguageWithOptions(elt: Element, options: PluginOptions): string | null {\n // Check custom attribute name\n if (options.languageAttribute) {\n const custom = elt.getAttribute(options.languageAttribute);\n if (custom) return custom.split('-')[0].toLowerCase();\n }\n\n // Standard resolution\n const resolved = resolveLanguage(elt);\n if (resolved) return resolved;\n\n // Fall back to default language\n return options.defaultLanguage ?? null;\n}\n\n/**\n * Standalone preprocessing function for programmatic use.\n * Call this when using _hyperscript.evaluate() or _hyperscript(\"code\") directly.\n *\n * @example\n * const english = preprocess(\"トグル .active\", \"ja\");\n * _hyperscript(english);\n */\nexport function preprocess(\n src: string,\n lang: string,\n config: Partial<PreprocessorConfig> = {}\n): string {\n if (lang === 'en') return src;\n return preprocessToEnglish(src, lang, config);\n}\n"],"mappings":"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","names":["preprocessToEnglish","translate","src","english"]}
1
+ {"version":3,"sources":["../src/language-resolver.ts","../src/preprocessor.ts","../src/preprocessor-core.ts","../src/attribute-translator.ts","../src/host-validate.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Language Resolver\n *\n * Determines the language of hyperscript code on a given element\n * using a cascading resolution strategy.\n */\n\n/**\n * Resolve the language for a hyperscript attribute on an element.\n *\n * Resolution order:\n * 1. `data-lang` attribute on the element itself\n * 2. `data-hyperscript-lang` attribute on the element or closest ancestor\n * 3. `lang` attribute on the element or closest ancestor (the HTML-standard\n * cascade — a `<section lang=\"es\">` localizes everything inside it, and a\n * nested `lang=\"en\"` opts back out). This is how the paired htmx-adapter\n * (`langOf()`) and loka-js resolve language, so `hx-*` and `_` attributes\n * on the same element agree.\n * 4. `lang` on `<html>` via `document.documentElement` — only reachable when\n * the element is DETACHED (step 3's ancestor walk covers `<html>` for\n * attached elements): a hook processing a not-yet-inserted fragment still\n * picks up the page default.\n * 5. null (assume English, no preprocessing needed)\n */\nexport function resolveLanguage(elt: Element): string | null {\n // 1. Explicit per-element\n const dataLang = elt.getAttribute('data-lang');\n if (dataLang) return normalizeLangCode(dataLang);\n\n // 2. Inherited from ancestor\n const hsLang =\n elt.getAttribute('data-hyperscript-lang') ??\n elt.closest?.('[data-hyperscript-lang]')?.getAttribute('data-hyperscript-lang');\n if (hsLang) return normalizeLangCode(hsLang);\n\n // 3. Standard lang cascade (nearest ancestor wins)\n const closestLang = elt.closest?.('[lang]')?.getAttribute('lang');\n if (closestLang) return normalizeLangCode(closestLang);\n\n // 4. Document-level lang (detached elements only — see doc comment)\n const htmlLang = typeof document !== 'undefined' ? document.documentElement?.lang : null;\n if (htmlLang && htmlLang !== 'en') return normalizeLangCode(htmlLang);\n\n return null;\n}\n\n/**\n * Normalize a BCP-47 language tag to an ISO 639-1 code.\n * e.g., \"ja-JP\" → \"ja\", \"zh-Hans\" → \"zh\", \"pt-BR\" → \"pt\"\n */\nfunction normalizeLangCode(lang: string): string {\n return lang.split('-')[0].toLowerCase();\n}\n","/**\n * Preprocessor (full path)\n *\n * Translates non-English hyperscript to English using the semantic parser\n * (with optional i18n fallback), so the original _hyperscript can parse it.\n *\n * The strategy/split/strip skeleton lives in preprocessor-core.ts, shared\n * with the slim path — only HOW one statement is parsed and rendered\n * differs here: semantic's parseSemantic + render('en'), plus a\n * translate() rescue when the parse is confident but yields no node.\n */\n\nimport { translate, render, parseSemantic, isLanguageRegistered } from '@lokascript/semantic';\n\nimport { createPreprocessToEnglish, type PreprocessorConfig } from './preprocessor-core';\n\nexport type { PreprocessorConfig };\n\n/**\n * Preprocess non-English hyperscript into English.\n *\n * Uses the semantic parser to parse the input in the source language,\n * then renders back to English. Falls through to i18n if configured.\n */\nexport const preprocessToEnglish = createPreprocessToEnglish({\n isLanguageRegistered,\n translateSingle(src, lang, threshold) {\n const result = parseSemantic(src, lang);\n if (result.confidence < threshold || !result.node) {\n // If we got SOME confidence but no node, fall back to translate()\n if (result.confidence >= threshold) {\n return translate(src, lang, 'en');\n }\n return null;\n }\n\n // Render the semantic node to English\n return render(result.node, 'en');\n },\n});\n","/**\n * Preprocessor core — the shared skeleton of the full and slim\n * preprocessors.\n *\n * Everything that is NOT genuinely different between the two paths lives\n * here exactly once: config defaults/threshold resolution, strategy\n * ordering (semantic → optional i18n), event-prefix stripping, and the\n * return-original fallback. The two real differences stay with each path,\n * injected as hooks:\n *\n * - HOW one statement is parsed and rendered to English\n * (`translateSingle`): the full path uses semantic's parseSemantic +\n * render + a translate() rescue for confident-but-nodeless parses;\n * the slim path uses parseWithConfidence + the custom\n * hyperscript-renderer (no English language data).\n * - WHERE the registry lookup comes from (`isLanguageRegistered`):\n * `@lokascript/semantic` vs `…/core` — under tsup's split dist these\n * are separate registry instances, so each path must bring its own.\n *\n * Behavior is pinned by the parity ratchet\n * (test/preprocessor-parity.*.test.ts + the committed fixture): both paths\n * are byte-identical to their pre-extraction outputs over the corpus.\n *\n * HISTORY: the skeleton used to carry a compound-splitting fallback\n * (`splitStatements` on localized `then` keywords + newlines,\n * `translateCompound` rejoining pieces with a hardcoded ` then `), run when\n * the whole-string parse declined. Deleted 2026-08-07 after measurement:\n * over all 3703 patterns.db corpus translations plus the parity corpus, on\n * BOTH paths, the split arm produced ZERO final outputs (full: 115\n * invocations, all returned null; slim: 32, all null) — post-whole-string-\n * first (#899), an input whose pieces each parse would have parsed whole,\n * so the arm's success condition is self-defeating. It was also defective\n * twice over: the regex split was string-literal-blind (`say 'now then\n * later'` split inside the literal) and the ` then ` rejoin is invalid\n * immediately after block headers. Deleting it erased both defect classes\n * wholesale; the parity fixture regenerated byte-identical.\n */\n\nexport interface PreprocessorConfig {\n /**\n * Minimum confidence threshold for semantic parsing (0-1). Default: 0.5.\n * Can be a single number (applies to all languages) or a per-language map.\n * Per-language thresholds are useful because SOV languages (ja, ko, tr) produce\n * inherently lower confidence scores than SVO languages (es, fr, de).\n *\n * @example\n * // Single threshold\n * { confidenceThreshold: 0.5 }\n *\n * @example\n * // Per-language thresholds\n * { confidenceThreshold: { es: 0.7, ja: 0.1, ko: 0.05, '*': 0.5 } }\n */\n confidenceThreshold: number | Record<string, number>;\n /** Strategy: 'semantic' (default), 'i18n', or 'auto' (semantic then i18n) */\n strategy: 'semantic' | 'i18n' | 'auto';\n /**\n * @deprecated Never implemented — `preprocessToEnglish` always returns a\n * string, and on translation failure that string is the original source\n * (there is nothing else it could return). The option has had no effect in\n * any released version and is ignored; it will be removed in a future major.\n */\n fallbackToOriginal?: boolean;\n /** Optional i18n toEnglish function (loaded dynamically if available) */\n i18nToEnglish?: (input: string, locale: string) => string;\n}\n\n/** The per-path seam — see module doc. */\nexport interface PreprocessorHooks {\n isLanguageRegistered(lang: string): boolean;\n /**\n * Parse ONE statement in `lang` and render it to English. Return null\n * when it cannot be translated (below-threshold confidence, no node, …);\n * the skeleton then tries the remaining strategies / falls back.\n */\n translateSingle(src: string, lang: string, threshold: number): string | null;\n}\n\nconst DEFAULT_THRESHOLD = 0.5;\n\nconst DEFAULT_CONFIG: PreprocessorConfig = {\n confidenceThreshold: DEFAULT_THRESHOLD,\n strategy: 'semantic',\n};\n\n/**\n * Resolve the confidence threshold for a specific language.\n * Supports both a single number and a per-language map with '*' as default.\n */\nfunction resolveThreshold(threshold: number | Record<string, number>, lang: string): number {\n if (typeof threshold === 'number') return threshold;\n return threshold[lang] ?? threshold['*'] ?? DEFAULT_THRESHOLD;\n}\n\n/**\n * Match _hyperscript event handler prefix: \"on [every] <event>[filter][.modifiers] \"\n *\n * Examples:\n * \"on click toggle .active\" → prefix: \"on click \", commands: \"toggle .active\"\n * \"on every click toggle .active\" → prefix: \"on every click \", commands: \"toggle .active\"\n * \"on click.debounce(300) toggle .x\" → prefix: \"on click.debounce(300) \", commands: \"toggle .x\"\n * \"on keyup[key=='Enter'] set x to 1\" → prefix: \"on keyup[key=='Enter'] \", commands: \"set x to 1\"\n * \"on click from body toggle .active\" → prefix: \"on click from body \", commands: \"toggle .active\"\n */\nconst EVENT_PREFIX_RE =\n /^(on\\s+(?:every\\s+)?[\\w-]+(?:\\[.*?\\])?(?:\\.[\\w-]+(?:\\([^)]*\\))?)*(?:\\s+from\\s+\\S+)?(?:\\s+queue\\s+\\w+)?\\s+)/;\n\nfunction stripEventPrefix(src: string): { prefix: string; commands: string } | null {\n const match = src.match(EVENT_PREFIX_RE);\n if (!match) return null;\n const prefix = match[1];\n const commands = src.slice(prefix.length);\n if (!commands) return null;\n return { prefix, commands };\n}\n\n/**\n * Build a `preprocessToEnglish` function from the per-path hooks.\n *\n * Handles _hyperscript feature prefixes (e.g. \"on click\", \"on every keyup\")\n * by stripping them, translating only the command portion, then reassembling.\n */\nexport function createPreprocessToEnglish(\n hooks: PreprocessorHooks\n): (src: string, lang: string, config?: Partial<PreprocessorConfig>) => string {\n /**\n * Try semantic translation: parse the WHOLE string in the source\n * language, render it to English. Returns null if confidence is below\n * threshold. The semantic parser handles `then`-sequences, newlines,\n * loop/tell bodies and behavior blocks natively (top-level\n * command-sequence support landed with the multiset-recall arc), so\n * there is deliberately no pre-splitting here — see the module doc's\n * HISTORY note for the measured deletion of the old split fallback.\n */\n function trySemanticTranslation(src: string, lang: string, threshold: number): string | null {\n try {\n // Check if the semantic parser can handle this\n if (!hooks.isLanguageRegistered(lang)) return null;\n\n return hooks.translateSingle(src, lang, threshold);\n } catch {\n return null;\n }\n }\n\n /**\n * Try i18n grammar transformation to English.\n * Returns null if the result is identical to input (no translation happened).\n */\n function tryI18nTranslation(\n src: string,\n lang: string,\n toEnglish: (input: string, locale: string) => string\n ): string | null {\n try {\n const result = toEnglish(src, lang);\n return result !== src ? result : null;\n } catch {\n return null;\n }\n }\n\n /**\n * Try all configured translation strategies on the input.\n * Returns null if none succeed.\n */\n function tryTranslateWithStrategies(\n src: string,\n lang: string,\n cfg: PreprocessorConfig\n ): string | null {\n // Strategy: semantic-only\n if (cfg.strategy === 'semantic' || cfg.strategy === 'auto') {\n const threshold = resolveThreshold(cfg.confidenceThreshold, lang);\n const result = trySemanticTranslation(src, lang, threshold);\n if (result !== null) return result;\n }\n\n // Strategy: i18n fallback (auto mode or i18n-only)\n if ((cfg.strategy === 'auto' || cfg.strategy === 'i18n') && cfg.i18nToEnglish) {\n const result = tryI18nTranslation(src, lang, cfg.i18nToEnglish);\n if (result !== null) return result;\n }\n\n return null;\n }\n\n return function preprocessToEnglish(\n src: string,\n lang: string,\n config: Partial<PreprocessorConfig> = {}\n ): string {\n // English→English is identity; skip semantic parsing which may mangle\n // the input. (Historically full-path only — the slim path gained it in\n // the shared-skeleton extraction, deliberately: it is a mangle guard.)\n if (lang === 'en') return src;\n\n const cfg = { ...DEFAULT_CONFIG, ...config };\n\n // Try translating the full string first\n const fullResult = tryTranslateWithStrategies(src, lang, cfg);\n if (fullResult !== null) return fullResult;\n\n // If full translation failed, try stripping event/feature prefix.\n // _hyperscript attributes often contain \"on <event> <commands>\" — the semantic\n // parser only understands command syntax, not event declarations.\n const stripped = stripEventPrefix(src);\n if (stripped) {\n const translated = tryTranslateWithStrategies(stripped.commands, lang, cfg);\n if (translated !== null) return stripped.prefix + translated;\n }\n\n // Fallback: return original (unconditional — see fallbackToOriginal's\n // deprecation note; the string contract leaves nothing else to return)\n return src;\n };\n}\n","/**\n * Attribute translator\n *\n * _hyperscript.org's Runtime#getScript is a private class field (`#getScript`),\n * not a property reachable through `internals.runtime` — every plugin variant\n * in this package used to monkey-patch `internals.runtime.getScript`, which\n * silently no-ops against current _hyperscript.org builds (the assignment\n * creates a stray own-property that the runtime's internal `#getScript()`\n * calls never read, so translation never happens and errors are swallowed by\n * `runtime.getScript.bind` throwing before any hyperscript even parses).\n *\n * `addBeforeProcessHook` is the supported, public extension point instead: it\n * fires on the subtree root passed to `processNode()` before the runtime reads\n * whichever configured attribute (`_`, `script`, `data-script` by default) or\n * `<script type=\"text/hyperscript\">` body holds the source. Rewriting that\n * attribute/body in place — before the runtime's own scan reaches it — gets\n * the same \"translate before parse\" effect through a mechanism the runtime\n * actually calls.\n *\n * A host may offer `addSourceTransform` instead (`@hyperfixi/engine`\n * does; _hyperscript.org does not). The host then\n * hands each script to the translator as it reads it, and the attribute keeps\n * the author's text: nothing in the DOM is rewritten, and a parse error can\n * say what was written.\n */\n\nexport interface HyperscriptHost {\n addBeforeProcessHook?: (fn: (elt: Element) => void) => void;\n /** Rewrite a script as the host reads it; the element is left as written. */\n addSourceTransform?: (fn: (src: string, elt: Element) => string | null) => void;\n config?: { attributes?: string };\n}\n\n/** Elements already processed, so a later `processNode()` call over the same\n * subtree (e.g. a sibling swap re-scanning a shared ancestor) doesn't\n * re-translate already-English text as if it were still the original language.\n * A WeakSet instead of a marker attribute: the same idempotency with zero DOM\n * mutation (devtools/serialization show exactly what the author wrote).\n * Serialize→reparse (e.g. an innerHTML round-trip) produces NEW elements that\n * are re-processed — safe, because re-translating already-English text is\n * confidence-gated into a no-op. */\nconst processed = new WeakSet<Element>();\n\nfunction scriptAttributeNames(hs: HyperscriptHost): string[] {\n const raw = hs.config?.attributes ?? '_, script, data-script';\n return raw\n .split(',')\n .map(s => s.trim())\n .filter(Boolean);\n}\n\nfunction findScriptAttribute(elt: Element, attrNames: string[]): string | null {\n for (const name of attrNames) {\n if (elt.hasAttribute(name)) return name;\n }\n return null;\n}\n\n/**\n * Install a translator that turns non-English scripts into English before the\n * host parses them: through the host's source transform when it has one,\n * otherwise by rewriting the script attributes in place.\n *\n * @param translate Given the raw source and its element, return the English\n * translation, or `null`/the same string to leave the element untouched\n * (English input, unresolved language, translation failure, etc).\n */\nexport function installAttributeTranslator(\n hs: HyperscriptHost,\n translate: (src: string, elt: Element) => string | null\n): void {\n if (typeof hs.addSourceTransform === 'function') {\n // The host asks only for an element it is about to initialise, so the\n // processed-set below is not needed on this path.\n hs.addSourceTransform(translate);\n return;\n }\n\n if (typeof hs.addBeforeProcessHook !== 'function') {\n console.warn(\n '[hyperscript-i18n] _hyperscript.addBeforeProcessHook is unavailable — ' +\n 'this build of _hyperscript.org is not supported. Load a current version ' +\n 'from https://unpkg.com/hyperscript.org.'\n );\n return;\n }\n\n const attrNames = scriptAttributeNames(hs);\n const selector = [...attrNames.map(a => `[${a}]`), 'script[type=\"text/hyperscript\"]'].join(', ');\n\n const translateOne = (elt: Element): void => {\n if (processed.has(elt)) return;\n\n if (elt instanceof HTMLScriptElement && elt.type === 'text/hyperscript') {\n const src = elt.textContent ?? '';\n if (!src) return;\n const english = translate(src, elt);\n processed.add(elt);\n if (english != null && english !== src) elt.textContent = english;\n return;\n }\n\n const attr = findScriptAttribute(elt, attrNames);\n if (!attr) return;\n const src = elt.getAttribute(attr);\n if (!src) return;\n const english = translate(src, elt);\n processed.add(elt);\n if (english != null && english !== src) elt.setAttribute(attr, english);\n };\n\n hs.addBeforeProcessHook((root: Element) => {\n if (!root || typeof root.querySelectorAll !== 'function') return;\n if (typeof root.matches === 'function' && root.matches(selector)) translateOne(root);\n root.querySelectorAll(selector).forEach(translateOne);\n });\n}\n","/**\n * Host-parser validity gate (review item F8).\n *\n * After the preprocessor rewrites an attribute to English, the plugin asks\n * the HOST _hyperscript runtime — the same parser that will consume the\n * rewrite moments later — whether the result actually parses. On rejection\n * the plugin falls back to the author's original text, so any parse error\n * the author then sees names code they wrote, not invisible generated\n * English. This is the runtime analog of the offline R4 canonical-validity\n * gate, and the F5 arc measured its failure class shipping in practice:\n * until the whole-string-first reorder (#899), 256 corpus rows rendered\n * English the engine rejects, with no warning anywhere.\n *\n * The engine has two failure channels — `parse().errors` collects grammar\n * errors, and the tokenizer THROWS on an unknown character — folded here\n * the same way test/whole-string-first.test.ts folds them.\n *\n * Zero-dependency module: shared by the full, slim, and lite plugin\n * variants, which must not share heavier import chains (the slim/lite\n * bundles exclude the full semantic package by construction).\n */\n\n/** The part of a host parse result the gate reads: grammar errors, and each handler's events. */\nexport interface HostParseResult {\n errors?: unknown[];\n features?: Array<{ events?: Array<{ on?: unknown }> }>;\n}\n\nexport interface HyperscriptParseHost {\n parse?: (src: string) => HostParseResult | null | undefined;\n}\n\n/**\n * Names that are references, never events. A translation that reads one as a\n * handler's event parses cleanly and does nothing: the README's Japanese\n * example `on click .active を me で 切り替え` became `on click on me toggle\n * .active`, which the engine reads as an empty click handler plus a handler for\n * an event named `me`, and the button stayed dead. The host accepts that, so\n * the gate asks for it by name.\n */\nconst REFERENCE_NAMES = new Set(['me', 'my', 'i', 'it', 'its', 'you', 'your']);\n\n/** The reference a parse result listens for as an event, if any. */\nexport function referenceReadAsEvent(\n result: HostParseResult | null | undefined\n): string | undefined {\n for (const feature of result?.features ?? []) {\n for (const event of feature.events ?? []) {\n if (typeof event.on === 'string' && REFERENCE_NAMES.has(event.on.toLowerCase()))\n return event.on;\n }\n }\n return undefined;\n}\n\n/**\n * True when the host's parser accepts `src` and reads no reference as a\n * handler's event (see REFERENCE_NAMES). Also true when the host exposes no\n * `parse()` — with nothing to validate against, the gate degrades to a no-op\n * rather than suppressing translation on unusual builds (same graceful\n * posture as the `addBeforeProcessHook` check).\n */\nexport function acceptedByHost(hs: HyperscriptParseHost, src: string): boolean {\n if (typeof hs.parse !== 'function') return true;\n try {\n const result = hs.parse(src);\n if (result?.errors && result.errors.length > 0) return false;\n return referenceReadAsEvent(result) === undefined;\n } catch {\n return false;\n }\n}\n\n/** Languages already warned about a rejected translation this page load —\n * same warn-once-per-lang convention as the unchanged-translation warning\n * (and htmx-adapter's warnMissingLangOnce). */\nconst warnedRejectedLang = new Set<string>();\n\n/** Reset the warn-once state. Mainly for tests. */\nexport function resetHostValidationWarnings(): void {\n warnedRejectedLang.clear();\n}\n\nexport function warnRejectedOnce(lang: string, src: string, english: string): void {\n if (warnedRejectedLang.has(lang)) return;\n warnedRejectedLang.add(lang);\n console.warn(\n `[hyperscript-i18n] Translation for lang=\"${lang}\" rendered hyperscript the host parser ` +\n `rejects (or reads as a handler for an event named like me/it/you) — falling back to the ` +\n `original text. ` +\n `Source: \"${src.length > 60 ? src.slice(0, 60) + '…' : src}\" → ` +\n `\"${english.length > 60 ? english.slice(0, 60) + '…' : english}\". ` +\n 'Further elements in this language stay quiet — enable { debug: true } for per-element detail.'\n );\n}\n","/**\n * _hyperscript Plugin\n *\n * Registers with _hyperscript.use() to rewrite non-English hyperscript\n * attributes into English before _hyperscript.org parses them.\n */\n\nimport { resolveLanguage } from './language-resolver';\nimport { preprocessToEnglish, type PreprocessorConfig } from './preprocessor';\nimport { installAttributeTranslator, type HyperscriptHost } from './attribute-translator';\nimport {\n acceptedByHost,\n warnRejectedOnce,\n resetHostValidationWarnings,\n type HyperscriptParseHost,\n} from './host-validate';\n\nexport interface PluginOptions extends Partial<PreprocessorConfig> {\n /** Default language for all elements (overridable per-element). */\n defaultLanguage?: string;\n /** Custom attribute name for per-element language. Default: \"data-lang\" */\n languageAttribute?: string;\n /** Enable debug logging to console. Default: false */\n debug?: boolean;\n /**\n * Validate rendered English on the HOST parser before committing the\n * rewrite; on rejection, fall back to the original text (so parse errors\n * name the author's code, not generated English). Default: true.\n * No-op on host builds that expose no `parse()`.\n */\n validateWithHost?: boolean;\n}\n\n/** Languages already warned about an unchanged translation this page load.\n * Unchanged output is common and often legitimate (canonical-English\n * hyperscript under a non-en lang scope), so warning per element per\n * processNode was pure noise — mirror htmx-adapter's warn-once-per-lang\n * convention and leave per-element detail to `debug: true`. */\nconst warnedUnchangedLang = new Set<string>();\n\n/** Reset the warn-once state (unchanged + host-rejected). Mainly for tests. */\nexport function resetTranslationWarnings(): void {\n warnedUnchangedLang.clear();\n resetHostValidationWarnings();\n}\n\nfunction warnUnchangedOnce(lang: string, src: string): void {\n if (warnedUnchangedLang.has(lang)) return;\n warnedUnchangedLang.add(lang);\n console.warn(\n `[hyperscript-i18n] Translation unchanged for lang=\"${lang}\": \"${src.length > 60 ? src.slice(0, 60) + '…' : src}\". ` +\n 'This is fine if the source is already canonical English; otherwise the input may not match ' +\n 'any known pattern, or the language may not be registered. Original text is passed to ' +\n '_hyperscript as-is. Further elements in this language stay quiet — enable { debug: true } ' +\n 'for per-element detail.'\n );\n}\n\n/**\n * Create a _hyperscript plugin that enables multilingual hyperscript.\n *\n * @example\n * // Basic usage\n * _hyperscript.use(hyperscriptI18n());\n *\n * @example\n * // With options\n * _hyperscript.use(hyperscriptI18n({\n * defaultLanguage: 'ja',\n * confidenceThreshold: 0.6,\n * debug: true,\n * }));\n */\nexport function hyperscriptI18n(options: PluginOptions = {}) {\n return function plugin(hs: unknown): void {\n const host = hs as HyperscriptHost & HyperscriptParseHost;\n installAttributeTranslator(host, (src, elt) => {\n // Resolve language\n const lang = resolveLanguageWithOptions(elt, options);\n\n // If English or no language detected, pass through\n if (!lang || lang === 'en') return src;\n\n // Preprocess to English\n const english = preprocessToEnglish(src, lang, options);\n\n if (english !== src) {\n // Validity gate: the host parser is the consumer of this rewrite —\n // if it rejects the English, committing it would only trade a\n // translation gap for a parse error naming code the author never\n // wrote. Fall back to the original text instead.\n if (options.validateWithHost !== false && !acceptedByHost(host, english)) {\n if (options.debug) {\n console.log(\n `[hyperscript-i18n] ${lang}: host rejected \"${english}\" — keeping \"${src}\"`\n );\n } else {\n warnRejectedOnce(lang, src, english);\n }\n return src;\n }\n if (options.debug) {\n console.log(`[hyperscript-i18n] ${lang}: \"${src}\" → \"${english}\"`);\n }\n } else if (options.debug) {\n console.log(`[hyperscript-i18n] ${lang}: unchanged \"${src}\"`);\n } else {\n warnUnchangedOnce(lang, src);\n }\n\n return english;\n });\n };\n}\n\n/**\n * Resolve language with plugin options factored in.\n */\nfunction resolveLanguageWithOptions(elt: Element, options: PluginOptions): string | null {\n // Check custom attribute name\n if (options.languageAttribute) {\n const custom = elt.getAttribute(options.languageAttribute);\n if (custom) return custom.split('-')[0].toLowerCase();\n }\n\n // Standard resolution\n const resolved = resolveLanguage(elt);\n if (resolved) return resolved;\n\n // Fall back to default language\n return options.defaultLanguage ?? null;\n}\n\n/**\n * Standalone preprocessing function for programmatic use.\n * Call this when using _hyperscript.evaluate() or _hyperscript(\"code\") directly.\n *\n * @example\n * const english = preprocess(\"トグル .active\", \"ja\");\n * _hyperscript(english);\n */\nexport function preprocess(\n src: string,\n lang: string,\n config: Partial<PreprocessorConfig> = {}\n): string {\n if (lang === 'en') return src;\n return preprocessToEnglish(src, lang, config);\n}\n"],"mappings":"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","names":["preprocessToEnglish","translate","src","english"]}
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "@lokascript/hyperscript-adapter",
3
- "version": "3.1.1",
3
+ "version": "3.3.0",
4
4
  "description": "Multilingual adapter plugin for original _hyperscript — write hyperscript in 24 languages",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
7
+ "unpkg": "dist/hyperscript-i18n.global.js",
8
+ "jsdelivr": "dist/hyperscript-i18n.global.js",
7
9
  "module": "dist/index.js",
8
10
  "types": "dist/index.d.ts",
9
11
  "exports": {
@@ -142,16 +144,17 @@
142
144
  },
143
145
  "homepage": "https://github.com/codetalcott/hyperfixi/tree/main/packages/hyperscript-adapter#readme",
144
146
  "dependencies": {
145
- "@lokascript/semantic": "^3.1.1"
147
+ "@lokascript/semantic": "^3.3.0"
146
148
  },
147
149
  "optionalDependencies": {
148
150
  "@lokascript/i18n": "*"
149
151
  },
150
152
  "devDependencies": {
151
- "@types/node": "^26.1.2",
153
+ "@hyperfixi/engine": "^3.3.0",
154
+ "@types/node": "^26.6.3",
152
155
  "tsup": "^8.0.0",
153
156
  "typescript": "^5.0.0",
154
- "vitest": "^4.1.5"
157
+ "vitest": "^5.0.2"
155
158
  },
156
159
  "files": [
157
160
  "dist",
@@ -16,10 +16,18 @@
16
16
  * attribute/body in place — before the runtime's own scan reaches it — gets
17
17
  * the same "translate before parse" effect through a mechanism the runtime
18
18
  * actually calls.
19
+ *
20
+ * A host may offer `addSourceTransform` instead (`@hyperfixi/engine`
21
+ * does; _hyperscript.org does not). The host then
22
+ * hands each script to the translator as it reads it, and the attribute keeps
23
+ * the author's text: nothing in the DOM is rewritten, and a parse error can
24
+ * say what was written.
19
25
  */
20
26
 
21
27
  export interface HyperscriptHost {
22
28
  addBeforeProcessHook?: (fn: (elt: Element) => void) => void;
29
+ /** Rewrite a script as the host reads it; the element is left as written. */
30
+ addSourceTransform?: (fn: (src: string, elt: Element) => string | null) => void;
23
31
  config?: { attributes?: string };
24
32
  }
25
33
 
@@ -49,8 +57,9 @@ function findScriptAttribute(elt: Element, attrNames: string[]): string | null {
49
57
  }
50
58
 
51
59
  /**
52
- * Install a translator that rewrites non-English script attributes to English
53
- * in place, before `_hyperscript.org` parses them.
60
+ * Install a translator that turns non-English scripts into English before the
61
+ * host parses them: through the host's source transform when it has one,
62
+ * otherwise by rewriting the script attributes in place.
54
63
  *
55
64
  * @param translate Given the raw source and its element, return the English
56
65
  * translation, or `null`/the same string to leave the element untouched
@@ -60,6 +69,13 @@ export function installAttributeTranslator(
60
69
  hs: HyperscriptHost,
61
70
  translate: (src: string, elt: Element) => string | null
62
71
  ): void {
72
+ if (typeof hs.addSourceTransform === 'function') {
73
+ // The host asks only for an element it is about to initialise, so the
74
+ // processed-set below is not needed on this path.
75
+ hs.addSourceTransform(translate);
76
+ return;
77
+ }
78
+
63
79
  if (typeof hs.addBeforeProcessHook !== 'function') {
64
80
  console.warn(
65
81
  '[hyperscript-i18n] _hyperscript.addBeforeProcessHook is unavailable — ' +
@@ -29,7 +29,7 @@ export const SYNTAX: Record<string, readonly [string, string][]> = {
29
29
  focus: [['patient', '']],
30
30
  for: [['patient', ''], ['source', 'in']],
31
31
  get: [['source', ''], ['destination', 'on']],
32
- go: [['destination', '']],
32
+ go: [['destination', ''], ['manner', 'in new']],
33
33
  halt: [['patient', '']],
34
34
  hide: [['patient', ''], ['style', 'with']],
35
35
  if: [['condition', '']],
@@ -20,21 +20,52 @@
20
20
  * bundles exclude the full semantic package by construction).
21
21
  */
22
22
 
23
+ /** The part of a host parse result the gate reads: grammar errors, and each handler's events. */
24
+ export interface HostParseResult {
25
+ errors?: unknown[];
26
+ features?: Array<{ events?: Array<{ on?: unknown }> }>;
27
+ }
28
+
23
29
  export interface HyperscriptParseHost {
24
- parse?: (src: string) => { errors?: unknown[] } | null | undefined;
30
+ parse?: (src: string) => HostParseResult | null | undefined;
31
+ }
32
+
33
+ /**
34
+ * Names that are references, never events. A translation that reads one as a
35
+ * handler's event parses cleanly and does nothing: the README's Japanese
36
+ * example `on click .active を me で 切り替え` became `on click on me toggle
37
+ * .active`, which the engine reads as an empty click handler plus a handler for
38
+ * an event named `me`, and the button stayed dead. The host accepts that, so
39
+ * the gate asks for it by name.
40
+ */
41
+ const REFERENCE_NAMES = new Set(['me', 'my', 'i', 'it', 'its', 'you', 'your']);
42
+
43
+ /** The reference a parse result listens for as an event, if any. */
44
+ export function referenceReadAsEvent(
45
+ result: HostParseResult | null | undefined
46
+ ): string | undefined {
47
+ for (const feature of result?.features ?? []) {
48
+ for (const event of feature.events ?? []) {
49
+ if (typeof event.on === 'string' && REFERENCE_NAMES.has(event.on.toLowerCase()))
50
+ return event.on;
51
+ }
52
+ }
53
+ return undefined;
25
54
  }
26
55
 
27
56
  /**
28
- * True when the host's parser accepts `src`. Also true when the host
29
- * exposes no `parse()` — with nothing to validate against, the gate
30
- * degrades to a no-op rather than suppressing translation on unusual
31
- * builds (same graceful posture as the `addBeforeProcessHook` check).
57
+ * True when the host's parser accepts `src` and reads no reference as a
58
+ * handler's event (see REFERENCE_NAMES). Also true when the host exposes no
59
+ * `parse()` — with nothing to validate against, the gate degrades to a no-op
60
+ * rather than suppressing translation on unusual builds (same graceful
61
+ * posture as the `addBeforeProcessHook` check).
32
62
  */
33
63
  export function acceptedByHost(hs: HyperscriptParseHost, src: string): boolean {
34
64
  if (typeof hs.parse !== 'function') return true;
35
65
  try {
36
66
  const result = hs.parse(src);
37
- return !result?.errors || result.errors.length === 0;
67
+ if (result?.errors && result.errors.length > 0) return false;
68
+ return referenceReadAsEvent(result) === undefined;
38
69
  } catch {
39
70
  return false;
40
71
  }
@@ -55,7 +86,8 @@ export function warnRejectedOnce(lang: string, src: string, english: string): vo
55
86
  warnedRejectedLang.add(lang);
56
87
  console.warn(
57
88
  `[hyperscript-i18n] Translation for lang="${lang}" rendered hyperscript the host parser ` +
58
- `rejects — falling back to the original text. ` +
89
+ `rejects (or reads as a handler for an event named like me/it/you) — falling back to the ` +
90
+ `original text. ` +
59
91
  `Source: "${src.length > 60 ? src.slice(0, 60) + '…' : src}" → ` +
60
92
  `"${english.length > 60 ? english.slice(0, 60) + '…' : english}". ` +
61
93
  'Further elements in this language stay quiet — enable { debug: true } for per-element detail.'
@@ -15,6 +15,7 @@ import type {
15
15
  SemanticRole,
16
16
  EventHandlerSemanticNode,
17
17
  CompoundSemanticNode,
18
+ LoopSemanticNode,
18
19
  } from '@lokascript/semantic/core';
19
20
 
20
21
  // ---------------------------------------------------------------------------
@@ -62,11 +63,31 @@ export function renderToHyperscript(node: SemanticNode): string {
62
63
  return renderEventHandler(node as EventHandlerSemanticNode);
63
64
  case 'compound':
64
65
  return renderCompound(node as CompoundSemanticNode);
66
+ case 'loop':
67
+ return renderLoopFlat(node as LoopSemanticNode);
65
68
  default:
66
69
  return renderCommand(node);
67
70
  }
68
71
  }
69
72
 
73
+ /**
74
+ * A loop renders FLAT on the slim path: its head, then its body as siblings,
75
+ * with no `end` — exactly the output from before the parser nested loop bodies.
76
+ * Closing it is not a free fix here: the slim syntax table drops `3 times`, so
77
+ * a closed `repeat … end` would be a VALID forever loop, where the flat form is
78
+ * invalid and host-validate keeps the author's text instead.
79
+ */
80
+ function renderLoopFlat(node: LoopSemanticNode): string {
81
+ const head: SemanticNode = { kind: 'command', action: node.action, roles: node.roles };
82
+ return renderCompound({
83
+ kind: 'compound',
84
+ action: 'compound',
85
+ roles: new Map(),
86
+ statements: [head, ...node.body],
87
+ chainType: 'then',
88
+ });
89
+ }
90
+
70
91
  // ---------------------------------------------------------------------------
71
92
  // Node-kind renderers
72
93
  // ---------------------------------------------------------------------------