@trackunit/eslint-plugin-trackunit 0.6.103 → 0.6.105

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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 0.6.105 (2026-08-13)
2
+
3
+ ### 🧱 Updated Dependencies
4
+
5
+ - Updated css-classname-utils to 0.0.11
6
+ - Updated shared-utils to 1.16.8
7
+
8
+ ## 0.6.104 (2026-08-13)
9
+
10
+ ### 🧱 Updated Dependencies
11
+
12
+ - Updated css-classname-utils to 0.0.10
13
+ - Updated shared-utils to 1.16.7
14
+
1
15
  ## 0.6.103 (2026-08-13)
2
16
 
3
17
  ### 🧱 Updated Dependencies
package/README.md CHANGED
@@ -95,7 +95,7 @@ All custom rules are enabled automatically through the presets under the `@track
95
95
  | Rule | Severity | Auto-fix | Description |
96
96
  | --------------------------------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
97
97
  | `@trackunit/no-dynamic-translation-key` | error | — | Discourages building i18next keys from template strings (`t(`a.${x}`)`). Map values to explicit keys with a `switch`/`Record`. |
98
- | `@trackunit/no-unused-translation-key` | warn | | Reports keys in a library's `locales/en/translation.json` that are never referenced in that library's source. |
98
+ | `@trackunit/no-unused-translation-key` | error | suggest | Reports keys in a library's `locales/en/translation.json` that are never referenced in that library's source. |
99
99
 
100
100
  ## Utilities
101
101
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/eslint-plugin-trackunit",
3
- "version": "0.6.103",
3
+ "version": "0.6.105",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "repository": "https://github.com/Trackunit/manager",
6
6
  "engines": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@nx/eslint-plugin": "23.1.0",
11
- "@trackunit/css-classname-utils": "0.0.9",
11
+ "@trackunit/css-classname-utils": "0.0.11",
12
12
  "@typescript-eslint/eslint-plugin": "8.58.1",
13
13
  "@typescript-eslint/utils": "8.58.1",
14
14
  "eslint-config-prettier": "^10.1.8",
@@ -41,7 +41,7 @@ export declare const configs: {
41
41
  "no-template-strings-in-classname-prop": import("@typescript-eslint/utils/ts-eslint").RuleModule<"templateStringInClassName", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
42
42
  name: string;
43
43
  };
44
- "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
44
+ "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey" | "removeUnusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
45
45
  name: string;
46
46
  };
47
47
  "require-classname-alternatives": import("@typescript-eslint/utils/ts-eslint").RuleModule<"bannedClassAutoFix" | "bannedClassSuggest" | "suggestReplacement", [({
@@ -245,7 +245,7 @@ export declare const configs: {
245
245
  "no-template-strings-in-classname-prop": import("@typescript-eslint/utils/ts-eslint").RuleModule<"templateStringInClassName", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
246
246
  name: string;
247
247
  };
248
- "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
248
+ "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey" | "removeUnusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
249
249
  name: string;
250
250
  };
251
251
  "require-classname-alternatives": import("@typescript-eslint/utils/ts-eslint").RuleModule<"bannedClassAutoFix" | "bannedClassSuggest" | "suggestReplacement", [({
@@ -365,7 +365,7 @@ export declare const configs: {
365
365
  "no-template-strings-in-classname-prop": import("@typescript-eslint/utils/ts-eslint").RuleModule<"templateStringInClassName", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
366
366
  name: string;
367
367
  };
368
- "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
368
+ "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey" | "removeUnusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
369
369
  name: string;
370
370
  };
371
371
  "require-classname-alternatives": import("@typescript-eslint/utils/ts-eslint").RuleModule<"bannedClassAutoFix" | "bannedClassSuggest" | "suggestReplacement", [({
@@ -501,7 +501,7 @@ export declare const configs: {
501
501
  "no-template-strings-in-classname-prop": import("@typescript-eslint/utils/ts-eslint").RuleModule<"templateStringInClassName", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
502
502
  name: string;
503
503
  };
504
- "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
504
+ "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey" | "removeUnusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
505
505
  name: string;
506
506
  };
507
507
  "require-classname-alternatives": import("@typescript-eslint/utils/ts-eslint").RuleModule<"bannedClassAutoFix" | "bannedClassSuggest" | "suggestReplacement", [({
@@ -59,7 +59,7 @@ export declare const localRulesPlugin: {
59
59
  "no-template-strings-in-classname-prop": import("@typescript-eslint/utils/ts-eslint").RuleModule<"templateStringInClassName", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
60
60
  name: string;
61
61
  };
62
- "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
62
+ "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey" | "removeUnusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
63
63
  name: string;
64
64
  };
65
65
  "require-classname-alternatives": import("@typescript-eslint/utils/ts-eslint").RuleModule<"bannedClassAutoFix" | "bannedClassSuggest" | "suggestReplacement", [({
@@ -41,7 +41,7 @@ export declare const base: (import("eslint").Linter.FlatConfig<import("eslint").
41
41
  "no-template-strings-in-classname-prop": import("@typescript-eslint/utils/ts-eslint").RuleModule<"templateStringInClassName", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
42
42
  name: string;
43
43
  };
44
- "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
44
+ "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey" | "removeUnusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
45
45
  name: string;
46
46
  };
47
47
  "require-classname-alternatives": import("@typescript-eslint/utils/ts-eslint").RuleModule<"bannedClassAutoFix" | "bannedClassSuggest" | "suggestReplacement", [({
@@ -245,7 +245,7 @@ export declare const base: (import("eslint").Linter.FlatConfig<import("eslint").
245
245
  "no-template-strings-in-classname-prop": import("@typescript-eslint/utils/ts-eslint").RuleModule<"templateStringInClassName", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
246
246
  name: string;
247
247
  };
248
- "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
248
+ "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey" | "removeUnusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
249
249
  name: string;
250
250
  };
251
251
  "require-classname-alternatives": import("@typescript-eslint/utils/ts-eslint").RuleModule<"bannedClassAutoFix" | "bannedClassSuggest" | "suggestReplacement", [({
@@ -365,7 +365,7 @@ export declare const base: (import("eslint").Linter.FlatConfig<import("eslint").
365
365
  "no-template-strings-in-classname-prop": import("@typescript-eslint/utils/ts-eslint").RuleModule<"templateStringInClassName", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
366
366
  name: string;
367
367
  };
368
- "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
368
+ "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey" | "removeUnusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
369
369
  name: string;
370
370
  };
371
371
  "require-classname-alternatives": import("@typescript-eslint/utils/ts-eslint").RuleModule<"bannedClassAutoFix" | "bannedClassSuggest" | "suggestReplacement", [({
@@ -135,7 +135,7 @@ exports.base = [
135
135
  "@trackunit": plugins_1.localRulesPlugin,
136
136
  },
137
137
  rules: {
138
- "@trackunit/no-unused-translation-key": "warn",
138
+ "@trackunit/no-unused-translation-key": "error",
139
139
  },
140
140
  },
141
141
  jest_overrides_1.baseJestOverrides,
@@ -40,7 +40,7 @@ export declare const reactPreset: (import("eslint").Linter.Config<import("eslint
40
40
  "no-template-strings-in-classname-prop": import("@typescript-eslint/utils/ts-eslint").RuleModule<"templateStringInClassName", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
41
41
  name: string;
42
42
  };
43
- "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
43
+ "no-unused-translation-key": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedKey" | "removeUnusedKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
44
44
  name: string;
45
45
  };
46
46
  "require-classname-alternatives": import("@typescript-eslint/utils/ts-eslint").RuleModule<"bannedClassAutoFix" | "bannedClassSuggest" | "suggestReplacement", [({
@@ -12,13 +12,16 @@
12
12
  * prefix, so keys behind one cannot be reported until the call site uses a
13
13
  * static key — what the companion `no-dynamic-translation-key` rule asks for.
14
14
  *
15
- * Matching is a heuristic over source text: this rule is `warn`-only and has no
16
- * fixer, so a key it reports is a candidate for review rather than for deletion.
15
+ * Matching is a heuristic over source text: this rule has no autofix, so a key
16
+ * it reports is still a candidate for review before applying the removal
17
+ * suggestion.
17
18
  *
18
19
  * Runs only on `**\/locales/en/translation.json` — the English file is the
19
20
  * source of truth; other languages are managed by Crowdin.
20
21
  */
21
22
  import { ESLintUtils } from "@typescript-eslint/utils";
22
- export declare const noUnusedTranslationKey: ESLintUtils.RuleModule<"unusedKey", [], unknown, ESLintUtils.RuleListener> & {
23
+ type MessageIds = "unusedKey" | "removeUnusedKey";
24
+ export declare const noUnusedTranslationKey: ESLintUtils.RuleModule<MessageIds, [], unknown, ESLintUtils.RuleListener> & {
23
25
  name: string;
24
26
  };
27
+ export {};
@@ -16,8 +16,9 @@ const tslib_1 = require("tslib");
16
16
  * prefix, so keys behind one cannot be reported until the call site uses a
17
17
  * static key — what the companion `no-dynamic-translation-key` rule asks for.
18
18
  *
19
- * Matching is a heuristic over source text: this rule is `warn`-only and has no
20
- * fixer, so a key it reports is a candidate for review rather than for deletion.
19
+ * Matching is a heuristic over source text: this rule has no autofix, so a key
20
+ * it reports is still a candidate for review before applying the removal
21
+ * suggestion.
21
22
  *
22
23
  * Runs only on `**\/locales/en/translation.json` — the English file is the
23
24
  * source of truth; other languages are managed by Crowdin.
@@ -28,67 +29,78 @@ const file_utils_1 = require("../../utils/file-utils");
28
29
  const used_strings_1 = require("./used-strings");
29
30
  const createRule = utils_1.ESLintUtils.RuleCreator(name => `https://github.com/trackunit/manager/blob/main/libs/eslint/plugin-trackunit/src/lib/rules/${name}/${name}.ts`);
30
31
  const TRANSLATION_FILE_SUFFIX = "/locales/en/translation.json";
31
- const parseTranslationObject = (raw) => {
32
- let parsed;
33
- try {
34
- parsed = JSON.parse(raw);
35
- }
36
- catch {
37
- return null;
32
+ const getKeyText = (key) => (key.type === "JSONIdentifier" ? key.name : String(key.value));
33
+ /**
34
+ * The source range to drop so the remaining object stays valid JSON: a property
35
+ * with a following sibling reaches up to that sibling (taking its own separating
36
+ * comma and indentation with it), a trailing property reaches back to the end of
37
+ * the preceding one, and a sole property leaves an empty object behind.
38
+ */
39
+ const getRemovalRange = (property) => {
40
+ const siblings = property.parent.properties;
41
+ const position = siblings.indexOf(property);
42
+ const nextSibling = siblings[position + 1];
43
+ const previousSibling = siblings[position - 1];
44
+ if (nextSibling) {
45
+ return [property.range[0], nextSibling.range[0]];
38
46
  }
39
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
40
- return null;
47
+ if (previousSibling) {
48
+ return [previousSibling.range[1], property.range[1]];
41
49
  }
42
- return { ...parsed };
50
+ return [property.parent.range[0] + 1, property.parent.range[1] - 1];
43
51
  };
44
52
  exports.noUnusedTranslationKey = createRule({
45
53
  name: "no-unused-translation-key",
46
54
  meta: {
47
55
  type: "suggestion",
56
+ hasSuggestions: true,
48
57
  docs: {
49
58
  description: "Report translation keys in a library's locales/en/translation.json that are never referenced in that library's source.",
50
59
  },
51
60
  messages: {
52
61
  unusedKey: 'Translation key "{{key}}" is not referenced anywhere in this library and can be removed.',
62
+ removeUnusedKey: 'Remove translation key "{{key}}".',
53
63
  },
54
64
  schema: [],
55
65
  },
56
66
  defaultOptions: [],
57
67
  create(context) {
68
+ const filePath = context.filename.replace(/\\/g, "/");
69
+ if (!filePath.endsWith(TRANSLATION_FILE_SUFFIX)) {
70
+ return {};
71
+ }
72
+ const projectJsonPath = (0, file_utils_1.findNearestFile)(context.filename, "project.json");
73
+ if (!projectJsonPath) {
74
+ return {};
75
+ }
76
+ // Scanning the whole library is expensive, so only pay for it once a key is actually inspected.
77
+ let usedStrings;
78
+ const getUsed = () => {
79
+ usedStrings ?? (usedStrings = (0, used_strings_1.getUsedTranslationStrings)(path.dirname(projectJsonPath)));
80
+ return usedStrings;
81
+ };
58
82
  return {
59
- Program: () => {
60
- const filePath = context.filename.replace(/\\/g, "/");
61
- if (!filePath.endsWith(TRANSLATION_FILE_SUFFIX)) {
83
+ JSONProperty: (property) => {
84
+ // Translation files are flat; anything nested belongs to a value, not to a key.
85
+ if (property.parent.parent.type !== "JSONExpressionStatement") {
62
86
  return;
63
87
  }
64
- const projectJsonPath = (0, file_utils_1.findNearestFile)(context.filename, "project.json");
65
- if (!projectJsonPath) {
88
+ const key = getKeyText(property.key);
89
+ if ((0, used_strings_1.isTranslationKeyUsed)(getUsed(), key)) {
66
90
  return;
67
91
  }
68
- const raw = context.sourceCode.getText();
69
- const translations = parseTranslationObject(raw);
70
- if (!translations) {
71
- return;
72
- }
73
- const used = (0, used_strings_1.getUsedTranslationStrings)(path.dirname(projectJsonPath));
74
- const lines = raw.split("\n");
75
- for (const key of Object.keys(translations)) {
76
- if ((0, used_strings_1.isTranslationKeyUsed)(used, key)) {
77
- continue;
78
- }
79
- const keyToken = `"${key}"`;
80
- const lineIndex = lines.findIndex(text => text.includes(`${keyToken}:`));
81
- const line = lineIndex >= 0 ? lineIndex + 1 : 1;
82
- const startColumn = lineIndex >= 0 ? Math.max(lines[lineIndex]?.indexOf(keyToken) ?? 0, 0) : 0;
83
- context.report({
84
- messageId: "unusedKey",
85
- data: { key },
86
- loc: {
87
- start: { line, column: startColumn },
88
- end: { line, column: startColumn + keyToken.length },
92
+ context.report({
93
+ messageId: "unusedKey",
94
+ data: { key },
95
+ loc: property.key.loc,
96
+ suggest: [
97
+ {
98
+ messageId: "removeUnusedKey",
99
+ data: { key },
100
+ fix: fixer => fixer.removeRange(getRemovalRange(property)),
89
101
  },
90
- });
91
- }
102
+ ],
103
+ });
92
104
  },
93
105
  };
94
106
  },
@@ -37,7 +37,7 @@ export declare const rulesMap: {
37
37
  "no-template-strings-in-classname-prop": ESLintUtils.RuleModule<"templateStringInClassName", [], unknown, ESLintUtils.RuleListener> & {
38
38
  name: string;
39
39
  };
40
- "no-unused-translation-key": ESLintUtils.RuleModule<"unusedKey", [], unknown, ESLintUtils.RuleListener> & {
40
+ "no-unused-translation-key": ESLintUtils.RuleModule<"unusedKey" | "removeUnusedKey", [], unknown, ESLintUtils.RuleListener> & {
41
41
  name: string;
42
42
  };
43
43
  "require-classname-alternatives": ESLintUtils.RuleModule<"bannedClassAutoFix" | "bannedClassSuggest" | "suggestReplacement", [({