eslint-plugin-intlayer 9.7.2 → 9.8.0-canary.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 +8 -2
- package/dist/cjs/@intlayer/config/dist/esm/extract/ignoreDirectives.cjs +60 -0
- package/dist/cjs/@intlayer/config/dist/esm/extract/ignoreDirectives.cjs.map +1 -0
- package/dist/cjs/@intlayer/config/dist/esm/extract/shouldExtract.cjs +3 -2
- package/dist/cjs/@intlayer/config/dist/esm/extract/shouldExtract.cjs.map +1 -1
- package/dist/cjs/rules/noRawText.cjs +35 -0
- package/dist/cjs/rules/noRawText.cjs.map +1 -1
- package/dist/esm/@intlayer/config/dist/esm/extract/ignoreDirectives.mjs +60 -0
- package/dist/esm/@intlayer/config/dist/esm/extract/ignoreDirectives.mjs.map +1 -0
- package/dist/esm/@intlayer/config/dist/esm/extract/shouldExtract.mjs +3 -2
- package/dist/esm/@intlayer/config/dist/esm/extract/shouldExtract.mjs.map +1 -1
- package/dist/esm/rules/noRawText.mjs +35 -0
- package/dist/esm/rules/noRawText.mjs.map +1 -1
- package/dist/types/rules/noRawText.d.ts.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -184,8 +184,14 @@ Reports user-facing strings that are not declared in a dictionary.
|
|
|
184
184
|
|
|
185
185
|
The predicate is `shouldExtract` from `@intlayer/config/extract` — the very same
|
|
186
186
|
heuristic `intlayer extract` uses — so what this rule reports is exactly what the
|
|
187
|
-
extractor would rewrite.
|
|
188
|
-
|
|
187
|
+
extractor would rewrite. CSS class strings, technical identifiers and
|
|
188
|
+
code-shaped strings are filtered out.
|
|
189
|
+
|
|
190
|
+
Proper nouns and brand names cannot be told apart from labels, so they are
|
|
191
|
+
skipped with the compiler's comment directives, which this rule honors too:
|
|
192
|
+
`// intlayer-ignore-line`, `// intlayer-ignore-next-line` (or
|
|
193
|
+
`{/* intlayer-ignore-next-line */}` / `<!-- intlayer-ignore-next-line -->` above
|
|
194
|
+
an element) and `// intlayer-ignore-file`.
|
|
189
195
|
|
|
190
196
|
```jsx
|
|
191
197
|
// ✗
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// ../@intlayer/config/dist/esm/extract/ignoreDirectives.mjs
|
|
2
|
+
/**
|
|
3
|
+
* A directive only counts when it opens a comment (`//`, `/*`, `{/*` or
|
|
4
|
+
* `<!--`), so the same words inside a translatable string are not mistaken
|
|
5
|
+
* for one.
|
|
6
|
+
*/
|
|
7
|
+
const DIRECTIVE_REGEX = /(?:\/\/|\/\*|<!--)\s*(intlayer-ignore-(?:file|next-line|line))\b/g;
|
|
8
|
+
/** Offsets at which each line of the source starts (index 0 = line 1). */
|
|
9
|
+
const getLineStartOffsets = (source) => {
|
|
10
|
+
const lineStartOffsets = [0];
|
|
11
|
+
for (let index = 0; index < source.length; index++) if (source[index] === "\n") lineStartOffsets.push(index + 1);
|
|
12
|
+
return lineStartOffsets;
|
|
13
|
+
};
|
|
14
|
+
/** Zero-based line index of a character offset, by binary search. */
|
|
15
|
+
const getLineIndex = (lineStartOffsets, offset) => {
|
|
16
|
+
let low = 0;
|
|
17
|
+
let high = lineStartOffsets.length - 1;
|
|
18
|
+
while (low < high) {
|
|
19
|
+
const middle = Math.ceil((low + high) / 2);
|
|
20
|
+
if ((lineStartOffsets[middle] ?? 0) <= offset) low = middle;
|
|
21
|
+
else high = middle - 1;
|
|
22
|
+
}
|
|
23
|
+
return low;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Reads the extraction ignore directives of a source file:
|
|
27
|
+
*
|
|
28
|
+
* - `// intlayer-ignore-file` anywhere in the file skips the whole file
|
|
29
|
+
* - `// intlayer-ignore-next-line` skips the strings of the following line
|
|
30
|
+
* - `// intlayer-ignore-line` skips the strings of its own line
|
|
31
|
+
*
|
|
32
|
+
* Works for JS/TS (`//`, `/* *\/`), JSX (`{/* *\/}`) and Vue / Svelte
|
|
33
|
+
* templates (`<!-- -->`).
|
|
34
|
+
*/
|
|
35
|
+
const getExtractionIgnoreDirectives = (source) => {
|
|
36
|
+
const ignoredLineIndexes = /* @__PURE__ */ new Set();
|
|
37
|
+
let isFileIgnored = false;
|
|
38
|
+
let lineStartOffsets;
|
|
39
|
+
for (const match of source.matchAll(DIRECTIVE_REGEX)) {
|
|
40
|
+
const directive = match[1];
|
|
41
|
+
if (directive === "intlayer-ignore-file") {
|
|
42
|
+
isFileIgnored = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
lineStartOffsets ??= getLineStartOffsets(source);
|
|
46
|
+
const lineIndex = getLineIndex(lineStartOffsets, match.index);
|
|
47
|
+
ignoredLineIndexes.add(directive === "intlayer-ignore-next-line" ? lineIndex + 1 : lineIndex);
|
|
48
|
+
}
|
|
49
|
+
const isOffsetIgnored = (offset) => {
|
|
50
|
+
if (!lineStartOffsets || typeof offset !== "number") return false;
|
|
51
|
+
return ignoredLineIndexes.has(getLineIndex(lineStartOffsets, offset));
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
isFileIgnored,
|
|
55
|
+
isOffsetIgnored
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
exports.getExtractionIgnoreDirectives = getExtractionIgnoreDirectives;
|
|
60
|
+
//# sourceMappingURL=ignoreDirectives.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ignoreDirectives.cjs","names":[],"sources":["../../../../../../../../@intlayer/config/dist/esm/extract/ignoreDirectives.mjs"],"sourcesContent":["// src/extract/ignoreDirectives.ts\n/** Comment directive that keeps a whole file out of extraction. */\nconst IGNORE_FILE_DIRECTIVE = \"intlayer-ignore-file\";\n/** Comment directive that keeps the following line out of extraction. */\nconst IGNORE_NEXT_LINE_DIRECTIVE = \"intlayer-ignore-next-line\";\n/** Comment directive that keeps its own line out of extraction. */\nconst IGNORE_LINE_DIRECTIVE = \"intlayer-ignore-line\";\n/**\n* A directive only counts when it opens a comment (`//`, `/*`, `{/*` or\n* `<!--`), so the same words inside a translatable string are not mistaken\n* for one.\n*/\nconst DIRECTIVE_REGEX = /(?:\\/\\/|\\/\\*|<!--)\\s*(intlayer-ignore-(?:file|next-line|line))\\b/g;\n/** Offsets at which each line of the source starts (index 0 = line 1). */\nconst getLineStartOffsets = (source) => {\n\tconst lineStartOffsets = [0];\n\tfor (let index = 0; index < source.length; index++) if (source[index] === \"\\n\") lineStartOffsets.push(index + 1);\n\treturn lineStartOffsets;\n};\n/** Zero-based line index of a character offset, by binary search. */\nconst getLineIndex = (lineStartOffsets, offset) => {\n\tlet low = 0;\n\tlet high = lineStartOffsets.length - 1;\n\twhile (low < high) {\n\t\tconst middle = Math.ceil((low + high) / 2);\n\t\tif ((lineStartOffsets[middle] ?? 0) <= offset) low = middle;\n\t\telse high = middle - 1;\n\t}\n\treturn low;\n};\n/**\n* Reads the extraction ignore directives of a source file:\n*\n* - `// intlayer-ignore-file` anywhere in the file skips the whole file\n* - `// intlayer-ignore-next-line` skips the strings of the following line\n* - `// intlayer-ignore-line` skips the strings of its own line\n*\n* Works for JS/TS (`//`, `/* *\\/`), JSX (`{/* *\\/}`) and Vue / Svelte\n* templates (`<!-- -->`).\n*/\nconst getExtractionIgnoreDirectives = (source) => {\n\tconst ignoredLineIndexes = /* @__PURE__ */ new Set();\n\tlet isFileIgnored = false;\n\tlet lineStartOffsets;\n\tfor (const match of source.matchAll(DIRECTIVE_REGEX)) {\n\t\tconst directive = match[1];\n\t\tif (directive === \"intlayer-ignore-file\") {\n\t\t\tisFileIgnored = true;\n\t\t\tcontinue;\n\t\t}\n\t\tlineStartOffsets ??= getLineStartOffsets(source);\n\t\tconst lineIndex = getLineIndex(lineStartOffsets, match.index);\n\t\tignoredLineIndexes.add(directive === \"intlayer-ignore-next-line\" ? lineIndex + 1 : lineIndex);\n\t}\n\tconst isOffsetIgnored = (offset) => {\n\t\tif (!lineStartOffsets || typeof offset !== \"number\") return false;\n\t\treturn ignoredLineIndexes.has(getLineIndex(lineStartOffsets, offset));\n\t};\n\treturn {\n\t\tisFileIgnored,\n\t\tisOffsetIgnored\n\t};\n};\n\nexport { IGNORE_FILE_DIRECTIVE, IGNORE_LINE_DIRECTIVE, IGNORE_NEXT_LINE_DIRECTIVE, getExtractionIgnoreDirectives };\n//# sourceMappingURL=ignoreDirectives.mjs.map"],"mappings":";;;;;;AAYA,MAAM,kBAAkB;;AAExB,MAAM,uBAAuB,WAAW;CACvC,MAAM,mBAAmB,CAAC,CAAC;CAC3B,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,QAAQ,SAAS,IAAI,OAAO,WAAW,MAAM,iBAAiB,KAAK,QAAQ,CAAC;CAC/G,OAAO;AACR;;AAEA,MAAM,gBAAgB,kBAAkB,WAAW;CAClD,IAAI,MAAM;CACV,IAAI,OAAO,iBAAiB,SAAS;CACrC,OAAO,MAAM,MAAM;EAClB,MAAM,SAAS,KAAK,MAAM,MAAM,QAAQ,CAAC;EACzC,KAAK,iBAAiB,WAAW,MAAM,QAAQ,MAAM;OAChD,OAAO,SAAS;CACtB;CACA,OAAO;AACR;;;;;;;;;;;AAWA,MAAM,iCAAiC,WAAW;CACjD,MAAM,qCAAqC,IAAI,IAAI;CACnD,IAAI,gBAAgB;CACpB,IAAI;CACJ,KAAK,MAAM,SAAS,OAAO,SAAS,eAAe,GAAG;EACrD,MAAM,YAAY,MAAM;EACxB,IAAI,cAAc,wBAAwB;GACzC,gBAAgB;GAChB;EACD;EACA,qBAAqB,oBAAoB,MAAM;EAC/C,MAAM,YAAY,aAAa,kBAAkB,MAAM,KAAK;EAC5D,mBAAmB,IAAI,cAAc,8BAA8B,YAAY,IAAI,SAAS;CAC7F;CACA,MAAM,mBAAmB,WAAW;EACnC,IAAI,CAAC,oBAAoB,OAAO,WAAW,UAAU,OAAO;EAC5D,OAAO,mBAAmB,IAAI,aAAa,kBAAkB,MAAM,CAAC;CACrE;CACA,OAAO;EACN;EACA;CACD;AACD"}
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Checks whether the given text should be extracted as a translatable string.
|
|
4
4
|
*
|
|
5
|
+
* Proper nouns and brand names cannot be told apart from labels here; exclude
|
|
6
|
+
* them with the comment directives read by `getExtractionIgnoreDirectives`.
|
|
7
|
+
*
|
|
5
8
|
* Filters out:
|
|
6
9
|
* - Empty strings
|
|
7
10
|
* - Emails
|
|
8
|
-
* - Single capitalized words (likely brand/proper nouns, e.g. "Intlayer")
|
|
9
11
|
* - Uncapitalized strings of 2 words or fewer (likely technical terms)
|
|
10
12
|
* - Dynamic content patterns like Vue bindings (`v-`) or object patterns (`{`)
|
|
11
13
|
*/
|
|
@@ -23,7 +25,6 @@ const shouldExtract = (text) => {
|
|
|
23
25
|
if (tokens.every((token) => cssClassTokenRegex.test(token)) && tokens.some((token) => token.includes("-"))) return false;
|
|
24
26
|
}
|
|
25
27
|
const isCapitalized = /^['"([]*\p{Lu}/u.test(trimmed);
|
|
26
|
-
if (wordCount === 1 && isCapitalized) return false;
|
|
27
28
|
if (wordCount === 1) {
|
|
28
29
|
if (/[a-z]\p{Lu}/u.test(trimmed)) return false;
|
|
29
30
|
if (trimmed.includes("-") || trimmed.includes("_")) return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shouldExtract.cjs","names":[],"sources":["../../../../../../../../@intlayer/config/dist/esm/extract/shouldExtract.mjs"],"sourcesContent":["// src/extract/shouldExtract.ts\n/**\n* Checks whether the given text should be extracted as a translatable string.\n*\n* Filters out:\n* - Empty strings\n* - Emails\n* -
|
|
1
|
+
{"version":3,"file":"shouldExtract.cjs","names":[],"sources":["../../../../../../../../@intlayer/config/dist/esm/extract/shouldExtract.mjs"],"sourcesContent":["// src/extract/shouldExtract.ts\n/**\n* Checks whether the given text should be extracted as a translatable string.\n*\n* Proper nouns and brand names cannot be told apart from labels here; exclude\n* them with the comment directives read by `getExtractionIgnoreDirectives`.\n*\n* Filters out:\n* - Empty strings\n* - Emails\n* - Uncapitalized strings of 2 words or fewer (likely technical terms)\n* - Dynamic content patterns like Vue bindings (`v-`) or object patterns (`{`)\n*/\nconst shouldExtract = (text) => {\n\tconst trimmed = text.trim();\n\tif (!trimmed) return false;\n\tif (/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(trimmed)) return false;\n\tif (trimmed.startsWith(\"{\") && !trimmed.startsWith(\"{{\") || trimmed.startsWith(\"v-\")) return false;\n\tif (trimmed.includes(\"=>\") || trimmed.includes(\");\") || trimmed.includes(\"(){\") || trimmed.includes(\"==\") || trimmed.includes(\"window.\") || trimmed.startsWith(\"(function\") || trimmed.startsWith(\"function(\")) return false;\n\tif ((trimmed.match(/[^\\p{L}\\p{N}\\s.,!?;:'\"()[\\]{}–—/«»„“\\p{Sc}%&*+#@^_+=<>/~]/gu) || []).length > 5) return false;\n\tconst wordCount = trimmed.split(/\\s+/).length;\n\tconst cssClassTokenRegex = /^!?([a-z][a-z0-9-]*:)*[a-z][a-z0-9]*(-[a-z0-9[\\].,%#/]+)*(\\/[a-z0-9]+)?$/;\n\tif (wordCount > 1) {\n\t\tconst tokens = trimmed.split(/\\s+/);\n\t\tif (tokens.every((token) => cssClassTokenRegex.test(token)) && tokens.some((token) => token.includes(\"-\"))) return false;\n\t}\n\tconst isCapitalized = /^['\"([]*\\p{Lu}/u.test(trimmed);\n\tif (wordCount === 1) {\n\t\tif (/[a-z]\\p{Lu}/u.test(trimmed)) return false;\n\t\tif (trimmed.includes(\"-\") || trimmed.includes(\"_\")) return false;\n\t}\n\tif (!isCapitalized && wordCount <= 2) return false;\n\treturn true;\n};\n\nexport { shouldExtract };\n//# sourceMappingURL=shouldExtract.mjs.map"],"mappings":";;;;;;;;;;;;;AAaA,MAAM,iBAAiB,SAAS;CAC/B,MAAM,UAAU,KAAK,KAAK;CAC1B,IAAI,CAAC,SAAS,OAAO;CACrB,IAAI,6BAA6B,KAAK,OAAO,GAAG,OAAO;CACvD,IAAI,QAAQ,WAAW,GAAG,KAAK,CAAC,QAAQ,WAAW,IAAI,KAAK,QAAQ,WAAW,IAAI,GAAG,OAAO;CAC7F,IAAI,QAAQ,SAAS,IAAI,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,SAAS,KAAK,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,SAAS,SAAS,KAAK,QAAQ,WAAW,WAAW,KAAK,QAAQ,WAAW,WAAW,GAAG,OAAO;CACvN,KAAK,QAAQ,MAAM,6DAA6D,KAAK,CAAC,EAAC,CAAE,SAAS,GAAG,OAAO;CAC5G,MAAM,YAAY,QAAQ,MAAM,KAAK,CAAC,CAAC;CACvC,MAAM,qBAAqB;CAC3B,IAAI,YAAY,GAAG;EAClB,MAAM,SAAS,QAAQ,MAAM,KAAK;EAClC,IAAI,OAAO,OAAO,UAAU,mBAAmB,KAAK,KAAK,CAAC,KAAK,OAAO,MAAM,UAAU,MAAM,SAAS,GAAG,CAAC,GAAG,OAAO;CACpH;CACA,MAAM,gBAAgB,kBAAkB,KAAK,OAAO;CACpD,IAAI,cAAc,GAAG;EACpB,IAAI,eAAe,KAAK,OAAO,GAAG,OAAO;EACzC,IAAI,QAAQ,SAAS,GAAG,KAAK,QAAQ,SAAS,GAAG,GAAG,OAAO;CAC5D;CACA,IAAI,CAAC,iBAAiB,aAAa,GAAG,OAAO;CAC7C,OAAO;AACR"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
const require_constants = require("../@intlayer/config/dist/esm/extract/constants.cjs");
|
|
3
|
+
const require_ignoreDirectives = require("../@intlayer/config/dist/esm/extract/ignoreDirectives.cjs");
|
|
3
4
|
const require_shouldExtract = require("../@intlayer/config/dist/esm/extract/shouldExtract.cjs");
|
|
4
5
|
const require_utils_ast = require("../utils/ast.cjs");
|
|
5
6
|
const require_utils_createRule = require("../utils/createRule.cjs");
|
|
@@ -28,6 +29,22 @@ const isContentDeclarationFile = (filename) => _intlayer_config_defaultValues.FI
|
|
|
28
29
|
*
|
|
29
30
|
* @param node - The reported node.
|
|
30
31
|
*/
|
|
32
|
+
/** Start offset of an ESLint node (`range[0]`), when it has one. */
|
|
33
|
+
const getNodeStart = (node) => {
|
|
34
|
+
const range = node?.["range"];
|
|
35
|
+
return Array.isArray(range) && typeof range[0] === "number" ? range[0] : void 0;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Start offset of the element a text, `{'literal'}` child or attribute
|
|
39
|
+
* belongs to (JSX, Vue and Svelte trees alike).
|
|
40
|
+
*/
|
|
41
|
+
const getEnclosingElementStart = (node) => {
|
|
42
|
+
let current = node.parent;
|
|
43
|
+
if (current?.type === "JSXExpressionContainer") current = current.parent;
|
|
44
|
+
if (current?.type === "JSXAttribute") current = current.parent;
|
|
45
|
+
if (current?.type === "JSXOpeningElement") current = current.parent;
|
|
46
|
+
return getNodeStart(current);
|
|
47
|
+
};
|
|
31
48
|
const getEnclosingElementName = (node) => {
|
|
32
49
|
let current = node;
|
|
33
50
|
while (current) {
|
|
@@ -93,6 +110,20 @@ const noRawText = require_utils_createRule.createRule("no-raw-text", {
|
|
|
93
110
|
create: (context) => {
|
|
94
111
|
const filename = context.filename;
|
|
95
112
|
if (isContentDeclarationFile(filename)) return {};
|
|
113
|
+
const sourceText = context.sourceCode.text;
|
|
114
|
+
const ignoreDirectives = require_ignoreDirectives.getExtractionIgnoreDirectives(sourceText);
|
|
115
|
+
if (ignoreDirectives.isFileIgnored) return {};
|
|
116
|
+
/**
|
|
117
|
+
* Whether an `intlayer-ignore-line` / `intlayer-ignore-next-line`
|
|
118
|
+
* directive covers the node: on the line where its text starts, or on
|
|
119
|
+
* the opening tag of its element.
|
|
120
|
+
*/
|
|
121
|
+
const isIgnoredNode = (node) => {
|
|
122
|
+
const astNode = require_utils_ast.toAstNode(node);
|
|
123
|
+
let textStart = getNodeStart(astNode);
|
|
124
|
+
if (textStart !== void 0) while (/\s/u.test(sourceText[textStart] ?? "")) textStart++;
|
|
125
|
+
return ignoreDirectives.isOffsetIgnored(textStart) || ignoreDirectives.isOffsetIgnored(getEnclosingElementStart(astNode));
|
|
126
|
+
};
|
|
96
127
|
const [options = {}] = context.options;
|
|
97
128
|
const attributes = new Set(options.attributes ?? require_constants.ATTRIBUTES_TO_EXTRACT);
|
|
98
129
|
const ignoredElements = new Set(options.ignoreElements ?? DEFAULT_IGNORED_ELEMENTS);
|
|
@@ -115,6 +146,7 @@ const noRawText = require_utils_createRule.createRule("no-raw-text", {
|
|
|
115
146
|
if (elementName && ignoredElements.has(elementName)) return;
|
|
116
147
|
if (attributeName !== null && !attributes.has(attributeName)) return;
|
|
117
148
|
if (!isReportableText(text)) return;
|
|
149
|
+
if (isIgnoredNode(node)) return;
|
|
118
150
|
context.report({
|
|
119
151
|
node,
|
|
120
152
|
messageId: attributeName === null ? "rawText" : "rawAttribute",
|
|
@@ -131,6 +163,7 @@ const noRawText = require_utils_createRule.createRule("no-raw-text", {
|
|
|
131
163
|
if (!isReportableText(text)) return;
|
|
132
164
|
const elementName = getEnclosingElementName(astNode.parent);
|
|
133
165
|
if (elementName && ignoredElements.has(elementName)) return;
|
|
166
|
+
if (isIgnoredNode(node)) return;
|
|
134
167
|
context.report({
|
|
135
168
|
node,
|
|
136
169
|
messageId: "rawText",
|
|
@@ -146,6 +179,7 @@ const noRawText = require_utils_createRule.createRule("no-raw-text", {
|
|
|
146
179
|
if (text === null || !isReportableText(text)) return;
|
|
147
180
|
const elementName = getEnclosingElementName(astNode.parent);
|
|
148
181
|
if (elementName && ignoredElements.has(elementName)) return;
|
|
182
|
+
if (isIgnoredNode(node)) return;
|
|
149
183
|
context.report({
|
|
150
184
|
node,
|
|
151
185
|
messageId: "rawAttribute",
|
|
@@ -162,6 +196,7 @@ const noRawText = require_utils_createRule.createRule("no-raw-text", {
|
|
|
162
196
|
if (isStructuralPosition(astNode)) return;
|
|
163
197
|
const text = astNode["value"];
|
|
164
198
|
if (!isReportableText(text)) return;
|
|
199
|
+
if (isIgnoredNode(node)) return;
|
|
165
200
|
context.report({
|
|
166
201
|
node,
|
|
167
202
|
messageId: "rawStringLiteral",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noRawText.cjs","names":["FILE_EXTENSIONS","getJsxElementName","createRule","ATTRIBUTES_TO_EXTRACT","shouldExtract","createTemplateVisitor","combineVisitors","toAstNode","getJsxAttributeName","getJsxAttributeValueNode","getStaticStringValue"],"sources":["../../../src/rules/noRawText.ts"],"sourcesContent":["import { FILE_EXTENSIONS } from '@intlayer/config/defaultValues';\nimport { ATTRIBUTES_TO_EXTRACT, shouldExtract } from '@intlayer/config/extract';\nimport {\n type AstNode,\n type AstNodeWithParent,\n getJsxAttributeName,\n getJsxAttributeValueNode,\n getJsxElementName,\n getStaticStringValue,\n toAstNode,\n} from '../utils/ast';\nimport { createRule } from '../utils/createRule';\nimport {\n combineVisitors,\n createTemplateVisitor,\n type TemplateVisitor,\n} from '../utils/templates';\n\n/**\n * Elements whose text is markup, code or metadata rather than user-facing copy.\n * Extracting them would be wrong, so they are skipped by default.\n */\nconst DEFAULT_IGNORED_ELEMENTS = ['code', 'pre', 'script', 'style'];\n\nexport type NoRawTextOptions = {\n /**\n * JSX attributes whose string value is user-facing copy.\n * Defaults to the compiler's own list, so the linter flags exactly what\n * `intlayer extract` would rewrite.\n */\n attributes?: string[];\n /** JSX elements whose children are never translatable. */\n ignoreElements?: string[];\n /** Regular-expression sources for text that should never be reported. */\n ignorePatterns?: string[];\n /**\n * Also report string literals outside JSX (function arguments, object values,\n * …). Off by default: outside JSX the false-positive rate is much higher, and\n * the compiler's own extractor is the better first pass.\n */\n includeStringLiterals?: boolean;\n};\n\ntype MessageIds = 'rawText' | 'rawAttribute' | 'rawStringLiteral';\n\n/**\n * True for a content declaration file, where raw strings are the whole point.\n *\n * @param filename - Absolute path of the linted file.\n */\nconst isContentDeclarationFile = (filename: string): boolean =>\n FILE_EXTENSIONS.some((extension) => filename.endsWith(extension));\n\n/**\n * Walk up from a JSX child or attribute to the tag name that encloses it.\n *\n * @param node - The reported node.\n */\nconst getEnclosingElementName = (\n node: AstNodeWithParent | undefined\n): string | null => {\n let current = node;\n\n while (current) {\n if (current.type === 'JSXElement') {\n return getJsxElementName(current['openingElement'] as AstNode);\n }\n\n if (current.type === 'JSXOpeningElement') {\n return getJsxElementName(current);\n }\n\n current = current.parent;\n }\n\n return null;\n};\n\n/**\n * True when the string literal sits in a position that is structural rather\n * than user-facing: module specifiers, property keys, directives, and the\n * arguments of the Intlayer getters themselves.\n *\n * @param node - The Literal node.\n */\nconst isStructuralPosition = (node: AstNodeWithParent): boolean => {\n const parent = node.parent;\n\n if (!parent) return true;\n\n switch (parent.type) {\n case 'ImportDeclaration':\n case 'ImportExpression':\n case 'ExportNamedDeclaration':\n case 'ExportAllDeclaration':\n case 'JSXAttribute':\n case 'TSLiteralType':\n case 'TSEnumMember':\n case 'ExpressionStatement':\n return true;\n\n case 'Property':\n // Keys are structural; values are not.\n return parent['key'] === node && parent['computed'] !== true;\n\n case 'MemberExpression':\n return parent['property'] === node;\n\n default:\n return false;\n }\n};\n\nexport const noRawText = createRule<[NoRawTextOptions?], MessageIds>(\n 'no-raw-text',\n {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Report user-facing strings that are not declared in an Intlayer dictionary',\n },\n messages: {\n rawText:\n 'Untranslated text \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawAttribute:\n 'Untranslated `{{attribute}}` value \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawStringLiteral:\n 'Untranslated string \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n attributes: { type: 'array', items: { type: 'string' } },\n ignoreElements: { type: 'array', items: { type: 'string' } },\n ignorePatterns: { type: 'array', items: { type: 'string' } },\n includeStringLiterals: { type: 'boolean' },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create: (context) => {\n const filename = context.filename;\n\n // Content declaration files hold the translations themselves.\n if (isContentDeclarationFile(filename)) return {};\n\n const [options = {}] = context.options;\n const attributes = new Set<string>(\n options.attributes ?? ATTRIBUTES_TO_EXTRACT\n );\n const ignoredElements = new Set<string>(\n options.ignoreElements ?? DEFAULT_IGNORED_ELEMENTS\n );\n const ignorePatterns = (options.ignorePatterns ?? []).map(\n (pattern) => new RegExp(pattern, 'u')\n );\n const includeStringLiterals = options.includeStringLiterals === true;\n\n /** True when the text is copy the extractor would pick up. */\n const isReportableText = (text: string): boolean =>\n shouldExtract(text) &&\n !ignorePatterns.some((pattern) => pattern.test(text.trim()));\n\n /** Collapse whitespace so multi-line JSX text reads well in the report. */\n const formatText = (text: string): string => {\n const normalized = text.replace(/\\s+/gu, ' ').trim();\n\n return normalized.length > 40\n ? `${normalized.slice(0, 40)}…`\n : normalized;\n };\n\n /**\n * Handlers for the markup languages that are not JSX — Vue, Svelte and\n * Angular templates. Reports through the same predicate and options as\n * the JSX path so a project gets identical results whatever it is written\n * in.\n */\n const templateVisitor = createTemplateVisitor(\n ({ node, text, attributeName, elementName }) => {\n if (elementName && ignoredElements.has(elementName)) return;\n\n if (attributeName !== null && !attributes.has(attributeName)) return;\n\n if (!isReportableText(text)) return;\n\n context.report({\n node: node as never,\n messageId: attributeName === null ? 'rawText' : 'rawAttribute',\n data: {\n text: formatText(text),\n ...(attributeName === null ? {} : { attribute: attributeName }),\n },\n });\n }\n );\n\n const scriptVisitor: TemplateVisitor = {\n JSXText: (node) => {\n const astNode = toAstNode(node as never);\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawText',\n data: { text: formatText(text) },\n });\n },\n\n JSXAttribute: (node) => {\n const astNode = toAstNode(node as never);\n const attributeName = getJsxAttributeName(astNode);\n\n if (!attributeName || !attributes.has(attributeName)) return;\n\n const valueNode = getJsxAttributeValueNode(astNode);\n const text = getStaticStringValue(valueNode);\n\n if (text === null || !isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawAttribute',\n data: { attribute: attributeName, text: formatText(text) },\n });\n },\n\n Literal: (node) => {\n if (!includeStringLiterals) return;\n\n const astNode = toAstNode(node as never);\n\n if (typeof astNode['value'] !== 'string') return;\n\n if (isStructuralPosition(astNode)) return;\n\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawStringLiteral',\n data: { text: formatText(text) },\n });\n },\n };\n\n return combineVisitors(\n context.sourceCode.parserServices,\n scriptVisitor,\n templateVisitor\n );\n },\n }\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"noRawText.cjs","names":["FILE_EXTENSIONS","getJsxElementName","createRule","getExtractionIgnoreDirectives","toAstNode","ATTRIBUTES_TO_EXTRACT","shouldExtract","createTemplateVisitor","combineVisitors","getJsxAttributeName","getJsxAttributeValueNode","getStaticStringValue"],"sources":["../../../src/rules/noRawText.ts"],"sourcesContent":["import { FILE_EXTENSIONS } from '@intlayer/config/defaultValues';\nimport {\n ATTRIBUTES_TO_EXTRACT,\n getExtractionIgnoreDirectives,\n shouldExtract,\n} from '@intlayer/config/extract';\nimport {\n type AstNode,\n type AstNodeWithParent,\n getJsxAttributeName,\n getJsxAttributeValueNode,\n getJsxElementName,\n getStaticStringValue,\n toAstNode,\n} from '../utils/ast';\nimport { createRule } from '../utils/createRule';\nimport {\n combineVisitors,\n createTemplateVisitor,\n type TemplateVisitor,\n} from '../utils/templates';\n\n/**\n * Elements whose text is markup, code or metadata rather than user-facing copy.\n * Extracting them would be wrong, so they are skipped by default.\n */\nconst DEFAULT_IGNORED_ELEMENTS = ['code', 'pre', 'script', 'style'];\n\nexport type NoRawTextOptions = {\n /**\n * JSX attributes whose string value is user-facing copy.\n * Defaults to the compiler's own list, so the linter flags exactly what\n * `intlayer extract` would rewrite.\n */\n attributes?: string[];\n /** JSX elements whose children are never translatable. */\n ignoreElements?: string[];\n /** Regular-expression sources for text that should never be reported. */\n ignorePatterns?: string[];\n /**\n * Also report string literals outside JSX (function arguments, object values,\n * …). Off by default: outside JSX the false-positive rate is much higher, and\n * the compiler's own extractor is the better first pass.\n */\n includeStringLiterals?: boolean;\n};\n\ntype MessageIds = 'rawText' | 'rawAttribute' | 'rawStringLiteral';\n\n/**\n * True for a content declaration file, where raw strings are the whole point.\n *\n * @param filename - Absolute path of the linted file.\n */\nconst isContentDeclarationFile = (filename: string): boolean =>\n FILE_EXTENSIONS.some((extension) => filename.endsWith(extension));\n\n/**\n * Walk up from a JSX child or attribute to the tag name that encloses it.\n *\n * @param node - The reported node.\n */\n/** Start offset of an ESLint node (`range[0]`), when it has one. */\nconst getNodeStart = (node: AstNode | undefined): number | undefined => {\n const range = node?.['range'];\n\n return Array.isArray(range) && typeof range[0] === 'number'\n ? range[0]\n : undefined;\n};\n\n/**\n * Start offset of the element a text, `{'literal'}` child or attribute\n * belongs to (JSX, Vue and Svelte trees alike).\n */\nconst getEnclosingElementStart = (\n node: AstNodeWithParent\n): number | undefined => {\n let current = node.parent;\n\n if (current?.type === 'JSXExpressionContainer') current = current.parent;\n if (current?.type === 'JSXAttribute') current = current.parent;\n if (current?.type === 'JSXOpeningElement') current = current.parent;\n\n return getNodeStart(current);\n};\n\nconst getEnclosingElementName = (\n node: AstNodeWithParent | undefined\n): string | null => {\n let current = node;\n\n while (current) {\n if (current.type === 'JSXElement') {\n return getJsxElementName(current['openingElement'] as AstNode);\n }\n\n if (current.type === 'JSXOpeningElement') {\n return getJsxElementName(current);\n }\n\n current = current.parent;\n }\n\n return null;\n};\n\n/**\n * True when the string literal sits in a position that is structural rather\n * than user-facing: module specifiers, property keys, directives, and the\n * arguments of the Intlayer getters themselves.\n *\n * @param node - The Literal node.\n */\nconst isStructuralPosition = (node: AstNodeWithParent): boolean => {\n const parent = node.parent;\n\n if (!parent) return true;\n\n switch (parent.type) {\n case 'ImportDeclaration':\n case 'ImportExpression':\n case 'ExportNamedDeclaration':\n case 'ExportAllDeclaration':\n case 'JSXAttribute':\n case 'TSLiteralType':\n case 'TSEnumMember':\n case 'ExpressionStatement':\n return true;\n\n case 'Property':\n // Keys are structural; values are not.\n return parent['key'] === node && parent['computed'] !== true;\n\n case 'MemberExpression':\n return parent['property'] === node;\n\n default:\n return false;\n }\n};\n\nexport const noRawText = createRule<[NoRawTextOptions?], MessageIds>(\n 'no-raw-text',\n {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Report user-facing strings that are not declared in an Intlayer dictionary',\n },\n messages: {\n rawText:\n 'Untranslated text \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawAttribute:\n 'Untranslated `{{attribute}}` value \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawStringLiteral:\n 'Untranslated string \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n attributes: { type: 'array', items: { type: 'string' } },\n ignoreElements: { type: 'array', items: { type: 'string' } },\n ignorePatterns: { type: 'array', items: { type: 'string' } },\n includeStringLiterals: { type: 'boolean' },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create: (context) => {\n const filename = context.filename;\n\n // Content declaration files hold the translations themselves.\n if (isContentDeclarationFile(filename)) return {};\n\n const sourceText = context.sourceCode.text;\n const ignoreDirectives = getExtractionIgnoreDirectives(sourceText);\n\n // Same comment directives as the compiler: what it skips is not reported.\n if (ignoreDirectives.isFileIgnored) return {};\n\n /**\n * Whether an `intlayer-ignore-line` / `intlayer-ignore-next-line`\n * directive covers the node: on the line where its text starts, or on\n * the opening tag of its element.\n */\n const isIgnoredNode = (node: unknown): boolean => {\n const astNode = toAstNode(node as never);\n let textStart = getNodeStart(astNode);\n\n if (textStart !== undefined) {\n while (/\\s/u.test(sourceText[textStart] ?? '')) textStart++;\n }\n\n return (\n ignoreDirectives.isOffsetIgnored(textStart) ||\n ignoreDirectives.isOffsetIgnored(getEnclosingElementStart(astNode))\n );\n };\n\n const [options = {}] = context.options;\n const attributes = new Set<string>(\n options.attributes ?? ATTRIBUTES_TO_EXTRACT\n );\n const ignoredElements = new Set<string>(\n options.ignoreElements ?? DEFAULT_IGNORED_ELEMENTS\n );\n const ignorePatterns = (options.ignorePatterns ?? []).map(\n (pattern) => new RegExp(pattern, 'u')\n );\n const includeStringLiterals = options.includeStringLiterals === true;\n\n /** True when the text is copy the extractor would pick up. */\n const isReportableText = (text: string): boolean =>\n shouldExtract(text) &&\n !ignorePatterns.some((pattern) => pattern.test(text.trim()));\n\n /** Collapse whitespace so multi-line JSX text reads well in the report. */\n const formatText = (text: string): string => {\n const normalized = text.replace(/\\s+/gu, ' ').trim();\n\n return normalized.length > 40\n ? `${normalized.slice(0, 40)}…`\n : normalized;\n };\n\n /**\n * Handlers for the markup languages that are not JSX — Vue, Svelte and\n * Angular templates. Reports through the same predicate and options as\n * the JSX path so a project gets identical results whatever it is written\n * in.\n */\n const templateVisitor = createTemplateVisitor(\n ({ node, text, attributeName, elementName }) => {\n if (elementName && ignoredElements.has(elementName)) return;\n\n if (attributeName !== null && !attributes.has(attributeName)) return;\n\n if (!isReportableText(text)) return;\n\n if (isIgnoredNode(node)) return;\n\n context.report({\n node: node as never,\n messageId: attributeName === null ? 'rawText' : 'rawAttribute',\n data: {\n text: formatText(text),\n ...(attributeName === null ? {} : { attribute: attributeName }),\n },\n });\n }\n );\n\n const scriptVisitor: TemplateVisitor = {\n JSXText: (node) => {\n const astNode = toAstNode(node as never);\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n if (isIgnoredNode(node)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawText',\n data: { text: formatText(text) },\n });\n },\n\n JSXAttribute: (node) => {\n const astNode = toAstNode(node as never);\n const attributeName = getJsxAttributeName(astNode);\n\n if (!attributeName || !attributes.has(attributeName)) return;\n\n const valueNode = getJsxAttributeValueNode(astNode);\n const text = getStaticStringValue(valueNode);\n\n if (text === null || !isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n if (isIgnoredNode(node)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawAttribute',\n data: { attribute: attributeName, text: formatText(text) },\n });\n },\n\n Literal: (node) => {\n if (!includeStringLiterals) return;\n\n const astNode = toAstNode(node as never);\n\n if (typeof astNode['value'] !== 'string') return;\n\n if (isStructuralPosition(astNode)) return;\n\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n if (isIgnoredNode(node)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawStringLiteral',\n data: { text: formatText(text) },\n });\n },\n };\n\n return combineVisitors(\n context.sourceCode.parserServices,\n scriptVisitor,\n templateVisitor\n );\n },\n }\n);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// ../@intlayer/config/dist/esm/extract/ignoreDirectives.mjs
|
|
2
|
+
/**
|
|
3
|
+
* A directive only counts when it opens a comment (`//`, `/*`, `{/*` or
|
|
4
|
+
* `<!--`), so the same words inside a translatable string are not mistaken
|
|
5
|
+
* for one.
|
|
6
|
+
*/
|
|
7
|
+
const DIRECTIVE_REGEX = /(?:\/\/|\/\*|<!--)\s*(intlayer-ignore-(?:file|next-line|line))\b/g;
|
|
8
|
+
/** Offsets at which each line of the source starts (index 0 = line 1). */
|
|
9
|
+
const getLineStartOffsets = (source) => {
|
|
10
|
+
const lineStartOffsets = [0];
|
|
11
|
+
for (let index = 0; index < source.length; index++) if (source[index] === "\n") lineStartOffsets.push(index + 1);
|
|
12
|
+
return lineStartOffsets;
|
|
13
|
+
};
|
|
14
|
+
/** Zero-based line index of a character offset, by binary search. */
|
|
15
|
+
const getLineIndex = (lineStartOffsets, offset) => {
|
|
16
|
+
let low = 0;
|
|
17
|
+
let high = lineStartOffsets.length - 1;
|
|
18
|
+
while (low < high) {
|
|
19
|
+
const middle = Math.ceil((low + high) / 2);
|
|
20
|
+
if ((lineStartOffsets[middle] ?? 0) <= offset) low = middle;
|
|
21
|
+
else high = middle - 1;
|
|
22
|
+
}
|
|
23
|
+
return low;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Reads the extraction ignore directives of a source file:
|
|
27
|
+
*
|
|
28
|
+
* - `// intlayer-ignore-file` anywhere in the file skips the whole file
|
|
29
|
+
* - `// intlayer-ignore-next-line` skips the strings of the following line
|
|
30
|
+
* - `// intlayer-ignore-line` skips the strings of its own line
|
|
31
|
+
*
|
|
32
|
+
* Works for JS/TS (`//`, `/* *\/`), JSX (`{/* *\/}`) and Vue / Svelte
|
|
33
|
+
* templates (`<!-- -->`).
|
|
34
|
+
*/
|
|
35
|
+
const getExtractionIgnoreDirectives = (source) => {
|
|
36
|
+
const ignoredLineIndexes = /* @__PURE__ */ new Set();
|
|
37
|
+
let isFileIgnored = false;
|
|
38
|
+
let lineStartOffsets;
|
|
39
|
+
for (const match of source.matchAll(DIRECTIVE_REGEX)) {
|
|
40
|
+
const directive = match[1];
|
|
41
|
+
if (directive === "intlayer-ignore-file") {
|
|
42
|
+
isFileIgnored = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
lineStartOffsets ??= getLineStartOffsets(source);
|
|
46
|
+
const lineIndex = getLineIndex(lineStartOffsets, match.index);
|
|
47
|
+
ignoredLineIndexes.add(directive === "intlayer-ignore-next-line" ? lineIndex + 1 : lineIndex);
|
|
48
|
+
}
|
|
49
|
+
const isOffsetIgnored = (offset) => {
|
|
50
|
+
if (!lineStartOffsets || typeof offset !== "number") return false;
|
|
51
|
+
return ignoredLineIndexes.has(getLineIndex(lineStartOffsets, offset));
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
isFileIgnored,
|
|
55
|
+
isOffsetIgnored
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export { getExtractionIgnoreDirectives };
|
|
60
|
+
//# sourceMappingURL=ignoreDirectives.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ignoreDirectives.mjs","names":[],"sources":["../../../../../../../../@intlayer/config/dist/esm/extract/ignoreDirectives.mjs"],"sourcesContent":["// src/extract/ignoreDirectives.ts\n/** Comment directive that keeps a whole file out of extraction. */\nconst IGNORE_FILE_DIRECTIVE = \"intlayer-ignore-file\";\n/** Comment directive that keeps the following line out of extraction. */\nconst IGNORE_NEXT_LINE_DIRECTIVE = \"intlayer-ignore-next-line\";\n/** Comment directive that keeps its own line out of extraction. */\nconst IGNORE_LINE_DIRECTIVE = \"intlayer-ignore-line\";\n/**\n* A directive only counts when it opens a comment (`//`, `/*`, `{/*` or\n* `<!--`), so the same words inside a translatable string are not mistaken\n* for one.\n*/\nconst DIRECTIVE_REGEX = /(?:\\/\\/|\\/\\*|<!--)\\s*(intlayer-ignore-(?:file|next-line|line))\\b/g;\n/** Offsets at which each line of the source starts (index 0 = line 1). */\nconst getLineStartOffsets = (source) => {\n\tconst lineStartOffsets = [0];\n\tfor (let index = 0; index < source.length; index++) if (source[index] === \"\\n\") lineStartOffsets.push(index + 1);\n\treturn lineStartOffsets;\n};\n/** Zero-based line index of a character offset, by binary search. */\nconst getLineIndex = (lineStartOffsets, offset) => {\n\tlet low = 0;\n\tlet high = lineStartOffsets.length - 1;\n\twhile (low < high) {\n\t\tconst middle = Math.ceil((low + high) / 2);\n\t\tif ((lineStartOffsets[middle] ?? 0) <= offset) low = middle;\n\t\telse high = middle - 1;\n\t}\n\treturn low;\n};\n/**\n* Reads the extraction ignore directives of a source file:\n*\n* - `// intlayer-ignore-file` anywhere in the file skips the whole file\n* - `// intlayer-ignore-next-line` skips the strings of the following line\n* - `// intlayer-ignore-line` skips the strings of its own line\n*\n* Works for JS/TS (`//`, `/* *\\/`), JSX (`{/* *\\/}`) and Vue / Svelte\n* templates (`<!-- -->`).\n*/\nconst getExtractionIgnoreDirectives = (source) => {\n\tconst ignoredLineIndexes = /* @__PURE__ */ new Set();\n\tlet isFileIgnored = false;\n\tlet lineStartOffsets;\n\tfor (const match of source.matchAll(DIRECTIVE_REGEX)) {\n\t\tconst directive = match[1];\n\t\tif (directive === \"intlayer-ignore-file\") {\n\t\t\tisFileIgnored = true;\n\t\t\tcontinue;\n\t\t}\n\t\tlineStartOffsets ??= getLineStartOffsets(source);\n\t\tconst lineIndex = getLineIndex(lineStartOffsets, match.index);\n\t\tignoredLineIndexes.add(directive === \"intlayer-ignore-next-line\" ? lineIndex + 1 : lineIndex);\n\t}\n\tconst isOffsetIgnored = (offset) => {\n\t\tif (!lineStartOffsets || typeof offset !== \"number\") return false;\n\t\treturn ignoredLineIndexes.has(getLineIndex(lineStartOffsets, offset));\n\t};\n\treturn {\n\t\tisFileIgnored,\n\t\tisOffsetIgnored\n\t};\n};\n\nexport { IGNORE_FILE_DIRECTIVE, IGNORE_LINE_DIRECTIVE, IGNORE_NEXT_LINE_DIRECTIVE, getExtractionIgnoreDirectives };\n//# sourceMappingURL=ignoreDirectives.mjs.map"],"mappings":";;;;;;AAYA,MAAM,kBAAkB;;AAExB,MAAM,uBAAuB,WAAW;CACvC,MAAM,mBAAmB,CAAC,CAAC;CAC3B,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,QAAQ,SAAS,IAAI,OAAO,WAAW,MAAM,iBAAiB,KAAK,QAAQ,CAAC;CAC/G,OAAO;AACR;;AAEA,MAAM,gBAAgB,kBAAkB,WAAW;CAClD,IAAI,MAAM;CACV,IAAI,OAAO,iBAAiB,SAAS;CACrC,OAAO,MAAM,MAAM;EAClB,MAAM,SAAS,KAAK,MAAM,MAAM,QAAQ,CAAC;EACzC,KAAK,iBAAiB,WAAW,MAAM,QAAQ,MAAM;OAChD,OAAO,SAAS;CACtB;CACA,OAAO;AACR;;;;;;;;;;;AAWA,MAAM,iCAAiC,WAAW;CACjD,MAAM,qCAAqC,IAAI,IAAI;CACnD,IAAI,gBAAgB;CACpB,IAAI;CACJ,KAAK,MAAM,SAAS,OAAO,SAAS,eAAe,GAAG;EACrD,MAAM,YAAY,MAAM;EACxB,IAAI,cAAc,wBAAwB;GACzC,gBAAgB;GAChB;EACD;EACA,qBAAqB,oBAAoB,MAAM;EAC/C,MAAM,YAAY,aAAa,kBAAkB,MAAM,KAAK;EAC5D,mBAAmB,IAAI,cAAc,8BAA8B,YAAY,IAAI,SAAS;CAC7F;CACA,MAAM,mBAAmB,WAAW;EACnC,IAAI,CAAC,oBAAoB,OAAO,WAAW,UAAU,OAAO;EAC5D,OAAO,mBAAmB,IAAI,aAAa,kBAAkB,MAAM,CAAC;CACrE;CACA,OAAO;EACN;EACA;CACD;AACD"}
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Checks whether the given text should be extracted as a translatable string.
|
|
4
4
|
*
|
|
5
|
+
* Proper nouns and brand names cannot be told apart from labels here; exclude
|
|
6
|
+
* them with the comment directives read by `getExtractionIgnoreDirectives`.
|
|
7
|
+
*
|
|
5
8
|
* Filters out:
|
|
6
9
|
* - Empty strings
|
|
7
10
|
* - Emails
|
|
8
|
-
* - Single capitalized words (likely brand/proper nouns, e.g. "Intlayer")
|
|
9
11
|
* - Uncapitalized strings of 2 words or fewer (likely technical terms)
|
|
10
12
|
* - Dynamic content patterns like Vue bindings (`v-`) or object patterns (`{`)
|
|
11
13
|
*/
|
|
@@ -23,7 +25,6 @@ const shouldExtract = (text) => {
|
|
|
23
25
|
if (tokens.every((token) => cssClassTokenRegex.test(token)) && tokens.some((token) => token.includes("-"))) return false;
|
|
24
26
|
}
|
|
25
27
|
const isCapitalized = /^['"([]*\p{Lu}/u.test(trimmed);
|
|
26
|
-
if (wordCount === 1 && isCapitalized) return false;
|
|
27
28
|
if (wordCount === 1) {
|
|
28
29
|
if (/[a-z]\p{Lu}/u.test(trimmed)) return false;
|
|
29
30
|
if (trimmed.includes("-") || trimmed.includes("_")) return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shouldExtract.mjs","names":[],"sources":["../../../../../../../../@intlayer/config/dist/esm/extract/shouldExtract.mjs"],"sourcesContent":["// src/extract/shouldExtract.ts\n/**\n* Checks whether the given text should be extracted as a translatable string.\n*\n* Filters out:\n* - Empty strings\n* - Emails\n* -
|
|
1
|
+
{"version":3,"file":"shouldExtract.mjs","names":[],"sources":["../../../../../../../../@intlayer/config/dist/esm/extract/shouldExtract.mjs"],"sourcesContent":["// src/extract/shouldExtract.ts\n/**\n* Checks whether the given text should be extracted as a translatable string.\n*\n* Proper nouns and brand names cannot be told apart from labels here; exclude\n* them with the comment directives read by `getExtractionIgnoreDirectives`.\n*\n* Filters out:\n* - Empty strings\n* - Emails\n* - Uncapitalized strings of 2 words or fewer (likely technical terms)\n* - Dynamic content patterns like Vue bindings (`v-`) or object patterns (`{`)\n*/\nconst shouldExtract = (text) => {\n\tconst trimmed = text.trim();\n\tif (!trimmed) return false;\n\tif (/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(trimmed)) return false;\n\tif (trimmed.startsWith(\"{\") && !trimmed.startsWith(\"{{\") || trimmed.startsWith(\"v-\")) return false;\n\tif (trimmed.includes(\"=>\") || trimmed.includes(\");\") || trimmed.includes(\"(){\") || trimmed.includes(\"==\") || trimmed.includes(\"window.\") || trimmed.startsWith(\"(function\") || trimmed.startsWith(\"function(\")) return false;\n\tif ((trimmed.match(/[^\\p{L}\\p{N}\\s.,!?;:'\"()[\\]{}–—/«»„“\\p{Sc}%&*+#@^_+=<>/~]/gu) || []).length > 5) return false;\n\tconst wordCount = trimmed.split(/\\s+/).length;\n\tconst cssClassTokenRegex = /^!?([a-z][a-z0-9-]*:)*[a-z][a-z0-9]*(-[a-z0-9[\\].,%#/]+)*(\\/[a-z0-9]+)?$/;\n\tif (wordCount > 1) {\n\t\tconst tokens = trimmed.split(/\\s+/);\n\t\tif (tokens.every((token) => cssClassTokenRegex.test(token)) && tokens.some((token) => token.includes(\"-\"))) return false;\n\t}\n\tconst isCapitalized = /^['\"([]*\\p{Lu}/u.test(trimmed);\n\tif (wordCount === 1) {\n\t\tif (/[a-z]\\p{Lu}/u.test(trimmed)) return false;\n\t\tif (trimmed.includes(\"-\") || trimmed.includes(\"_\")) return false;\n\t}\n\tif (!isCapitalized && wordCount <= 2) return false;\n\treturn true;\n};\n\nexport { shouldExtract };\n//# sourceMappingURL=shouldExtract.mjs.map"],"mappings":";;;;;;;;;;;;;AAaA,MAAM,iBAAiB,SAAS;CAC/B,MAAM,UAAU,KAAK,KAAK;CAC1B,IAAI,CAAC,SAAS,OAAO;CACrB,IAAI,6BAA6B,KAAK,OAAO,GAAG,OAAO;CACvD,IAAI,QAAQ,WAAW,GAAG,KAAK,CAAC,QAAQ,WAAW,IAAI,KAAK,QAAQ,WAAW,IAAI,GAAG,OAAO;CAC7F,IAAI,QAAQ,SAAS,IAAI,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,SAAS,KAAK,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,SAAS,SAAS,KAAK,QAAQ,WAAW,WAAW,KAAK,QAAQ,WAAW,WAAW,GAAG,OAAO;CACvN,KAAK,QAAQ,MAAM,6DAA6D,KAAK,CAAC,EAAC,CAAE,SAAS,GAAG,OAAO;CAC5G,MAAM,YAAY,QAAQ,MAAM,KAAK,CAAC,CAAC;CACvC,MAAM,qBAAqB;CAC3B,IAAI,YAAY,GAAG;EAClB,MAAM,SAAS,QAAQ,MAAM,KAAK;EAClC,IAAI,OAAO,OAAO,UAAU,mBAAmB,KAAK,KAAK,CAAC,KAAK,OAAO,MAAM,UAAU,MAAM,SAAS,GAAG,CAAC,GAAG,OAAO;CACpH;CACA,MAAM,gBAAgB,kBAAkB,KAAK,OAAO;CACpD,IAAI,cAAc,GAAG;EACpB,IAAI,eAAe,KAAK,OAAO,GAAG,OAAO;EACzC,IAAI,QAAQ,SAAS,GAAG,KAAK,QAAQ,SAAS,GAAG,GAAG,OAAO;CAC5D;CACA,IAAI,CAAC,iBAAiB,aAAa,GAAG,OAAO;CAC7C,OAAO;AACR"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ATTRIBUTES_TO_EXTRACT } from "../@intlayer/config/dist/esm/extract/constants.mjs";
|
|
2
|
+
import { getExtractionIgnoreDirectives } from "../@intlayer/config/dist/esm/extract/ignoreDirectives.mjs";
|
|
2
3
|
import { shouldExtract } from "../@intlayer/config/dist/esm/extract/shouldExtract.mjs";
|
|
3
4
|
import { getJsxAttributeName, getJsxAttributeValueNode, getJsxElementName, getStaticStringValue, toAstNode } from "../utils/ast.mjs";
|
|
4
5
|
import { createRule } from "../utils/createRule.mjs";
|
|
@@ -27,6 +28,22 @@ const isContentDeclarationFile = (filename) => FILE_EXTENSIONS.some((extension)
|
|
|
27
28
|
*
|
|
28
29
|
* @param node - The reported node.
|
|
29
30
|
*/
|
|
31
|
+
/** Start offset of an ESLint node (`range[0]`), when it has one. */
|
|
32
|
+
const getNodeStart = (node) => {
|
|
33
|
+
const range = node?.["range"];
|
|
34
|
+
return Array.isArray(range) && typeof range[0] === "number" ? range[0] : void 0;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Start offset of the element a text, `{'literal'}` child or attribute
|
|
38
|
+
* belongs to (JSX, Vue and Svelte trees alike).
|
|
39
|
+
*/
|
|
40
|
+
const getEnclosingElementStart = (node) => {
|
|
41
|
+
let current = node.parent;
|
|
42
|
+
if (current?.type === "JSXExpressionContainer") current = current.parent;
|
|
43
|
+
if (current?.type === "JSXAttribute") current = current.parent;
|
|
44
|
+
if (current?.type === "JSXOpeningElement") current = current.parent;
|
|
45
|
+
return getNodeStart(current);
|
|
46
|
+
};
|
|
30
47
|
const getEnclosingElementName = (node) => {
|
|
31
48
|
let current = node;
|
|
32
49
|
while (current) {
|
|
@@ -92,6 +109,20 @@ const noRawText = createRule("no-raw-text", {
|
|
|
92
109
|
create: (context) => {
|
|
93
110
|
const filename = context.filename;
|
|
94
111
|
if (isContentDeclarationFile(filename)) return {};
|
|
112
|
+
const sourceText = context.sourceCode.text;
|
|
113
|
+
const ignoreDirectives = getExtractionIgnoreDirectives(sourceText);
|
|
114
|
+
if (ignoreDirectives.isFileIgnored) return {};
|
|
115
|
+
/**
|
|
116
|
+
* Whether an `intlayer-ignore-line` / `intlayer-ignore-next-line`
|
|
117
|
+
* directive covers the node: on the line where its text starts, or on
|
|
118
|
+
* the opening tag of its element.
|
|
119
|
+
*/
|
|
120
|
+
const isIgnoredNode = (node) => {
|
|
121
|
+
const astNode = toAstNode(node);
|
|
122
|
+
let textStart = getNodeStart(astNode);
|
|
123
|
+
if (textStart !== void 0) while (/\s/u.test(sourceText[textStart] ?? "")) textStart++;
|
|
124
|
+
return ignoreDirectives.isOffsetIgnored(textStart) || ignoreDirectives.isOffsetIgnored(getEnclosingElementStart(astNode));
|
|
125
|
+
};
|
|
95
126
|
const [options = {}] = context.options;
|
|
96
127
|
const attributes = new Set(options.attributes ?? ATTRIBUTES_TO_EXTRACT);
|
|
97
128
|
const ignoredElements = new Set(options.ignoreElements ?? DEFAULT_IGNORED_ELEMENTS);
|
|
@@ -114,6 +145,7 @@ const noRawText = createRule("no-raw-text", {
|
|
|
114
145
|
if (elementName && ignoredElements.has(elementName)) return;
|
|
115
146
|
if (attributeName !== null && !attributes.has(attributeName)) return;
|
|
116
147
|
if (!isReportableText(text)) return;
|
|
148
|
+
if (isIgnoredNode(node)) return;
|
|
117
149
|
context.report({
|
|
118
150
|
node,
|
|
119
151
|
messageId: attributeName === null ? "rawText" : "rawAttribute",
|
|
@@ -130,6 +162,7 @@ const noRawText = createRule("no-raw-text", {
|
|
|
130
162
|
if (!isReportableText(text)) return;
|
|
131
163
|
const elementName = getEnclosingElementName(astNode.parent);
|
|
132
164
|
if (elementName && ignoredElements.has(elementName)) return;
|
|
165
|
+
if (isIgnoredNode(node)) return;
|
|
133
166
|
context.report({
|
|
134
167
|
node,
|
|
135
168
|
messageId: "rawText",
|
|
@@ -145,6 +178,7 @@ const noRawText = createRule("no-raw-text", {
|
|
|
145
178
|
if (text === null || !isReportableText(text)) return;
|
|
146
179
|
const elementName = getEnclosingElementName(astNode.parent);
|
|
147
180
|
if (elementName && ignoredElements.has(elementName)) return;
|
|
181
|
+
if (isIgnoredNode(node)) return;
|
|
148
182
|
context.report({
|
|
149
183
|
node,
|
|
150
184
|
messageId: "rawAttribute",
|
|
@@ -161,6 +195,7 @@ const noRawText = createRule("no-raw-text", {
|
|
|
161
195
|
if (isStructuralPosition(astNode)) return;
|
|
162
196
|
const text = astNode["value"];
|
|
163
197
|
if (!isReportableText(text)) return;
|
|
198
|
+
if (isIgnoredNode(node)) return;
|
|
164
199
|
context.report({
|
|
165
200
|
node,
|
|
166
201
|
messageId: "rawStringLiteral",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noRawText.mjs","names":[],"sources":["../../../src/rules/noRawText.ts"],"sourcesContent":["import { FILE_EXTENSIONS } from '@intlayer/config/defaultValues';\nimport { ATTRIBUTES_TO_EXTRACT, shouldExtract } from '@intlayer/config/extract';\nimport {\n type AstNode,\n type AstNodeWithParent,\n getJsxAttributeName,\n getJsxAttributeValueNode,\n getJsxElementName,\n getStaticStringValue,\n toAstNode,\n} from '../utils/ast';\nimport { createRule } from '../utils/createRule';\nimport {\n combineVisitors,\n createTemplateVisitor,\n type TemplateVisitor,\n} from '../utils/templates';\n\n/**\n * Elements whose text is markup, code or metadata rather than user-facing copy.\n * Extracting them would be wrong, so they are skipped by default.\n */\nconst DEFAULT_IGNORED_ELEMENTS = ['code', 'pre', 'script', 'style'];\n\nexport type NoRawTextOptions = {\n /**\n * JSX attributes whose string value is user-facing copy.\n * Defaults to the compiler's own list, so the linter flags exactly what\n * `intlayer extract` would rewrite.\n */\n attributes?: string[];\n /** JSX elements whose children are never translatable. */\n ignoreElements?: string[];\n /** Regular-expression sources for text that should never be reported. */\n ignorePatterns?: string[];\n /**\n * Also report string literals outside JSX (function arguments, object values,\n * …). Off by default: outside JSX the false-positive rate is much higher, and\n * the compiler's own extractor is the better first pass.\n */\n includeStringLiterals?: boolean;\n};\n\ntype MessageIds = 'rawText' | 'rawAttribute' | 'rawStringLiteral';\n\n/**\n * True for a content declaration file, where raw strings are the whole point.\n *\n * @param filename - Absolute path of the linted file.\n */\nconst isContentDeclarationFile = (filename: string): boolean =>\n FILE_EXTENSIONS.some((extension) => filename.endsWith(extension));\n\n/**\n * Walk up from a JSX child or attribute to the tag name that encloses it.\n *\n * @param node - The reported node.\n */\nconst getEnclosingElementName = (\n node: AstNodeWithParent | undefined\n): string | null => {\n let current = node;\n\n while (current) {\n if (current.type === 'JSXElement') {\n return getJsxElementName(current['openingElement'] as AstNode);\n }\n\n if (current.type === 'JSXOpeningElement') {\n return getJsxElementName(current);\n }\n\n current = current.parent;\n }\n\n return null;\n};\n\n/**\n * True when the string literal sits in a position that is structural rather\n * than user-facing: module specifiers, property keys, directives, and the\n * arguments of the Intlayer getters themselves.\n *\n * @param node - The Literal node.\n */\nconst isStructuralPosition = (node: AstNodeWithParent): boolean => {\n const parent = node.parent;\n\n if (!parent) return true;\n\n switch (parent.type) {\n case 'ImportDeclaration':\n case 'ImportExpression':\n case 'ExportNamedDeclaration':\n case 'ExportAllDeclaration':\n case 'JSXAttribute':\n case 'TSLiteralType':\n case 'TSEnumMember':\n case 'ExpressionStatement':\n return true;\n\n case 'Property':\n // Keys are structural; values are not.\n return parent['key'] === node && parent['computed'] !== true;\n\n case 'MemberExpression':\n return parent['property'] === node;\n\n default:\n return false;\n }\n};\n\nexport const noRawText = createRule<[NoRawTextOptions?], MessageIds>(\n 'no-raw-text',\n {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Report user-facing strings that are not declared in an Intlayer dictionary',\n },\n messages: {\n rawText:\n 'Untranslated text \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawAttribute:\n 'Untranslated `{{attribute}}` value \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawStringLiteral:\n 'Untranslated string \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n attributes: { type: 'array', items: { type: 'string' } },\n ignoreElements: { type: 'array', items: { type: 'string' } },\n ignorePatterns: { type: 'array', items: { type: 'string' } },\n includeStringLiterals: { type: 'boolean' },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create: (context) => {\n const filename = context.filename;\n\n // Content declaration files hold the translations themselves.\n if (isContentDeclarationFile(filename)) return {};\n\n const [options = {}] = context.options;\n const attributes = new Set<string>(\n options.attributes ?? ATTRIBUTES_TO_EXTRACT\n );\n const ignoredElements = new Set<string>(\n options.ignoreElements ?? DEFAULT_IGNORED_ELEMENTS\n );\n const ignorePatterns = (options.ignorePatterns ?? []).map(\n (pattern) => new RegExp(pattern, 'u')\n );\n const includeStringLiterals = options.includeStringLiterals === true;\n\n /** True when the text is copy the extractor would pick up. */\n const isReportableText = (text: string): boolean =>\n shouldExtract(text) &&\n !ignorePatterns.some((pattern) => pattern.test(text.trim()));\n\n /** Collapse whitespace so multi-line JSX text reads well in the report. */\n const formatText = (text: string): string => {\n const normalized = text.replace(/\\s+/gu, ' ').trim();\n\n return normalized.length > 40\n ? `${normalized.slice(0, 40)}…`\n : normalized;\n };\n\n /**\n * Handlers for the markup languages that are not JSX — Vue, Svelte and\n * Angular templates. Reports through the same predicate and options as\n * the JSX path so a project gets identical results whatever it is written\n * in.\n */\n const templateVisitor = createTemplateVisitor(\n ({ node, text, attributeName, elementName }) => {\n if (elementName && ignoredElements.has(elementName)) return;\n\n if (attributeName !== null && !attributes.has(attributeName)) return;\n\n if (!isReportableText(text)) return;\n\n context.report({\n node: node as never,\n messageId: attributeName === null ? 'rawText' : 'rawAttribute',\n data: {\n text: formatText(text),\n ...(attributeName === null ? {} : { attribute: attributeName }),\n },\n });\n }\n );\n\n const scriptVisitor: TemplateVisitor = {\n JSXText: (node) => {\n const astNode = toAstNode(node as never);\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawText',\n data: { text: formatText(text) },\n });\n },\n\n JSXAttribute: (node) => {\n const astNode = toAstNode(node as never);\n const attributeName = getJsxAttributeName(astNode);\n\n if (!attributeName || !attributes.has(attributeName)) return;\n\n const valueNode = getJsxAttributeValueNode(astNode);\n const text = getStaticStringValue(valueNode);\n\n if (text === null || !isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawAttribute',\n data: { attribute: attributeName, text: formatText(text) },\n });\n },\n\n Literal: (node) => {\n if (!includeStringLiterals) return;\n\n const astNode = toAstNode(node as never);\n\n if (typeof astNode['value'] !== 'string') return;\n\n if (isStructuralPosition(astNode)) return;\n\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawStringLiteral',\n data: { text: formatText(text) },\n });\n },\n };\n\n return combineVisitors(\n context.sourceCode.parserServices,\n scriptVisitor,\n templateVisitor\n );\n },\n }\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"noRawText.mjs","names":[],"sources":["../../../src/rules/noRawText.ts"],"sourcesContent":["import { FILE_EXTENSIONS } from '@intlayer/config/defaultValues';\nimport {\n ATTRIBUTES_TO_EXTRACT,\n getExtractionIgnoreDirectives,\n shouldExtract,\n} from '@intlayer/config/extract';\nimport {\n type AstNode,\n type AstNodeWithParent,\n getJsxAttributeName,\n getJsxAttributeValueNode,\n getJsxElementName,\n getStaticStringValue,\n toAstNode,\n} from '../utils/ast';\nimport { createRule } from '../utils/createRule';\nimport {\n combineVisitors,\n createTemplateVisitor,\n type TemplateVisitor,\n} from '../utils/templates';\n\n/**\n * Elements whose text is markup, code or metadata rather than user-facing copy.\n * Extracting them would be wrong, so they are skipped by default.\n */\nconst DEFAULT_IGNORED_ELEMENTS = ['code', 'pre', 'script', 'style'];\n\nexport type NoRawTextOptions = {\n /**\n * JSX attributes whose string value is user-facing copy.\n * Defaults to the compiler's own list, so the linter flags exactly what\n * `intlayer extract` would rewrite.\n */\n attributes?: string[];\n /** JSX elements whose children are never translatable. */\n ignoreElements?: string[];\n /** Regular-expression sources for text that should never be reported. */\n ignorePatterns?: string[];\n /**\n * Also report string literals outside JSX (function arguments, object values,\n * …). Off by default: outside JSX the false-positive rate is much higher, and\n * the compiler's own extractor is the better first pass.\n */\n includeStringLiterals?: boolean;\n};\n\ntype MessageIds = 'rawText' | 'rawAttribute' | 'rawStringLiteral';\n\n/**\n * True for a content declaration file, where raw strings are the whole point.\n *\n * @param filename - Absolute path of the linted file.\n */\nconst isContentDeclarationFile = (filename: string): boolean =>\n FILE_EXTENSIONS.some((extension) => filename.endsWith(extension));\n\n/**\n * Walk up from a JSX child or attribute to the tag name that encloses it.\n *\n * @param node - The reported node.\n */\n/** Start offset of an ESLint node (`range[0]`), when it has one. */\nconst getNodeStart = (node: AstNode | undefined): number | undefined => {\n const range = node?.['range'];\n\n return Array.isArray(range) && typeof range[0] === 'number'\n ? range[0]\n : undefined;\n};\n\n/**\n * Start offset of the element a text, `{'literal'}` child or attribute\n * belongs to (JSX, Vue and Svelte trees alike).\n */\nconst getEnclosingElementStart = (\n node: AstNodeWithParent\n): number | undefined => {\n let current = node.parent;\n\n if (current?.type === 'JSXExpressionContainer') current = current.parent;\n if (current?.type === 'JSXAttribute') current = current.parent;\n if (current?.type === 'JSXOpeningElement') current = current.parent;\n\n return getNodeStart(current);\n};\n\nconst getEnclosingElementName = (\n node: AstNodeWithParent | undefined\n): string | null => {\n let current = node;\n\n while (current) {\n if (current.type === 'JSXElement') {\n return getJsxElementName(current['openingElement'] as AstNode);\n }\n\n if (current.type === 'JSXOpeningElement') {\n return getJsxElementName(current);\n }\n\n current = current.parent;\n }\n\n return null;\n};\n\n/**\n * True when the string literal sits in a position that is structural rather\n * than user-facing: module specifiers, property keys, directives, and the\n * arguments of the Intlayer getters themselves.\n *\n * @param node - The Literal node.\n */\nconst isStructuralPosition = (node: AstNodeWithParent): boolean => {\n const parent = node.parent;\n\n if (!parent) return true;\n\n switch (parent.type) {\n case 'ImportDeclaration':\n case 'ImportExpression':\n case 'ExportNamedDeclaration':\n case 'ExportAllDeclaration':\n case 'JSXAttribute':\n case 'TSLiteralType':\n case 'TSEnumMember':\n case 'ExpressionStatement':\n return true;\n\n case 'Property':\n // Keys are structural; values are not.\n return parent['key'] === node && parent['computed'] !== true;\n\n case 'MemberExpression':\n return parent['property'] === node;\n\n default:\n return false;\n }\n};\n\nexport const noRawText = createRule<[NoRawTextOptions?], MessageIds>(\n 'no-raw-text',\n {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Report user-facing strings that are not declared in an Intlayer dictionary',\n },\n messages: {\n rawText:\n 'Untranslated text \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawAttribute:\n 'Untranslated `{{attribute}}` value \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawStringLiteral:\n 'Untranslated string \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n attributes: { type: 'array', items: { type: 'string' } },\n ignoreElements: { type: 'array', items: { type: 'string' } },\n ignorePatterns: { type: 'array', items: { type: 'string' } },\n includeStringLiterals: { type: 'boolean' },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create: (context) => {\n const filename = context.filename;\n\n // Content declaration files hold the translations themselves.\n if (isContentDeclarationFile(filename)) return {};\n\n const sourceText = context.sourceCode.text;\n const ignoreDirectives = getExtractionIgnoreDirectives(sourceText);\n\n // Same comment directives as the compiler: what it skips is not reported.\n if (ignoreDirectives.isFileIgnored) return {};\n\n /**\n * Whether an `intlayer-ignore-line` / `intlayer-ignore-next-line`\n * directive covers the node: on the line where its text starts, or on\n * the opening tag of its element.\n */\n const isIgnoredNode = (node: unknown): boolean => {\n const astNode = toAstNode(node as never);\n let textStart = getNodeStart(astNode);\n\n if (textStart !== undefined) {\n while (/\\s/u.test(sourceText[textStart] ?? '')) textStart++;\n }\n\n return (\n ignoreDirectives.isOffsetIgnored(textStart) ||\n ignoreDirectives.isOffsetIgnored(getEnclosingElementStart(astNode))\n );\n };\n\n const [options = {}] = context.options;\n const attributes = new Set<string>(\n options.attributes ?? ATTRIBUTES_TO_EXTRACT\n );\n const ignoredElements = new Set<string>(\n options.ignoreElements ?? DEFAULT_IGNORED_ELEMENTS\n );\n const ignorePatterns = (options.ignorePatterns ?? []).map(\n (pattern) => new RegExp(pattern, 'u')\n );\n const includeStringLiterals = options.includeStringLiterals === true;\n\n /** True when the text is copy the extractor would pick up. */\n const isReportableText = (text: string): boolean =>\n shouldExtract(text) &&\n !ignorePatterns.some((pattern) => pattern.test(text.trim()));\n\n /** Collapse whitespace so multi-line JSX text reads well in the report. */\n const formatText = (text: string): string => {\n const normalized = text.replace(/\\s+/gu, ' ').trim();\n\n return normalized.length > 40\n ? `${normalized.slice(0, 40)}…`\n : normalized;\n };\n\n /**\n * Handlers for the markup languages that are not JSX — Vue, Svelte and\n * Angular templates. Reports through the same predicate and options as\n * the JSX path so a project gets identical results whatever it is written\n * in.\n */\n const templateVisitor = createTemplateVisitor(\n ({ node, text, attributeName, elementName }) => {\n if (elementName && ignoredElements.has(elementName)) return;\n\n if (attributeName !== null && !attributes.has(attributeName)) return;\n\n if (!isReportableText(text)) return;\n\n if (isIgnoredNode(node)) return;\n\n context.report({\n node: node as never,\n messageId: attributeName === null ? 'rawText' : 'rawAttribute',\n data: {\n text: formatText(text),\n ...(attributeName === null ? {} : { attribute: attributeName }),\n },\n });\n }\n );\n\n const scriptVisitor: TemplateVisitor = {\n JSXText: (node) => {\n const astNode = toAstNode(node as never);\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n if (isIgnoredNode(node)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawText',\n data: { text: formatText(text) },\n });\n },\n\n JSXAttribute: (node) => {\n const astNode = toAstNode(node as never);\n const attributeName = getJsxAttributeName(astNode);\n\n if (!attributeName || !attributes.has(attributeName)) return;\n\n const valueNode = getJsxAttributeValueNode(astNode);\n const text = getStaticStringValue(valueNode);\n\n if (text === null || !isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n if (isIgnoredNode(node)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawAttribute',\n data: { attribute: attributeName, text: formatText(text) },\n });\n },\n\n Literal: (node) => {\n if (!includeStringLiterals) return;\n\n const astNode = toAstNode(node as never);\n\n if (typeof astNode['value'] !== 'string') return;\n\n if (isStructuralPosition(astNode)) return;\n\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n if (isIgnoredNode(node)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawStringLiteral',\n data: { text: formatText(text) },\n });\n },\n };\n\n return combineVisitors(\n context.sourceCode.parserServices,\n scriptVisitor,\n templateVisitor\n );\n },\n }\n);\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noRawText.d.ts","names":["_$___utils_createRule0.IntlayerRule"],"sources":["../../../src/rules/noRawText.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"noRawText.d.ts","names":["_$___utils_createRule0.IntlayerRule"],"sources":["../../../src/rules/noRawText.ts"],"mappings":";YA4BY;;;;;;EAMV;;EAEA;;EAEA;;;;;;EAMA;;KAGG;qBA+FQ,WAASA,cAAA,oBAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-intlayer",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.8.0-canary.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "ESLint (and oxlint) plugin for Intlayer. Catches untranslated strings and the dynamic call shapes the Intlayer compiler cannot optimize.",
|
|
6
6
|
"keywords": [
|
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
"typecheck": "tsc --noEmit --project tsconfig.types.json"
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
-
"@intlayer/config": "9.
|
|
81
|
-
"@intlayer/lsp": "9.
|
|
80
|
+
"@intlayer/config": "9.8.0-canary.0",
|
|
81
|
+
"@intlayer/lsp": "9.8.0-canary.0",
|
|
82
82
|
"fast-glob": "3.3.3"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|