@hyperscript-tools/i18n 2.7.1 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -6
- package/dist/chunk-7ZOFDQYR.js +57 -0
- package/dist/chunk-SYQOFAWY.js +89 -0
- package/dist/cli.js +64 -4
- package/dist/eleventy.d.ts +17 -1
- package/dist/eleventy.js +45 -4
- package/dist/index.d.ts +33 -1
- package/dist/index.js +14 -2
- package/dist/validate.d.ts +44 -0
- package/dist/validate.js +14 -0
- package/package.json +9 -4
- package/dist/chunk-XIEA5H7Z.js +0 -37
package/README.md
CHANGED
|
@@ -21,16 +21,68 @@ HTML structure is preserved.
|
|
|
21
21
|
|
|
22
22
|
### Flags
|
|
23
23
|
|
|
24
|
-
| Flag | Description
|
|
25
|
-
| --------------- |
|
|
26
|
-
| `--langs`, `-l` | Comma-separated target language codes (`ja,es,ko`).
|
|
27
|
-
| `--out`, `-o` | Output directory. Defaults to `.`.
|
|
28
|
-
| `--from`, `-f` | Source locale of the input. Defaults to `en`.
|
|
29
|
-
| `--strict` | Fail the run on any single attribute that can't be translated. Default is lenient.
|
|
24
|
+
| Flag | Description |
|
|
25
|
+
| --------------- | ------------------------------------------------------------------------------------------------ |
|
|
26
|
+
| `--langs`, `-l` | Comma-separated target language codes (`ja,es,ko`). |
|
|
27
|
+
| `--out`, `-o` | Output directory. Defaults to `.`. |
|
|
28
|
+
| `--from`, `-f` | Source locale of the input. Defaults to `en`. |
|
|
29
|
+
| `--strict` | Fail the run on any single attribute that can't be translated. Default is lenient. |
|
|
30
|
+
| `--check` | Parse-check the English hyperscript on the real engine and exit 3 if any is invalid (see below). |
|
|
30
31
|
|
|
31
32
|
Inputs may be individual files or directories — every `.html` in a directory
|
|
32
33
|
is processed.
|
|
33
34
|
|
|
35
|
+
## Canonical parse-check
|
|
36
|
+
|
|
37
|
+
Every `_="..."` attribute this tool touches on the **English side** can be
|
|
38
|
+
validated against the real [`hyperscript.org`](https://hyperscript.org) parser —
|
|
39
|
+
the same engine the browser runs. This is checked for:
|
|
40
|
+
|
|
41
|
+
- **input** when translating _from_ English (`--from en`, the default) — catches
|
|
42
|
+
an invalid canonical source before it ships in all 24 languages, and
|
|
43
|
+
- **output** when a target language is `en` (foreign → English).
|
|
44
|
+
|
|
45
|
+
Foreign-language _output_ is deliberately **not** round-trip-checked here: this
|
|
46
|
+
package's grammar transformer is lossy in reverse, so a round-trip gate would be
|
|
47
|
+
noise. Faithful foreign-output gating belongs to the semantic-engine transpiler
|
|
48
|
+
(roadmap §5, "later").
|
|
49
|
+
|
|
50
|
+
By default, invalid English prints a deduped warning and the run continues:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
sample.html: invalid hyperscript (English input -> ja): _="on click qqqq zzzz"
|
|
54
|
+
- Unexpected Token : qqqq
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Pass `--check` to escalate to a build failure (exit code **3**) after all files
|
|
58
|
+
are processed and written:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx @hyperscript-tools/i18n translate src/page.html --langs ja,es --out dist/ --check
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Exit codes:** `0` success · `1` no inputs matched (or `--check` but the parser
|
|
65
|
+
failed to load) · `2` usage error · `3` `--check` found invalid hyperscript.
|
|
66
|
+
|
|
67
|
+
In the Eleventy plugin, use the `parseCheck` option (`'off' | 'warn' | 'error'`,
|
|
68
|
+
default `'warn'`):
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
eleventyConfig.addPlugin(hyperscriptI18n, { parseCheck: 'error' });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Programmatically:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { validateHyperscript } from '@hyperscript-tools/i18n/validate';
|
|
78
|
+
|
|
79
|
+
const errors = await validateHyperscript('on click toggle .active'); // [] = valid
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
> **Caveat:** the attribute scanner is a regex over `_="..."`, so hyperscript
|
|
83
|
+
> inside HTML comments or `<script>` bodies is checked too. Warn-by-default keeps
|
|
84
|
+
> that from breaking builds.
|
|
85
|
+
|
|
34
86
|
## Eleventy plugin
|
|
35
87
|
|
|
36
88
|
```js
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/validate.ts
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import path from "path";
|
|
5
|
+
var cached;
|
|
6
|
+
function loadValidator() {
|
|
7
|
+
if (!cached) {
|
|
8
|
+
cached = (async () => {
|
|
9
|
+
const require2 = createRequire(import.meta.url);
|
|
10
|
+
const iife = require2.resolve("hyperscript.org");
|
|
11
|
+
const esm = path.join(path.dirname(iife), "_hyperscript.esm.js");
|
|
12
|
+
const mod = await import(pathToFileURL(esm).href);
|
|
13
|
+
const hs = mod.default ?? mod;
|
|
14
|
+
if (typeof hs?.parse !== "function") {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"Loaded hyperscript.org but it did not expose parse(); the package layout may have changed."
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return (src) => {
|
|
20
|
+
try {
|
|
21
|
+
return (hs.parse(src)?.errors ?? []).map((e) => e.message.split("\n")[0]);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return ["threw: " + e.message.split("\n")[0]];
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
})();
|
|
27
|
+
}
|
|
28
|
+
return cached;
|
|
29
|
+
}
|
|
30
|
+
async function validateHyperscript(src) {
|
|
31
|
+
return (await loadValidator())(src);
|
|
32
|
+
}
|
|
33
|
+
function formatParseCheckReport(r) {
|
|
34
|
+
const code = r.code.length > 80 ? r.code.slice(0, 77) + "..." : r.code;
|
|
35
|
+
const where = r.stage === "input" ? `English input -> ${r.to}` : `English output <- ${r.from}`;
|
|
36
|
+
return [`invalid hyperscript (${where}): _="${code}"`, ...r.errors.map((e) => ` - ${e}`)].join(
|
|
37
|
+
"\n"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
var warned = /* @__PURE__ */ new Set();
|
|
41
|
+
function warnInvalidOnce(r) {
|
|
42
|
+
if (warned.has(r.code)) return false;
|
|
43
|
+
warned.add(r.code);
|
|
44
|
+
console.warn(`[hyperscript-i18n] ${formatParseCheckReport(r)}`);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function __resetParseCheckWarnings() {
|
|
48
|
+
warned.clear();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
loadValidator,
|
|
53
|
+
validateHyperscript,
|
|
54
|
+
formatParseCheckReport,
|
|
55
|
+
warnInvalidOnce,
|
|
56
|
+
__resetParseCheckWarnings
|
|
57
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatParseCheckReport,
|
|
3
|
+
warnInvalidOnce
|
|
4
|
+
} from "./chunk-7ZOFDQYR.js";
|
|
5
|
+
|
|
6
|
+
// src/html.ts
|
|
7
|
+
import { translate } from "@lokascript/i18n";
|
|
8
|
+
var ATTR_PATTERNS = [
|
|
9
|
+
/(_\s*=\s*")([^"]+)(")/g,
|
|
10
|
+
/(_\s*=\s*')([^']+)(')/g,
|
|
11
|
+
/(_\s*=\s*`)([^`]+)(`)/g
|
|
12
|
+
];
|
|
13
|
+
function extractHyperscriptAttributes(html) {
|
|
14
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15
|
+
for (const pattern of ATTR_PATTERNS) {
|
|
16
|
+
for (const m of html.matchAll(pattern)) {
|
|
17
|
+
seen.add(m[2]);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return [...seen];
|
|
21
|
+
}
|
|
22
|
+
function checkHtmlInput(html, validate, to = "*") {
|
|
23
|
+
const reports = [];
|
|
24
|
+
for (const code of extractHyperscriptAttributes(html)) {
|
|
25
|
+
const errors = validate(code);
|
|
26
|
+
if (errors.length > 0) {
|
|
27
|
+
reports.push({ stage: "input", code, errors, from: "en", to });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return reports;
|
|
31
|
+
}
|
|
32
|
+
function dispatch(report, onInvalid) {
|
|
33
|
+
if (typeof onInvalid === "function") onInvalid(report);
|
|
34
|
+
else if (onInvalid === "error")
|
|
35
|
+
throw new Error(`[hyperscript-i18n] ${formatParseCheckReport(report)}`);
|
|
36
|
+
else warnInvalidOnce(report);
|
|
37
|
+
}
|
|
38
|
+
function translateHtml(html, to, options = {}) {
|
|
39
|
+
const from = options.from ?? "en";
|
|
40
|
+
const lenient = options.lenient ?? true;
|
|
41
|
+
const { validate } = options;
|
|
42
|
+
const onInvalid = options.onInvalid ?? "warn";
|
|
43
|
+
if (from === to) return html;
|
|
44
|
+
if (validate && options.checkInput !== false && from === "en") {
|
|
45
|
+
for (const report of checkHtmlInput(html, validate, to)) dispatch(report, onInvalid);
|
|
46
|
+
}
|
|
47
|
+
let out = html;
|
|
48
|
+
for (const pattern of ATTR_PATTERNS) {
|
|
49
|
+
out = out.replace(pattern, (_match, before, body, after) => {
|
|
50
|
+
let translated;
|
|
51
|
+
try {
|
|
52
|
+
translated = translate(body, from, to);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (lenient) return `${before}${body}${after}`;
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
if (validate && to === "en") {
|
|
58
|
+
const errors = validate(translated);
|
|
59
|
+
if (errors.length > 0) {
|
|
60
|
+
dispatch({ stage: "output", code: translated, errors, from, to }, onInvalid);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return `${before}${translated}${after}`;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
function translateHtmlToManyLangs(html, langs, options = {}) {
|
|
69
|
+
const from = options.from ?? "en";
|
|
70
|
+
let perLang = options;
|
|
71
|
+
if (options.validate && options.checkInput !== false && from === "en") {
|
|
72
|
+
for (const report of checkHtmlInput(html, options.validate)) {
|
|
73
|
+
dispatch(report, options.onInvalid ?? "warn");
|
|
74
|
+
}
|
|
75
|
+
perLang = { ...options, checkInput: false };
|
|
76
|
+
}
|
|
77
|
+
const out = /* @__PURE__ */ new Map();
|
|
78
|
+
for (const lang of langs) {
|
|
79
|
+
out.set(lang, translateHtml(html, lang, perLang));
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
extractHyperscriptAttributes,
|
|
86
|
+
checkHtmlInput,
|
|
87
|
+
translateHtml,
|
|
88
|
+
translateHtmlToManyLangs
|
|
89
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
checkHtmlInput,
|
|
3
4
|
translateHtml
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-SYQOFAWY.js";
|
|
6
|
+
import {
|
|
7
|
+
formatParseCheckReport,
|
|
8
|
+
loadValidator
|
|
9
|
+
} from "./chunk-7ZOFDQYR.js";
|
|
5
10
|
|
|
6
11
|
// src/cli.ts
|
|
7
12
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, statSync, readdirSync } from "fs";
|
|
@@ -13,7 +18,8 @@ function parseArgs(argv) {
|
|
|
13
18
|
langs: [],
|
|
14
19
|
from: "en",
|
|
15
20
|
out: ".",
|
|
16
|
-
strict: false
|
|
21
|
+
strict: false,
|
|
22
|
+
check: false
|
|
17
23
|
};
|
|
18
24
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") return args;
|
|
19
25
|
args.command = argv[0] === "translate" ? "translate" : "help";
|
|
@@ -28,6 +34,8 @@ function parseArgs(argv) {
|
|
|
28
34
|
args.from = argv[++i] ?? "en";
|
|
29
35
|
} else if (a === "--strict") {
|
|
30
36
|
args.strict = true;
|
|
37
|
+
} else if (a === "--check") {
|
|
38
|
+
args.check = true;
|
|
31
39
|
} else if (a.startsWith("-")) {
|
|
32
40
|
throw new Error(`Unknown flag: ${a}`);
|
|
33
41
|
} else {
|
|
@@ -59,7 +67,7 @@ function printHelp() {
|
|
|
59
67
|
console.log(`hyperscript-i18n \u2014 translate _="..." attributes in HTML
|
|
60
68
|
|
|
61
69
|
Usage:
|
|
62
|
-
hyperscript-i18n translate <input> [<input> ...] --langs <codes> --out <dir> [--from <code>] [--strict]
|
|
70
|
+
hyperscript-i18n translate <input> [<input> ...] --langs <codes> --out <dir> [--from <code>] [--strict] [--check]
|
|
63
71
|
|
|
64
72
|
Options:
|
|
65
73
|
--langs, -l Comma-separated target language codes (e.g. ja,es,ko).
|
|
@@ -67,10 +75,20 @@ Options:
|
|
|
67
75
|
--from, -f Source locale of the input. Defaults to 'en'.
|
|
68
76
|
--strict Fail the run if any single attribute fails to translate.
|
|
69
77
|
Default is lenient: untranslatable snippets are left in place.
|
|
78
|
+
--check Validate the ENGLISH hyperscript on the real hyperscript.org
|
|
79
|
+
parser (input when --from en; output when a target is en) and
|
|
80
|
+
exit 3 if any attribute is invalid. Without --check, invalid
|
|
81
|
+
attributes only print a warning.
|
|
70
82
|
|
|
71
83
|
Output:
|
|
72
84
|
For each input file foo.html and each lang, writes <out>/foo.<lang>.html.
|
|
73
85
|
|
|
86
|
+
Exit codes:
|
|
87
|
+
0 success
|
|
88
|
+
1 no HTML inputs matched, or --check requested but the parser failed to load
|
|
89
|
+
2 usage error (bad flags / missing --langs / no inputs)
|
|
90
|
+
3 --check found invalid hyperscript
|
|
91
|
+
|
|
74
92
|
Example:
|
|
75
93
|
hyperscript-i18n translate src/patterns.html --langs ja,es,ko --out dist/
|
|
76
94
|
`);
|
|
@@ -101,14 +119,43 @@ async function run(argv) {
|
|
|
101
119
|
return 1;
|
|
102
120
|
}
|
|
103
121
|
mkdirSync(args.out, { recursive: true });
|
|
122
|
+
let validate;
|
|
123
|
+
try {
|
|
124
|
+
validate = await loadValidator();
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const msg = err.message;
|
|
127
|
+
if (args.check) {
|
|
128
|
+
console.error(`--check requested but the canonical parser failed to load: ${msg}`);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
console.warn(`parse-check disabled (could not load hyperscript.org): ${msg}`);
|
|
132
|
+
}
|
|
133
|
+
const failures = [];
|
|
134
|
+
const printedCodes = /* @__PURE__ */ new Set();
|
|
135
|
+
const recorderFor = (file) => (report) => {
|
|
136
|
+
failures.push({ file, ...report });
|
|
137
|
+
if (!printedCodes.has(report.code)) {
|
|
138
|
+
printedCodes.add(report.code);
|
|
139
|
+
console.warn(`${file}: ${formatParseCheckReport(report)}`);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
104
142
|
let written = 0;
|
|
105
143
|
for (const file of files) {
|
|
106
144
|
const html = readFileSync(file, "utf8");
|
|
107
145
|
const stem = basename(file, extname(file));
|
|
146
|
+
const onInvalid = recorderFor(file);
|
|
147
|
+
if (validate && args.from === "en") {
|
|
148
|
+
for (const report of checkHtmlInput(html, validate)) onInvalid(report);
|
|
149
|
+
}
|
|
108
150
|
for (const lang of args.langs) {
|
|
109
151
|
const translated = translateHtml(html, lang, {
|
|
110
152
|
from: args.from,
|
|
111
|
-
lenient: !args.strict
|
|
153
|
+
lenient: !args.strict,
|
|
154
|
+
validate,
|
|
155
|
+
// still drives the OUTPUT check when lang === 'en'
|
|
156
|
+
checkInput: false,
|
|
157
|
+
// input already checked once above
|
|
158
|
+
onInvalid
|
|
112
159
|
});
|
|
113
160
|
const outPath = join(args.out, `${stem}.${lang}.html`);
|
|
114
161
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
@@ -119,6 +166,19 @@ async function run(argv) {
|
|
|
119
166
|
console.log(
|
|
120
167
|
`Wrote ${written} files (${files.length} input \xD7 ${args.langs.length} langs) to ${args.out}`
|
|
121
168
|
);
|
|
169
|
+
if (args.check && failures.length > 0) {
|
|
170
|
+
const fileCount = new Set(failures.map((f) => f.file)).size;
|
|
171
|
+
console.error(
|
|
172
|
+
`
|
|
173
|
+
Parse-check failed: ${failures.length} invalid hyperscript attribute(s) in ${fileCount} file(s):`
|
|
174
|
+
);
|
|
175
|
+
for (const f of failures) {
|
|
176
|
+
console.error(
|
|
177
|
+
` ${f.file} [${f.stage}]: ${formatParseCheckReport(f).split("\n").join("\n ")}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return 3;
|
|
181
|
+
}
|
|
122
182
|
return 0;
|
|
123
183
|
}
|
|
124
184
|
var isMain = import.meta.url === `file://${process.argv[1]}`;
|
package/dist/eleventy.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LangCode } from './index.js';
|
|
2
2
|
export { TranslateHtmlOptions, translateHtml, translateHtmlToManyLangs } from './index.js';
|
|
3
3
|
import '@lokascript/i18n';
|
|
4
|
+
import './validate.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Eleventy plugin entry: `@hyperscript-tools/i18n/eleventy`
|
|
@@ -22,6 +23,13 @@ import '@lokascript/i18n';
|
|
|
22
23
|
* export default function (eleventyConfig) {
|
|
23
24
|
* eleventyConfig.addPlugin(hyperscriptI18n);
|
|
24
25
|
* }
|
|
26
|
+
*
|
|
27
|
+
* The plugin is ASYNC: when `parseCheck` is on (the default), it awaits the
|
|
28
|
+
* canonical `hyperscript.org` parser before returning. Eleventy 3 awaits plugin
|
|
29
|
+
* callbacks passed to `addPlugin`, so the check is active before any template
|
|
30
|
+
* renders. All three filters are registered SYNCHRONOUSLY before that await, so
|
|
31
|
+
* an un-awaited caller (or Eleventy 2) still gets working filters — the check
|
|
32
|
+
* simply activates once the parser finishes loading.
|
|
25
33
|
*/
|
|
26
34
|
|
|
27
35
|
interface EleventyPluginOptions {
|
|
@@ -29,6 +37,14 @@ interface EleventyPluginOptions {
|
|
|
29
37
|
defaultFrom?: LangCode;
|
|
30
38
|
/** Default lenient flag for HTML translation. Defaults to true. */
|
|
31
39
|
lenient?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Canonical parse-check for the ENGLISH side (input when source is 'en';
|
|
42
|
+
* output when a target is 'en'):
|
|
43
|
+
* 'off' — no check
|
|
44
|
+
* 'warn' — print a deduped warning for invalid English (default)
|
|
45
|
+
* 'error' — throw, failing the build
|
|
46
|
+
*/
|
|
47
|
+
parseCheck?: 'off' | 'warn' | 'error';
|
|
32
48
|
/** Override filter names (in case of collisions in your config). */
|
|
33
49
|
filterNames?: {
|
|
34
50
|
snippet?: string;
|
|
@@ -39,6 +55,6 @@ interface EleventyPluginOptions {
|
|
|
39
55
|
interface EleventyConfig {
|
|
40
56
|
addFilter(name: string, fn: (...args: unknown[]) => unknown): void;
|
|
41
57
|
}
|
|
42
|
-
declare function hyperscriptI18nPlugin(eleventyConfig: EleventyConfig, options?: EleventyPluginOptions): void
|
|
58
|
+
declare function hyperscriptI18nPlugin(eleventyConfig: EleventyConfig, options?: EleventyPluginOptions): Promise<void>;
|
|
43
59
|
|
|
44
60
|
export { type EleventyPluginOptions, LangCode, hyperscriptI18nPlugin as default };
|
package/dist/eleventy.js
CHANGED
|
@@ -1,31 +1,52 @@
|
|
|
1
1
|
import {
|
|
2
2
|
translateHtml,
|
|
3
3
|
translateHtmlToManyLangs
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-SYQOFAWY.js";
|
|
5
|
+
import {
|
|
6
|
+
formatParseCheckReport,
|
|
7
|
+
loadValidator,
|
|
8
|
+
warnInvalidOnce
|
|
9
|
+
} from "./chunk-7ZOFDQYR.js";
|
|
5
10
|
|
|
6
11
|
// src/eleventy.ts
|
|
7
12
|
import { translate } from "@lokascript/i18n";
|
|
8
|
-
function hyperscriptI18nPlugin(eleventyConfig, options = {}) {
|
|
13
|
+
async function hyperscriptI18nPlugin(eleventyConfig, options = {}) {
|
|
9
14
|
const defaultFrom = options.defaultFrom ?? "en";
|
|
10
15
|
const lenient = options.lenient ?? true;
|
|
16
|
+
const parseCheck = options.parseCheck ?? "warn";
|
|
11
17
|
const names = {
|
|
12
18
|
snippet: options.filterNames?.snippet ?? "translateHs",
|
|
13
19
|
snippetMany: options.filterNames?.snippetMany ?? "translateHsAll",
|
|
14
20
|
html: options.filterNames?.html ?? "translateHsHtml"
|
|
15
21
|
};
|
|
22
|
+
let validate;
|
|
23
|
+
const check = (code, stage, from, to) => {
|
|
24
|
+
if (!validate) return;
|
|
25
|
+
const errors = validate(code);
|
|
26
|
+
if (errors.length === 0) return;
|
|
27
|
+
const report = { stage, code, errors, from, to };
|
|
28
|
+
if (parseCheck === "error")
|
|
29
|
+
throw new Error(`[hyperscript-i18n] ${formatParseCheckReport(report)}`);
|
|
30
|
+
warnInvalidOnce(report);
|
|
31
|
+
};
|
|
16
32
|
eleventyConfig.addFilter(names.snippet, (input, to, from) => {
|
|
17
33
|
if (typeof input !== "string" || typeof to !== "string") return input;
|
|
18
34
|
const source = typeof from === "string" ? from : defaultFrom;
|
|
19
35
|
if (source === to) return input;
|
|
36
|
+
if (source === "en") check(input, "input", source, to);
|
|
37
|
+
let out;
|
|
20
38
|
try {
|
|
21
|
-
|
|
39
|
+
out = translate(input, source, to);
|
|
22
40
|
} catch {
|
|
23
41
|
return lenient ? input : "";
|
|
24
42
|
}
|
|
43
|
+
if (to === "en") check(out, "output", source, to);
|
|
44
|
+
return out;
|
|
25
45
|
});
|
|
26
46
|
eleventyConfig.addFilter(names.snippetMany, (input, langs, from) => {
|
|
27
47
|
if (typeof input !== "string" || !Array.isArray(langs)) return {};
|
|
28
48
|
const source = typeof from === "string" ? from : defaultFrom;
|
|
49
|
+
if (source === "en") check(input, "input", source, "*");
|
|
29
50
|
const out = {};
|
|
30
51
|
for (const lang of langs) {
|
|
31
52
|
if (typeof lang !== "string") continue;
|
|
@@ -37,7 +58,9 @@ function hyperscriptI18nPlugin(eleventyConfig, options = {}) {
|
|
|
37
58
|
out[lang] = translate(input, source, lang);
|
|
38
59
|
} catch {
|
|
39
60
|
out[lang] = lenient ? input : "";
|
|
61
|
+
continue;
|
|
40
62
|
}
|
|
63
|
+
if (lang === "en") check(out[lang], "output", source, "en");
|
|
41
64
|
}
|
|
42
65
|
return out;
|
|
43
66
|
});
|
|
@@ -45,10 +68,28 @@ function hyperscriptI18nPlugin(eleventyConfig, options = {}) {
|
|
|
45
68
|
if (typeof html !== "string" || typeof to !== "string") return html;
|
|
46
69
|
const opts = {
|
|
47
70
|
from: typeof from === "string" ? from : defaultFrom,
|
|
48
|
-
lenient
|
|
71
|
+
lenient,
|
|
72
|
+
validate: parseCheck === "off" ? void 0 : validate,
|
|
73
|
+
checkInput: true,
|
|
74
|
+
onInvalid: parseCheck === "error" ? "error" : "warn"
|
|
49
75
|
};
|
|
50
76
|
return translateHtml(html, to, opts);
|
|
51
77
|
});
|
|
78
|
+
if (parseCheck !== "off") {
|
|
79
|
+
try {
|
|
80
|
+
validate = await loadValidator();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const msg = err.message;
|
|
83
|
+
if (parseCheck === "error") {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`[hyperscript-i18n] parseCheck 'error' but the canonical parser failed to load: ${msg}`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
console.warn(
|
|
89
|
+
`[hyperscript-i18n] parse-check disabled (could not load hyperscript.org): ${msg}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
52
93
|
}
|
|
53
94
|
export {
|
|
54
95
|
hyperscriptI18nPlugin as default,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { GrammarTransformer, toEnglish, toLocale, translate } from '@lokascript/i18n';
|
|
2
|
+
import { CanonicalValidate, ParseCheckReport } from './validate.js';
|
|
3
|
+
export { formatParseCheckReport, loadValidator, validateHyperscript } from './validate.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* HTML attribute scanner + translator.
|
|
@@ -7,8 +9,16 @@ export { GrammarTransformer, toEnglish, toLocale, translate } from '@lokascript/
|
|
|
7
9
|
* attributes, translates each from a source locale to a target locale, and
|
|
8
10
|
* returns the rewritten HTML. The grammar transformer doesn't know about HTML;
|
|
9
11
|
* this module is the glue.
|
|
12
|
+
*
|
|
13
|
+
* Optionally parse-checks the ENGLISH side against the real `hyperscript.org`
|
|
14
|
+
* engine (see ./validate): the input when `from === 'en'` and the output when
|
|
15
|
+
* `to === 'en'`. The check only runs when a `validate` function is supplied, so
|
|
16
|
+
* existing callers see no behaviour change.
|
|
10
17
|
*/
|
|
18
|
+
|
|
11
19
|
type LangCode = string;
|
|
20
|
+
/** What to do with an invalid-English report. */
|
|
21
|
+
type OnInvalid = 'warn' | 'error' | ((report: ParseCheckReport) => void);
|
|
12
22
|
interface TranslateHtmlOptions {
|
|
13
23
|
/** Source locale of the input HTML's `_=` attributes. Defaults to 'en'. */
|
|
14
24
|
from?: LangCode;
|
|
@@ -18,8 +28,30 @@ interface TranslateHtmlOptions {
|
|
|
18
28
|
* whole page build.
|
|
19
29
|
*/
|
|
20
30
|
lenient?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Canonical validator (from `loadValidator()`). When absent, no parse-check
|
|
33
|
+
* runs and behaviour is unchanged.
|
|
34
|
+
*/
|
|
35
|
+
validate?: CanonicalValidate;
|
|
36
|
+
/** What to do with an invalid-English report. Default 'warn' (deduped console.warn). */
|
|
37
|
+
onInvalid?: OnInvalid;
|
|
38
|
+
/**
|
|
39
|
+
* Set false when the caller already validated the English input (as
|
|
40
|
+
* `translateHtmlToManyLangs` and the CLI do). Default true.
|
|
41
|
+
*/
|
|
42
|
+
checkInput?: boolean;
|
|
21
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Every `_=` attribute body in `html` (double/single/backtick quoted), deduped,
|
|
46
|
+
* in document order.
|
|
47
|
+
*/
|
|
48
|
+
declare function extractHyperscriptAttributes(html: string): string[];
|
|
49
|
+
/**
|
|
50
|
+
* Validate every English `_=` attribute body of `html`; returns a report for
|
|
51
|
+
* each invalid one (stage 'input').
|
|
52
|
+
*/
|
|
53
|
+
declare function checkHtmlInput(html: string, validate: CanonicalValidate, to?: LangCode): ParseCheckReport[];
|
|
22
54
|
declare function translateHtml(html: string, to: LangCode, options?: TranslateHtmlOptions): string;
|
|
23
55
|
declare function translateHtmlToManyLangs(html: string, langs: ReadonlyArray<LangCode>, options?: TranslateHtmlOptions): Map<LangCode, string>;
|
|
24
56
|
|
|
25
|
-
export { type LangCode, type TranslateHtmlOptions, translateHtml, translateHtmlToManyLangs };
|
|
57
|
+
export { CanonicalValidate, type LangCode, type OnInvalid, ParseCheckReport, type TranslateHtmlOptions, checkHtmlInput, extractHyperscriptAttributes, translateHtml, translateHtmlToManyLangs };
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
|
+
checkHtmlInput,
|
|
3
|
+
extractHyperscriptAttributes,
|
|
2
4
|
translateHtml,
|
|
3
5
|
translateHtmlToManyLangs
|
|
4
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-SYQOFAWY.js";
|
|
7
|
+
import {
|
|
8
|
+
formatParseCheckReport,
|
|
9
|
+
loadValidator,
|
|
10
|
+
validateHyperscript
|
|
11
|
+
} from "./chunk-7ZOFDQYR.js";
|
|
5
12
|
|
|
6
13
|
// src/index.ts
|
|
7
14
|
import { translate, toLocale, toEnglish, GrammarTransformer } from "@lokascript/i18n";
|
|
8
15
|
export {
|
|
9
16
|
GrammarTransformer,
|
|
17
|
+
checkHtmlInput,
|
|
18
|
+
extractHyperscriptAttributes,
|
|
19
|
+
formatParseCheckReport,
|
|
20
|
+
loadValidator,
|
|
10
21
|
toEnglish,
|
|
11
22
|
toLocale,
|
|
12
23
|
translate,
|
|
13
24
|
translateHtml,
|
|
14
|
-
translateHtmlToManyLangs
|
|
25
|
+
translateHtmlToManyLangs,
|
|
26
|
+
validateHyperscript
|
|
15
27
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical parse-check.
|
|
3
|
+
*
|
|
4
|
+
* Loads the REAL `hyperscript.org` engine headlessly (parse-only needs no DOM)
|
|
5
|
+
* and exposes a validate function that NEVER throws. The engine has two failure
|
|
6
|
+
* channels: `parse().errors` COLLECTS grammar errors, but the tokenizer THROWS
|
|
7
|
+
* on an unknown character (e.g. a leaked non-ASCII surface → `Unknown token: เ`)
|
|
8
|
+
* and the `js` command's `new Function` throws on invalid JavaScript. Both
|
|
9
|
+
* throws are folded into the returned array so callers have one contract —
|
|
10
|
+
* check `.length === 0`, never wrap in try/catch expecting invalidity to throw.
|
|
11
|
+
*
|
|
12
|
+
* Self-contained on purpose: node builtins + `hyperscript.org` only, no
|
|
13
|
+
* `@lokascript/*` or `@hyperfixi/*` imports, so the package stays extraction-ready.
|
|
14
|
+
*/
|
|
15
|
+
/** Parse `src` on the canonical engine; returns error messages (empty = valid). Never throws. */
|
|
16
|
+
type CanonicalValidate = (src: string) => string[];
|
|
17
|
+
interface ParseCheckReport {
|
|
18
|
+
/** 'input' = English source before translating; 'output' = English produced from a foreign source. */
|
|
19
|
+
stage: 'input' | 'output';
|
|
20
|
+
/** The invalid English hyperscript. */
|
|
21
|
+
code: string;
|
|
22
|
+
/** Canonical parser error messages (first line each). */
|
|
23
|
+
errors: string[];
|
|
24
|
+
/** Source locale of the translate call ('en' for stage 'input'). */
|
|
25
|
+
from: string;
|
|
26
|
+
/** Target locale ('en' for stage 'output'; '*' for an aggregated input check). */
|
|
27
|
+
to: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Load (once, module-cached) the canonical validator. Resolve `hyperscript.org`,
|
|
31
|
+
* then import the sibling prebuilt `_hyperscript.esm.js` by file URL. Rejects if
|
|
32
|
+
* the package cannot be loaded or does not expose `parse()`.
|
|
33
|
+
*/
|
|
34
|
+
declare function loadValidator(): Promise<CanonicalValidate>;
|
|
35
|
+
/** Convenience: load (cached) and validate in one call. */
|
|
36
|
+
declare function validateHyperscript(src: string): Promise<string[]>;
|
|
37
|
+
/** Multi-line human-readable rendering of a report (head line + one ` - <error>` line each). */
|
|
38
|
+
declare function formatParseCheckReport(r: ParseCheckReport): string;
|
|
39
|
+
/** Default warn printer; dedupes by `r.code` per process. Returns true if it printed. */
|
|
40
|
+
declare function warnInvalidOnce(r: ParseCheckReport): boolean;
|
|
41
|
+
/** @internal test hook — clears the per-process warn-dedupe set. */
|
|
42
|
+
declare function __resetParseCheckWarnings(): void;
|
|
43
|
+
|
|
44
|
+
export { type CanonicalValidate, type ParseCheckReport, __resetParseCheckWarnings, formatParseCheckReport, loadValidator, validateHyperscript, warnInvalidOnce };
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__resetParseCheckWarnings,
|
|
3
|
+
formatParseCheckReport,
|
|
4
|
+
loadValidator,
|
|
5
|
+
validateHyperscript,
|
|
6
|
+
warnInvalidOnce
|
|
7
|
+
} from "./chunk-7ZOFDQYR.js";
|
|
8
|
+
export {
|
|
9
|
+
__resetParseCheckWarnings,
|
|
10
|
+
formatParseCheckReport,
|
|
11
|
+
loadValidator,
|
|
12
|
+
validateHyperscript,
|
|
13
|
+
warnInvalidOnce
|
|
14
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperscript-tools/i18n",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Build-time translation for hyperscript code samples — generate per-language HTML from a canonical English source",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,11 +19,15 @@
|
|
|
19
19
|
},
|
|
20
20
|
"./cli": {
|
|
21
21
|
"default": "./dist/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"./validate": {
|
|
24
|
+
"types": "./dist/validate.d.ts",
|
|
25
|
+
"import": "./dist/validate.js"
|
|
22
26
|
}
|
|
23
27
|
},
|
|
24
28
|
"scripts": {
|
|
25
|
-
"build": "tsup src/index.ts src/cli.ts src/eleventy.ts --format esm --dts --clean",
|
|
26
|
-
"dev": "tsup src/index.ts src/cli.ts src/eleventy.ts --format esm --dts --watch",
|
|
29
|
+
"build": "tsup src/index.ts src/cli.ts src/eleventy.ts src/validate.ts --format esm --dts --clean",
|
|
30
|
+
"dev": "tsup src/index.ts src/cli.ts src/eleventy.ts src/validate.ts --format esm --dts --watch",
|
|
27
31
|
"typecheck": "tsc --noEmit",
|
|
28
32
|
"pretest": "../../scripts/ensure-fresh.sh ../i18n . && npm run build --silent",
|
|
29
33
|
"test": "node --test test/*.test.mjs",
|
|
@@ -44,7 +48,8 @@
|
|
|
44
48
|
"author": "HyperFixi Contributors",
|
|
45
49
|
"license": "MIT",
|
|
46
50
|
"dependencies": {
|
|
47
|
-
"@lokascript/i18n": "^2.
|
|
51
|
+
"@lokascript/i18n": "^2.8.0",
|
|
52
|
+
"hyperscript.org": "^0.9.93"
|
|
48
53
|
},
|
|
49
54
|
"devDependencies": {
|
|
50
55
|
"@types/node": "^20.0.0",
|
package/dist/chunk-XIEA5H7Z.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// src/html.ts
|
|
2
|
-
import { translate } from "@lokascript/i18n";
|
|
3
|
-
var ATTR_PATTERNS = [
|
|
4
|
-
/(_\s*=\s*")([^"]+)(")/g,
|
|
5
|
-
/(_\s*=\s*')([^']+)(')/g,
|
|
6
|
-
/(_\s*=\s*`)([^`]+)(`)/g
|
|
7
|
-
];
|
|
8
|
-
function translateHtml(html, to, options = {}) {
|
|
9
|
-
const from = options.from ?? "en";
|
|
10
|
-
const lenient = options.lenient ?? true;
|
|
11
|
-
if (from === to) return html;
|
|
12
|
-
let out = html;
|
|
13
|
-
for (const pattern of ATTR_PATTERNS) {
|
|
14
|
-
out = out.replace(pattern, (_match, before, body, after) => {
|
|
15
|
-
try {
|
|
16
|
-
const translated = translate(body, from, to);
|
|
17
|
-
return `${before}${translated}${after}`;
|
|
18
|
-
} catch (err) {
|
|
19
|
-
if (lenient) return `${before}${body}${after}`;
|
|
20
|
-
throw err;
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
return out;
|
|
25
|
-
}
|
|
26
|
-
function translateHtmlToManyLangs(html, langs, options = {}) {
|
|
27
|
-
const out = /* @__PURE__ */ new Map();
|
|
28
|
-
for (const lang of langs) {
|
|
29
|
-
out.set(lang, translateHtml(html, lang, options));
|
|
30
|
-
}
|
|
31
|
-
return out;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export {
|
|
35
|
-
translateHtml,
|
|
36
|
-
translateHtmlToManyLangs
|
|
37
|
-
};
|