eslint-plugin-intlayer 9.7.3 → 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 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. Brand names, CSS class strings, technical identifiers
188
- and code-shaped strings are filtered out.
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* - Single capitalized words (likely brand/proper nouns, e.g. \"Intlayer\")\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 && isCapitalized) return false;\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":";;;;;;;;;;;AAWA,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,KAAK,eAAe,OAAO;CAC7C,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
+ {"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* - Single capitalized words (likely brand/proper nouns, e.g. \"Intlayer\")\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 && isCapitalized) return false;\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":";;;;;;;;;;;AAWA,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,KAAK,eAAe,OAAO;CAC7C,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
+ {"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":";YAwBY;;;;;;EAMV;;EAEA;;EAEA;;;;;;EAMA;;KAGG;qBAsEQ,WAASA,cAAA,oBAAA"}
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.7.3",
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.7.3",
81
- "@intlayer/lsp": "9.7.3",
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": {