@ecodev/natural 69.0.1 → 69.0.3

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 (2) hide show
  1. package/bin/i18n-check.mjs +127 -0
  2. package/package.json +4 -1
@@ -0,0 +1,127 @@
1
+ #! /usr/bin/env node
2
+ "use strict";
3
+ import { fileURLToPath } from "node:url";
4
+ import { extname, join, resolve } from "node:path";
5
+ import { readdirSync, readFileSync, statSync } from "node:fs";
6
+ import { parse, serializeOuter } from "parse5";
7
+ const currentFilePath = fileURLToPath(import.meta.url);
8
+ if (currentFilePath === process.argv[1]) {
9
+ main();
10
+ }
11
+ function main() {
12
+ const targets = process.argv.slice(2);
13
+ if (!targets.length) {
14
+ console.error("Error: Missing arguments for file or directory path(s).");
15
+ process.exit(1);
16
+ }
17
+ const errorCount = checkAll(targets);
18
+ if (errorCount) {
19
+ console.error(`${errorCount} i18n errors found !`);
20
+ process.exit(1);
21
+ }
22
+ }
23
+ function checkAll(targets) {
24
+ let errorCount = 0;
25
+ const files = allFiles(targets);
26
+ for (const file of files) {
27
+ const content = readFileSync(file, "utf-8");
28
+ if (file.endsWith(".html")) {
29
+ errorCount += checkHtml(file, content);
30
+ } else {
31
+ errorCount += checkXlf(file, content);
32
+ }
33
+ }
34
+ return errorCount;
35
+ }
36
+ function allFiles(targets) {
37
+ const files = /* @__PURE__ */ new Set();
38
+ for (const target of targets) {
39
+ const path = resolve(target);
40
+ const targetStat = statSync(path);
41
+ if (targetStat.isDirectory()) {
42
+ const entries = readdirSync(target, { recursive: true, withFileTypes: true });
43
+ for (const entry of entries) {
44
+ if (entry.isFile() && [".html", ".xlf"].includes(extname(entry.name))) {
45
+ files.add(resolve(join(entry.parentPath, entry.name)));
46
+ }
47
+ }
48
+ } else {
49
+ files.add(path);
50
+ }
51
+ }
52
+ return [...files.values()];
53
+ }
54
+ export function checkHtml(file, content) {
55
+ const document = parse(content);
56
+ return walkAST(document, file);
57
+ }
58
+ function walkAST(node, file) {
59
+ let errorCount = 0;
60
+ if ("tagName" in node && node.tagName) {
61
+ const element = node;
62
+ const hasI18n = element.attrs.some((attr) => attr.name === "i18n");
63
+ if (hasI18n) {
64
+ const textContent = getTextContent(element);
65
+ const errorMessage = checkHtmlError(textContent);
66
+ if (errorMessage) {
67
+ printError(file, errorMessage, serializeOuter(element));
68
+ errorCount++;
69
+ }
70
+ }
71
+ }
72
+ if ("childNodes" in node && node.childNodes) {
73
+ for (const child of node.childNodes) {
74
+ errorCount += walkAST(child, file);
75
+ }
76
+ }
77
+ if (node.nodeName === "template") {
78
+ errorCount += walkAST(node.content, file);
79
+ }
80
+ return errorCount;
81
+ }
82
+ function getTextContent(node) {
83
+ let textContent = "";
84
+ if (!("childNodes" in node)) {
85
+ return textContent;
86
+ }
87
+ for (const child of node.childNodes || []) {
88
+ if (child.nodeName === "#text") {
89
+ textContent += child.value;
90
+ } else {
91
+ textContent += getTextContent(child);
92
+ }
93
+ }
94
+ return textContent;
95
+ }
96
+ export function checkHtmlError(textContent) {
97
+ const nbsp = "\xA0";
98
+ textContent = textContent.replaceAll(nbsp, " ");
99
+ if (/^\s|\s$/.test(textContent)) {
100
+ return "Must not start or finish with whitespace";
101
+ } else if (/^\{\{[^}]*}}$/.test(textContent)) {
102
+ return "Must not contain only interpolation";
103
+ }
104
+ return "";
105
+ }
106
+ export function checkXlf(file, content) {
107
+ let errorCount = 0;
108
+ const patterns = {
109
+ "XLF must have ICU within <x> elements (not `{{ foo }}`)": /^\s*(?<line>.*[^";]\{\{.*)$/gm,
110
+ "XLF ICU must not have quotes that break syntax": /^\s*(?<line>.*\{\{[^}]*(‘|’)[^}]*}}.*)$/gm
111
+ };
112
+ for (const [message, r] of Object.entries(patterns)) {
113
+ let result = null;
114
+ while (result = r.exec(content)) {
115
+ const line = result.groups?.line ?? "";
116
+ printError(file, message, line);
117
+ errorCount++;
118
+ }
119
+ }
120
+ return errorCount;
121
+ }
122
+ function printError(file, errorMessage, content) {
123
+ console.log(`\u{1F6D1} ${errorMessage}:`);
124
+ console.log(file);
125
+ console.log(`${content}
126
+ `);
127
+ }
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@ecodev/natural",
3
- "version": "69.0.1",
3
+ "version": "69.0.3",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Ecodev/natural.git"
8
8
  },
9
9
  "sideEffects": false,
10
+ "bin": {
11
+ "i18n-check": "bin/i18n-check.mjs"
12
+ },
10
13
  "exports": {
11
14
  ".": {
12
15
  "sass": "./src/lib/_natural.scss",