@better-i18n/cli 0.1.7 → 0.2.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.
Files changed (142) hide show
  1. package/dist/analyzer/dynamic-matcher.d.ts +53 -0
  2. package/dist/analyzer/dynamic-matcher.d.ts.map +1 -0
  3. package/dist/analyzer/dynamic-matcher.js +93 -0
  4. package/dist/analyzer/dynamic-matcher.js.map +1 -0
  5. package/dist/analyzer/index.d.ts +13 -4
  6. package/dist/analyzer/index.d.ts.map +1 -1
  7. package/dist/analyzer/index.js +165 -22
  8. package/dist/analyzer/index.js.map +1 -1
  9. package/dist/analyzer/rules/data-structure.d.ts +13 -0
  10. package/dist/analyzer/rules/data-structure.d.ts.map +1 -0
  11. package/dist/analyzer/rules/data-structure.js +264 -0
  12. package/dist/analyzer/rules/data-structure.js.map +1 -0
  13. package/dist/analyzer/rules/index.d.ts +10 -7
  14. package/dist/analyzer/rules/index.d.ts.map +1 -1
  15. package/dist/analyzer/rules/index.js +10 -7
  16. package/dist/analyzer/rules/index.js.map +1 -1
  17. package/dist/analyzer/rules/jsx-text.js +1 -1
  18. package/dist/analyzer/rules/jsx-text.js.map +1 -1
  19. package/dist/analyzer/rules/translation-function.d.ts.map +1 -1
  20. package/dist/analyzer/rules/translation-function.js +94 -6
  21. package/dist/analyzer/rules/translation-function.js.map +1 -1
  22. package/dist/analyzer/types.d.ts +28 -3
  23. package/dist/analyzer/types.d.ts.map +1 -1
  24. package/dist/commands/check.d.ts +14 -0
  25. package/dist/commands/check.d.ts.map +1 -0
  26. package/dist/commands/check.js +70 -0
  27. package/dist/commands/check.js.map +1 -0
  28. package/dist/commands/doctor.d.ts +20 -0
  29. package/dist/commands/doctor.d.ts.map +1 -0
  30. package/dist/commands/doctor.js +77 -0
  31. package/dist/commands/doctor.js.map +1 -0
  32. package/dist/commands/sync.d.ts +1 -0
  33. package/dist/commands/sync.d.ts.map +1 -1
  34. package/dist/commands/sync.js +158 -272
  35. package/dist/commands/sync.js.map +1 -1
  36. package/dist/context/detector.js +1 -1
  37. package/dist/context/detector.js.map +1 -1
  38. package/dist/context/oidc.d.ts +33 -0
  39. package/dist/context/oidc.d.ts.map +1 -0
  40. package/dist/context/oidc.js +63 -0
  41. package/dist/context/oidc.js.map +1 -0
  42. package/dist/doctor/index.d.ts +65 -0
  43. package/dist/doctor/index.d.ts.map +1 -0
  44. package/dist/doctor/index.js +252 -0
  45. package/dist/doctor/index.js.map +1 -0
  46. package/dist/doctor/project-discovery.d.ts +37 -0
  47. package/dist/doctor/project-discovery.d.ts.map +1 -0
  48. package/dist/doctor/project-discovery.js +140 -0
  49. package/dist/doctor/project-discovery.js.map +1 -0
  50. package/dist/doctor/score.d.ts +41 -0
  51. package/dist/doctor/score.d.ts.map +1 -0
  52. package/dist/doctor/score.js +107 -0
  53. package/dist/doctor/score.js.map +1 -0
  54. package/dist/index.js +35 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/reporters/doctor-eslint.d.ts +17 -0
  57. package/dist/reporters/doctor-eslint.d.ts.map +1 -0
  58. package/dist/reporters/doctor-eslint.js +139 -0
  59. package/dist/reporters/doctor-eslint.js.map +1 -0
  60. package/dist/reporters/doctor-json.d.ts +12 -0
  61. package/dist/reporters/doctor-json.d.ts.map +1 -0
  62. package/dist/reporters/doctor-json.js +21 -0
  63. package/dist/reporters/doctor-json.js.map +1 -0
  64. package/dist/reporters/doctor-report.d.ts +23 -0
  65. package/dist/reporters/doctor-report.d.ts.map +1 -0
  66. package/dist/reporters/doctor-report.js +62 -0
  67. package/dist/reporters/doctor-report.js.map +1 -0
  68. package/dist/reporters/eslint-style.js +4 -4
  69. package/dist/reporters/eslint-style.js.map +1 -1
  70. package/dist/rules/categories.d.ts +36 -0
  71. package/dist/rules/categories.d.ts.map +1 -0
  72. package/dist/rules/categories.js +80 -0
  73. package/dist/rules/categories.js.map +1 -0
  74. package/dist/rules/code/index.d.ts +9 -0
  75. package/dist/rules/code/index.d.ts.map +1 -0
  76. package/dist/rules/code/index.js +9 -0
  77. package/dist/rules/code/index.js.map +1 -0
  78. package/dist/rules/code/jsx-attribute.d.ts +12 -0
  79. package/dist/rules/code/jsx-attribute.d.ts.map +1 -0
  80. package/dist/rules/code/jsx-attribute.js +78 -0
  81. package/dist/rules/code/jsx-attribute.js.map +1 -0
  82. package/dist/rules/code/jsx-text.d.ts +12 -0
  83. package/dist/rules/code/jsx-text.d.ts.map +1 -0
  84. package/dist/rules/code/jsx-text.js +47 -0
  85. package/dist/rules/code/jsx-text.js.map +1 -0
  86. package/dist/rules/code/string-variable.d.ts +13 -0
  87. package/dist/rules/code/string-variable.d.ts.map +1 -0
  88. package/dist/rules/code/string-variable.js +185 -0
  89. package/dist/rules/code/string-variable.js.map +1 -0
  90. package/dist/rules/code/ternary-locale.d.ts +12 -0
  91. package/dist/rules/code/ternary-locale.d.ts.map +1 -0
  92. package/dist/rules/code/ternary-locale.js +53 -0
  93. package/dist/rules/code/ternary-locale.js.map +1 -0
  94. package/dist/rules/code/toast-message.d.ts +13 -0
  95. package/dist/rules/code/toast-message.d.ts.map +1 -0
  96. package/dist/rules/code/toast-message.js +101 -0
  97. package/dist/rules/code/toast-message.js.map +1 -0
  98. package/dist/rules/extraction/data-structure.d.ts +13 -0
  99. package/dist/rules/extraction/data-structure.d.ts.map +1 -0
  100. package/dist/rules/extraction/data-structure.js +212 -0
  101. package/dist/rules/extraction/data-structure.js.map +1 -0
  102. package/dist/rules/extraction/index.d.ts +6 -0
  103. package/dist/rules/extraction/index.d.ts.map +1 -0
  104. package/dist/rules/extraction/index.js +6 -0
  105. package/dist/rules/extraction/index.js.map +1 -0
  106. package/dist/rules/extraction/translation-function.d.ts +12 -0
  107. package/dist/rules/extraction/translation-function.d.ts.map +1 -0
  108. package/dist/rules/extraction/translation-function.js +153 -0
  109. package/dist/rules/extraction/translation-function.js.map +1 -0
  110. package/dist/rules/health/index.d.ts +16 -0
  111. package/dist/rules/health/index.d.ts.map +1 -0
  112. package/dist/rules/health/index.js +22 -0
  113. package/dist/rules/health/index.js.map +1 -0
  114. package/dist/rules/health/missing-translations.d.ts +13 -0
  115. package/dist/rules/health/missing-translations.d.ts.map +1 -0
  116. package/dist/rules/health/missing-translations.js +48 -0
  117. package/dist/rules/health/missing-translations.js.map +1 -0
  118. package/dist/rules/health/orphan-keys.d.ts +16 -0
  119. package/dist/rules/health/orphan-keys.d.ts.map +1 -0
  120. package/dist/rules/health/orphan-keys.js +50 -0
  121. package/dist/rules/health/orphan-keys.js.map +1 -0
  122. package/dist/rules/health/placeholder-mismatch.d.ts +32 -0
  123. package/dist/rules/health/placeholder-mismatch.d.ts.map +1 -0
  124. package/dist/rules/health/placeholder-mismatch.js +120 -0
  125. package/dist/rules/health/placeholder-mismatch.js.map +1 -0
  126. package/dist/rules/registry.d.ts +84 -0
  127. package/dist/rules/registry.d.ts.map +1 -0
  128. package/dist/rules/registry.js +11 -0
  129. package/dist/rules/registry.js.map +1 -0
  130. package/dist/utils/cdn-client.d.ts +30 -0
  131. package/dist/utils/cdn-client.d.ts.map +1 -0
  132. package/dist/utils/cdn-client.js +59 -0
  133. package/dist/utils/cdn-client.js.map +1 -0
  134. package/dist/utils/json-keys.d.ts +60 -0
  135. package/dist/utils/json-keys.d.ts.map +1 -0
  136. package/dist/utils/json-keys.js +103 -0
  137. package/dist/utils/json-keys.js.map +1 -0
  138. package/dist/utils/key-comparison.d.ts +67 -0
  139. package/dist/utils/key-comparison.d.ts.map +1 -0
  140. package/dist/utils/key-comparison.js +299 -0
  141. package/dist/utils/key-comparison.js.map +1 -0
  142. package/package.json +2 -2
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Rule Metadata — Category and Help Maps
3
+ *
4
+ * Centralized metadata for all i18n Doctor rules.
5
+ * Following react-doctor's pattern: rules produce only `message`,
6
+ * category and help text are external for central management.
7
+ *
8
+ * To add a new rule:
9
+ * 1. Add entry to RULE_CATEGORY_MAP
10
+ * 2. Add entry to RULE_HELP_MAP
11
+ * 3. Implement the rule in src/rules/health/ or src/rules/code/
12
+ */
13
+ /**
14
+ * Maps rule ID → category.
15
+ *
16
+ * react-doctor uses RULE_CATEGORY_MAP + PLUGIN_CATEGORY_MAP with fallback.
17
+ * We simplify to a single map since all rules are "better-i18n" plugin.
18
+ */
19
+ export const RULE_CATEGORY_MAP = {
20
+ // ── Health Rules (translation file analysis) ──────────────────────
21
+ "missing-translations": "Coverage",
22
+ "orphan-keys": "Performance",
23
+ "placeholder-mismatch": "Quality",
24
+ "empty-translations": "Quality",
25
+ "key-naming": "Structure",
26
+ "namespace-structure": "Structure",
27
+ "file-format": "Structure",
28
+ // ── Code Rules (AST analysis, from existing analyzer) ─────────────
29
+ "jsx-text": "Code",
30
+ "jsx-attribute": "Code",
31
+ "ternary-locale": "Code",
32
+ "toast-message": "Code",
33
+ "string-variable": "Code",
34
+ };
35
+ /**
36
+ * Maps rule ID → actionable fix suggestion.
37
+ *
38
+ * react-doctor stores these in RULE_HELP_MAP in run-oxlint.ts.
39
+ * We centralize them here alongside categories.
40
+ */
41
+ export const RULE_HELP_MAP = {
42
+ // ── Health Rules ──────────────────────────────────────────────────
43
+ "missing-translations": "Add translations for missing keys using the AI Drawer or manual entry in the editor",
44
+ "orphan-keys": "Remove unused keys from translation files to reduce bundle size and maintenance cost",
45
+ "placeholder-mismatch": "Ensure {placeholders} in source and target translations match exactly. Missing or extra placeholders cause runtime errors",
46
+ "empty-translations": "Replace empty translation strings with actual translations, or remove the key if unused",
47
+ "key-naming": "Use consistent naming convention within each namespace (e.g., camelCase or snake_case, not mixed)",
48
+ "namespace-structure": "Maintain consistent namespace depth across translation files for predictable key organization",
49
+ "file-format": "Fix malformed JSON or encoding issues in translation files",
50
+ // ── Code Rules ────────────────────────────────────────────────────
51
+ "jsx-text": "Wrap hardcoded JSX text with t() function call for translation support",
52
+ "jsx-attribute": "Use t() for translatable attributes like title, alt, placeholder, aria-label",
53
+ "ternary-locale": "Replace locale-based ternary (locale === 'en' ? 'Hello' : 'Merhaba') with t() function",
54
+ "toast-message": "Wrap toast notification messages with t() for translation support",
55
+ "string-variable": "Extract user-facing string variables into translation keys using t()",
56
+ };
57
+ /**
58
+ * Default severity for code rules.
59
+ *
60
+ * Health rules define defaultSeverity on the rule object itself.
61
+ * Code rules (from the analyzer) have severity embedded in their output,
62
+ * but this map provides fallback/override capability.
63
+ */
64
+ export const RULE_DEFAULT_SEVERITY = {
65
+ // Health rules (also defined on rule object, this is for reference)
66
+ "missing-translations": "error",
67
+ "orphan-keys": "warning",
68
+ "placeholder-mismatch": "error",
69
+ "empty-translations": "warning",
70
+ "key-naming": "warning",
71
+ "namespace-structure": "info",
72
+ "file-format": "error",
73
+ // Code rules
74
+ "jsx-text": "warning",
75
+ "jsx-attribute": "warning",
76
+ "ternary-locale": "error",
77
+ "toast-message": "warning",
78
+ "string-variable": "warning",
79
+ };
80
+ //# sourceMappingURL=categories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"categories.js","sourceRoot":"","sources":["../../src/rules/categories.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAA+C;IAC3E,qEAAqE;IACrE,sBAAsB,EAAE,UAAU;IAClC,aAAa,EAAE,aAAa;IAC5B,sBAAsB,EAAE,SAAS;IACjC,oBAAoB,EAAE,SAAS;IAC/B,YAAY,EAAE,WAAW;IACzB,qBAAqB,EAAE,WAAW;IAClC,aAAa,EAAE,WAAW;IAE1B,qEAAqE;IACrE,UAAU,EAAE,MAAM;IAClB,eAAe,EAAE,MAAM;IACvB,gBAAgB,EAAE,MAAM;IACxB,eAAe,EAAE,MAAM;IACvB,iBAAiB,EAAE,MAAM;CAC1B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAA2B;IACnD,qEAAqE;IACrE,sBAAsB,EACpB,qFAAqF;IACvF,aAAa,EACX,sFAAsF;IACxF,sBAAsB,EACpB,2HAA2H;IAC7H,oBAAoB,EAClB,yFAAyF;IAC3F,YAAY,EACV,mGAAmG;IACrG,qBAAqB,EACnB,+FAA+F;IACjG,aAAa,EACX,4DAA4D;IAE9D,qEAAqE;IACrE,UAAU,EACR,wEAAwE;IAC1E,eAAe,EACb,8EAA8E;IAChF,gBAAgB,EACd,wFAAwF;IAC1F,eAAe,EACb,mEAAmE;IACrE,iBAAiB,EACf,sEAAsE;CACzE,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAA+C;IAC/E,oEAAoE;IACpE,sBAAsB,EAAE,OAAO;IAC/B,aAAa,EAAE,SAAS;IACxB,sBAAsB,EAAE,OAAO;IAC/B,oBAAoB,EAAE,SAAS;IAC/B,YAAY,EAAE,SAAS;IACvB,qBAAqB,EAAE,MAAM;IAC7B,aAAa,EAAE,OAAO;IAEtB,aAAa;IACb,UAAU,EAAE,SAAS;IACrB,eAAe,EAAE,SAAS;IAC1B,gBAAgB,EAAE,OAAO;IACzB,eAAe,EAAE,SAAS;IAC1B,iBAAiB,EAAE,SAAS;CAC7B,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Code detection rules — detect hardcoded strings in source code
3
+ */
4
+ export { checkJsxText } from "./jsx-text.js";
5
+ export { checkJsxAttribute } from "./jsx-attribute.js";
6
+ export { checkTernaryLocale } from "./ternary-locale.js";
7
+ export { checkToastMessage } from "./toast-message.js";
8
+ export { checkStringVariable } from "./string-variable.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/rules/code/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Code detection rules — detect hardcoded strings in source code
3
+ */
4
+ export { checkJsxText } from "./jsx-text.js";
5
+ export { checkJsxAttribute } from "./jsx-attribute.js";
6
+ export { checkTernaryLocale } from "./ternary-locale.js";
7
+ export { checkToastMessage } from "./toast-message.js";
8
+ export { checkStringVariable } from "./string-variable.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rules/code/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * JSX Attribute detection rule
3
+ *
4
+ * Detects hardcoded strings in title, alt, placeholder, etc.
5
+ */
6
+ import ts from "typescript";
7
+ import type { Issue, RuleContext } from "../../analyzer/types.js";
8
+ /**
9
+ * Check JSX attribute for hardcoded strings
10
+ */
11
+ export declare function checkJsxAttribute(node: ts.JsxAttribute, ctx: RuleContext): Issue | null;
12
+ //# sourceMappingURL=jsx-attribute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsx-attribute.d.ts","sourceRoot":"","sources":["../../../src/rules/code/jsx-attribute.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AA8BlE;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,EAAE,CAAC,YAAY,EACrB,GAAG,EAAE,WAAW,GACf,KAAK,GAAG,IAAI,CA6Cd"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * JSX Attribute detection rule
3
+ *
4
+ * Detects hardcoded strings in title, alt, placeholder, etc.
5
+ */
6
+ import ts from "typescript";
7
+ import { generateKeyFromContext, truncate } from "../../utils/text.js";
8
+ /**
9
+ * Attributes to check for hardcoded strings
10
+ */
11
+ const CHECK_ATTRIBUTES = new Set([
12
+ "title",
13
+ "alt",
14
+ "placeholder",
15
+ "aria-label",
16
+ "label",
17
+ ]);
18
+ /**
19
+ * Attributes to ignore (never flag these)
20
+ */
21
+ const IGNORE_ATTRIBUTES = new Set([
22
+ "className",
23
+ "class",
24
+ "id",
25
+ "key",
26
+ "ref",
27
+ "data-testid",
28
+ "href",
29
+ "src",
30
+ "name",
31
+ "type",
32
+ "role",
33
+ ]);
34
+ /**
35
+ * Check JSX attribute for hardcoded strings
36
+ */
37
+ export function checkJsxAttribute(node, ctx) {
38
+ const attrName = node.name.getText();
39
+ // Skip ignored attributes
40
+ if (IGNORE_ATTRIBUTES.has(attrName))
41
+ return null;
42
+ // Only check specific attributes
43
+ if (!CHECK_ATTRIBUTES.has(attrName))
44
+ return null;
45
+ // Get the value
46
+ const value = node.initializer;
47
+ if (!value)
48
+ return null;
49
+ let text = null;
50
+ // String literal: title="Hello"
51
+ if (ts.isStringLiteral(value)) {
52
+ text = value.text;
53
+ }
54
+ // JSX expression with string: title={"Hello"}
55
+ if (ts.isJsxExpression(value) && value.expression) {
56
+ if (ts.isStringLiteral(value.expression)) {
57
+ text = value.expression.text;
58
+ }
59
+ }
60
+ // No text found or too short
61
+ if (!text || text.length <= 2)
62
+ return null;
63
+ // Skip URLs
64
+ if (text.startsWith("http") || text.startsWith("/"))
65
+ return null;
66
+ const pos = ctx.sourceFile.getLineAndCharacterOfPosition(node.getStart());
67
+ return {
68
+ file: ctx.filePath,
69
+ line: pos.line + 1,
70
+ column: pos.character + 1,
71
+ text,
72
+ type: "jsx-attribute",
73
+ severity: "warning",
74
+ message: `Hardcoded ${attrName}: "${truncate(text, 40)}"`,
75
+ suggestedKey: generateKeyFromContext(text, ctx.filePath),
76
+ };
77
+ }
78
+ //# sourceMappingURL=jsx-attribute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsx-attribute.js","sourceRoot":"","sources":["../../../src/rules/code/jsx-attribute.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGvE;;GAEG;AACH,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,OAAO;IACP,KAAK;IACL,aAAa;IACb,YAAY;IACZ,OAAO;CACR,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC;IAChC,WAAW;IACX,OAAO;IACP,IAAI;IACJ,KAAK;IACL,KAAK;IACL,aAAa;IACb,MAAM;IACN,KAAK;IACL,MAAM;IACN,MAAM;IACN,MAAM;CACP,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAqB,EACrB,GAAgB;IAEhB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;IAErC,0BAA0B;IAC1B,IAAI,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAEjD,iCAAiC;IACjC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAEjD,gBAAgB;IAChB,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC;IAC/B,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,IAAI,IAAI,GAAkB,IAAI,CAAC;IAE/B,gCAAgC;IAChC,IAAI,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;IACpB,CAAC;IAED,8CAA8C;IAC9C,IAAI,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;QAClD,IAAI,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;YACzC,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,6BAA6B;IAC7B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAE3C,YAAY;IACZ,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAEjE,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,CAAC,6BAA6B,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IAE1E,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,QAAQ;QAClB,IAAI,EAAE,GAAG,CAAC,IAAI,GAAG,CAAC;QAClB,MAAM,EAAE,GAAG,CAAC,SAAS,GAAG,CAAC;QACzB,IAAI;QACJ,IAAI,EAAE,eAAe;QACrB,QAAQ,EAAE,SAAS;QACnB,OAAO,EAAE,aAAa,QAAQ,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG;QACzD,YAAY,EAAE,sBAAsB,CAAC,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC;KACzD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * JSX Text detection rule
3
+ *
4
+ * Detects hardcoded text content in JSX elements
5
+ */
6
+ import ts from "typescript";
7
+ import type { Issue, RuleContext } from "../../analyzer/types.js";
8
+ /**
9
+ * Check JSX text node for hardcoded strings
10
+ */
11
+ export declare function checkJsxText(node: ts.JsxText, ctx: RuleContext): Issue | null;
12
+ //# sourceMappingURL=jsx-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsx-text.d.ts","sourceRoot":"","sources":["../../../src/rules/code/jsx-text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAiBlE;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,WAAW,GAAG,KAAK,GAAG,IAAI,CAwB7E"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * JSX Text detection rule
3
+ *
4
+ * Detects hardcoded text content in JSX elements
5
+ */
6
+ import { generateKeyFromContext, truncate } from "../../utils/text.js";
7
+ /**
8
+ * Default patterns to ignore
9
+ */
10
+ const IGNORE_PATTERNS = [
11
+ /^[\s\n\r\t]+$/, // Whitespace only
12
+ /^[→←↑↓★•·\-–—/\\|,;:.!?()[\]{}]+$/, // Symbols only
13
+ /^\d+[+%KkMm]?$/, // Numbers with optional suffix
14
+ /^[A-Z_]+$/, // SCREAMING_CASE
15
+ /^https?:\/\//, // URLs
16
+ /^\//, // Paths
17
+ /^[a-z-]+$/, // CSS-like (lowercase with hyphens only)
18
+ /^&[a-z]+;$/, // HTML entities like &quot; &amp; &nbsp;
19
+ /^&#\d+;$/, // Numeric HTML entities like &#39;
20
+ ];
21
+ /**
22
+ * Check JSX text node for hardcoded strings
23
+ */
24
+ export function checkJsxText(node, ctx) {
25
+ const text = node.text.trim();
26
+ // Skip empty or very short
27
+ if (!text || text.length <= 2)
28
+ return null;
29
+ // Skip ignored patterns
30
+ for (const pattern of IGNORE_PATTERNS) {
31
+ if (pattern.test(text))
32
+ return null;
33
+ }
34
+ // Get position
35
+ const pos = ctx.sourceFile.getLineAndCharacterOfPosition(node.getStart());
36
+ return {
37
+ file: ctx.filePath,
38
+ line: pos.line + 1,
39
+ column: pos.character + 1,
40
+ text,
41
+ type: "jsx-text",
42
+ severity: "warning",
43
+ message: `Hardcoded text: "${truncate(text, 40)}"`,
44
+ suggestedKey: generateKeyFromContext(text, ctx.filePath),
45
+ };
46
+ }
47
+ //# sourceMappingURL=jsx-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsx-text.js","sourceRoot":"","sources":["../../../src/rules/code/jsx-text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGvE;;GAEG;AACH,MAAM,eAAe,GAAG;IACtB,eAAe,EAAE,kBAAkB;IACnC,mCAAmC,EAAE,eAAe;IACpD,gBAAgB,EAAE,+BAA+B;IACjD,WAAW,EAAE,iBAAiB;IAC9B,cAAc,EAAE,OAAO;IACvB,KAAK,EAAE,QAAQ;IACf,WAAW,EAAE,yCAAyC;IACtD,YAAY,EAAE,yCAAyC;IACvD,UAAU,EAAE,mCAAmC;CAChD,CAAC;AAEF;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,IAAgB,EAAE,GAAgB;IAC7D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAE9B,2BAA2B;IAC3B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAE3C,wBAAwB;IACxB,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;QACtC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACtC,CAAC;IAED,eAAe;IACf,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,CAAC,6BAA6B,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IAE1E,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,QAAQ;QAClB,IAAI,EAAE,GAAG,CAAC,IAAI,GAAG,CAAC;QAClB,MAAM,EAAE,GAAG,CAAC,SAAS,GAAG,CAAC;QACzB,IAAI;QACJ,IAAI,EAAE,UAAU;QAChB,QAAQ,EAAE,SAAS;QACnB,OAAO,EAAE,oBAAoB,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG;QAClD,YAAY,EAAE,sBAAsB,CAAC,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC;KACzD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * String variable detection rule
3
+ *
4
+ * Detects hardcoded user-facing strings assigned to variables
5
+ * Uses strict filtering to avoid false positives
6
+ */
7
+ import ts from "typescript";
8
+ import type { Issue, RuleContext } from "../../analyzer/types.js";
9
+ /**
10
+ * Check for string variable assignments with hardcoded user-facing text
11
+ */
12
+ export declare function checkStringVariable(node: ts.VariableDeclaration, ctx: RuleContext): Issue | null;
13
+ //# sourceMappingURL=string-variable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"string-variable.d.ts","sourceRoot":"","sources":["../../../src/rules/code/string-variable.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AA0ElE;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,EAAE,CAAC,mBAAmB,EAC5B,GAAG,EAAE,WAAW,GACf,KAAK,GAAG,IAAI,CA6Cd"}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * String variable detection rule
3
+ *
4
+ * Detects hardcoded user-facing strings assigned to variables
5
+ * Uses strict filtering to avoid false positives
6
+ */
7
+ import ts from "typescript";
8
+ import { generateKeyFromContext, truncate } from "../../utils/text.js";
9
+ /**
10
+ * Patterns to IGNORE (not user-facing text)
11
+ */
12
+ const IGNORE_PATTERNS = [
13
+ /^[A-Z_][A-Z0-9_]*$/, // SCREAMING_CASE constants
14
+ /^[a-z][a-z0-9]*$/, // Single lowercase word (identifiers)
15
+ /^[a-z-]+$/, // CSS-like (lowercase with hyphens)
16
+ /^[a-z_]+$/, // snake_case identifiers
17
+ /^https?:\/\//, // URLs
18
+ /^\/[\w/.-]+$/, // Paths
19
+ /^\d+(\.\d+)?(px|rem|em|%|vh|vw|ms|s)?$/, // Numbers with units
20
+ /^#[0-9a-fA-F]{3,8}$/, // Hex colors
21
+ /^rgba?\(/, // RGB colors
22
+ /^hsla?\(/, // HSL colors
23
+ /^[\w-]+:[\w-]+$/, // CSS-like property:value
24
+ /^\s*$/, // Whitespace only
25
+ /^[{}[\]()]+$/, // Brackets only
26
+ ];
27
+ /**
28
+ * Variable names that typically hold non-translatable values
29
+ */
30
+ const IGNORE_VARIABLE_NAMES = new Set([
31
+ "id",
32
+ "key",
33
+ "type",
34
+ "name",
35
+ "className",
36
+ "class",
37
+ "style",
38
+ "styles",
39
+ "href",
40
+ "src",
41
+ "url",
42
+ "path",
43
+ "route",
44
+ "endpoint",
45
+ "query",
46
+ "params",
47
+ "config",
48
+ "options",
49
+ "settings",
50
+ "env",
51
+ "mode",
52
+ "format",
53
+ "variant",
54
+ "size",
55
+ "color",
56
+ "status",
57
+ "state",
58
+ "icon",
59
+ "target",
60
+ "rel",
61
+ "method",
62
+ "headers",
63
+ "contentType",
64
+ "mimeType",
65
+ "encoding",
66
+ "charset",
67
+ "locale",
68
+ "lang",
69
+ "language",
70
+ "namespace",
71
+ "ns",
72
+ "prefix",
73
+ "suffix",
74
+ "extension",
75
+ "ext",
76
+ "version",
77
+ "v",
78
+ ]);
79
+ /**
80
+ * Check for string variable assignments with hardcoded user-facing text
81
+ */
82
+ export function checkStringVariable(node, ctx) {
83
+ // Must have an initializer
84
+ if (!node.initializer)
85
+ return null;
86
+ // Must be a string literal
87
+ if (!ts.isStringLiteral(node.initializer))
88
+ return null;
89
+ const text = node.initializer.text;
90
+ // Skip if it doesn't look like user-facing text
91
+ if (!isUserFacingText(text))
92
+ return null;
93
+ // Skip if variable name suggests non-translatable content
94
+ if (ts.isIdentifier(node.name)) {
95
+ const varName = node.name.text.toLowerCase();
96
+ if (IGNORE_VARIABLE_NAMES.has(varName))
97
+ return null;
98
+ // Skip if variable name ends with common technical suffixes
99
+ if (varName.endsWith("id") ||
100
+ varName.endsWith("key") ||
101
+ varName.endsWith("class") ||
102
+ varName.endsWith("style") ||
103
+ varName.endsWith("url") ||
104
+ varName.endsWith("path") ||
105
+ varName.endsWith("type")) {
106
+ return null;
107
+ }
108
+ }
109
+ const pos = ctx.sourceFile.getLineAndCharacterOfPosition(node.initializer.getStart());
110
+ return {
111
+ file: ctx.filePath,
112
+ line: pos.line + 1,
113
+ column: pos.character + 1,
114
+ text,
115
+ type: "string-variable",
116
+ severity: "warning",
117
+ message: `Hardcoded string: "${truncate(text, 40)}"`,
118
+ suggestedKey: generateKeyFromContext(text, ctx.filePath),
119
+ };
120
+ }
121
+ /**
122
+ * Determine if a string looks like user-facing text
123
+ * - Must be at least 5 characters
124
+ * - Must start with a capital letter OR contain a space
125
+ * - Must not match any ignore patterns
126
+ */
127
+ function isUserFacingText(text) {
128
+ // Too short
129
+ if (text.length < 5)
130
+ return false;
131
+ // Check ignore patterns
132
+ for (const pattern of IGNORE_PATTERNS) {
133
+ if (pattern.test(text))
134
+ return false;
135
+ }
136
+ // Must either:
137
+ // 1. Start with capital letter and have length >= 5, OR
138
+ // 2. Contain at least one space (indicates a phrase)
139
+ const startsWithCapital = /^[A-Z]/.test(text);
140
+ const hasSpace = /\s/.test(text);
141
+ if (!startsWithCapital && !hasSpace)
142
+ return false;
143
+ // Additional check: if no space, must look like a proper word/title
144
+ if (!hasSpace) {
145
+ const technicalWords = [
146
+ "Primary",
147
+ "Secondary",
148
+ "Default",
149
+ "None",
150
+ "True",
151
+ "False",
152
+ "Null",
153
+ "Undefined",
154
+ "Material",
155
+ "Outlined",
156
+ "Contained",
157
+ "Text",
158
+ "Small",
159
+ "Medium",
160
+ "Large",
161
+ "Left",
162
+ "Right",
163
+ "Center",
164
+ "Top",
165
+ "Bottom",
166
+ "Start",
167
+ "End",
168
+ "Vertical",
169
+ "Horizontal",
170
+ "Enabled",
171
+ "Disabled",
172
+ "Active",
173
+ "Inactive",
174
+ "Loading",
175
+ "Success",
176
+ "Error",
177
+ "Warning",
178
+ "Info",
179
+ ];
180
+ if (technicalWords.includes(text))
181
+ return false;
182
+ }
183
+ return true;
184
+ }
185
+ //# sourceMappingURL=string-variable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"string-variable.js","sourceRoot":"","sources":["../../../src/rules/code/string-variable.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGvE;;GAEG;AACH,MAAM,eAAe,GAAG;IACtB,oBAAoB,EAAE,2BAA2B;IACjD,kBAAkB,EAAE,sCAAsC;IAC1D,WAAW,EAAE,oCAAoC;IACjD,WAAW,EAAE,yBAAyB;IACtC,cAAc,EAAE,OAAO;IACvB,cAAc,EAAE,QAAQ;IACxB,wCAAwC,EAAE,qBAAqB;IAC/D,qBAAqB,EAAE,aAAa;IACpC,UAAU,EAAE,aAAa;IACzB,UAAU,EAAE,aAAa;IACzB,iBAAiB,EAAE,0BAA0B;IAC7C,OAAO,EAAE,kBAAkB;IAC3B,cAAc,EAAE,gBAAgB;CACjC,CAAC;AAEF;;GAEG;AACH,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC;IACpC,IAAI;IACJ,KAAK;IACL,MAAM;IACN,MAAM;IACN,WAAW;IACX,OAAO;IACP,OAAO;IACP,QAAQ;IACR,MAAM;IACN,KAAK;IACL,KAAK;IACL,MAAM;IACN,OAAO;IACP,UAAU;IACV,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,UAAU;IACV,KAAK;IACL,MAAM;IACN,QAAQ;IACR,SAAS;IACT,MAAM;IACN,OAAO;IACP,QAAQ;IACR,OAAO;IACP,MAAM;IACN,QAAQ;IACR,KAAK;IACL,QAAQ;IACR,SAAS;IACT,aAAa;IACb,UAAU;IACV,UAAU;IACV,SAAS;IACT,QAAQ;IACR,MAAM;IACN,UAAU;IACV,WAAW;IACX,IAAI;IACJ,QAAQ;IACR,QAAQ;IACR,WAAW;IACX,KAAK;IACL,SAAS;IACT,GAAG;CACJ,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAA4B,EAC5B,GAAgB;IAEhB,2BAA2B;IAC3B,IAAI,CAAC,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAEnC,2BAA2B;IAC3B,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;IAEnC,gDAAgD;IAChD,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzC,0DAA0D;IAC1D,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QAC7C,IAAI,qBAAqB,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QAEpD,4DAA4D;QAC5D,IACE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;YACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;YACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;YACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;YACzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;YACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;YACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EACxB,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,CAAC,6BAA6B,CACtD,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,CAC5B,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,QAAQ;QAClB,IAAI,EAAE,GAAG,CAAC,IAAI,GAAG,CAAC;QAClB,MAAM,EAAE,GAAG,CAAC,SAAS,GAAG,CAAC;QACzB,IAAI;QACJ,IAAI,EAAE,iBAAiB;QACvB,QAAQ,EAAE,SAAS;QACnB,OAAO,EAAE,sBAAsB,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG;QACpD,YAAY,EAAE,sBAAsB,CAAC,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC;KACzD,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,YAAY;IACZ,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAElC,wBAAwB;IACxB,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;QACtC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;IACvC,CAAC;IAED,eAAe;IACf,wDAAwD;IACxD,qDAAqD;IACrD,MAAM,iBAAiB,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEjC,IAAI,CAAC,iBAAiB,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAElD,oEAAoE;IACpE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,cAAc,GAAG;YACrB,SAAS;YACT,WAAW;YACX,SAAS;YACT,MAAM;YACN,MAAM;YACN,OAAO;YACP,MAAM;YACN,WAAW;YACX,UAAU;YACV,UAAU;YACV,WAAW;YACX,MAAM;YACN,OAAO;YACP,QAAQ;YACR,OAAO;YACP,MAAM;YACN,OAAO;YACP,QAAQ;YACR,KAAK;YACL,QAAQ;YACR,OAAO;YACP,KAAK;YACL,UAAU;YACV,YAAY;YACZ,SAAS;YACT,UAAU;YACV,QAAQ;YACR,UAAU;YACV,SAAS;YACT,SAAS;YACT,OAAO;YACP,SAAS;YACT,MAAM;SACP,CAAC;QACF,IAAI,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;IAClD,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Ternary locale detection rule
3
+ *
4
+ * Detects anti-pattern: locale === 'en' ? 'Hello' : 'Merhaba'
5
+ */
6
+ import ts from "typescript";
7
+ import type { Issue, RuleContext } from "../../analyzer/types.js";
8
+ /**
9
+ * Check conditional expression for locale-based ternary
10
+ */
11
+ export declare function checkTernaryLocale(node: ts.ConditionalExpression, ctx: RuleContext): Issue | null;
12
+ //# sourceMappingURL=ternary-locale.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ternary-locale.d.ts","sourceRoot":"","sources":["../../../src/rules/code/ternary-locale.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAElE;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,EAAE,CAAC,qBAAqB,EAC9B,GAAG,EAAE,WAAW,GACf,KAAK,GAAG,IAAI,CAgDd"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Ternary locale detection rule
3
+ *
4
+ * Detects anti-pattern: locale === 'en' ? 'Hello' : 'Merhaba'
5
+ */
6
+ import ts from "typescript";
7
+ import { generateKeyFromContext, truncate } from "../../utils/text.js";
8
+ /**
9
+ * Check conditional expression for locale-based ternary
10
+ */
11
+ export function checkTernaryLocale(node, ctx) {
12
+ const condition = node.condition;
13
+ // Check for binary expression
14
+ if (!ts.isBinaryExpression(condition))
15
+ return null;
16
+ // Check if comparing with locale
17
+ const left = condition.left;
18
+ let isLocaleComparison = false;
19
+ // Direct identifier: locale === 'en'
20
+ if (ts.isIdentifier(left) && left.text === "locale") {
21
+ isLocaleComparison = true;
22
+ }
23
+ // Property access: i18n.locale === 'en' or router.locale === 'en'
24
+ if (ts.isPropertyAccessExpression(left) && left.name.text === "locale") {
25
+ isLocaleComparison = true;
26
+ }
27
+ if (!isLocaleComparison)
28
+ return null;
29
+ // Check if both branches have string literals
30
+ const hasStringTrue = ts.isStringLiteral(node.whenTrue);
31
+ const hasStringFalse = ts.isStringLiteral(node.whenFalse);
32
+ if (!hasStringTrue && !hasStringFalse)
33
+ return null;
34
+ // Get text for message
35
+ const text = hasStringTrue
36
+ ? node.whenTrue.text
37
+ : node.whenFalse.text;
38
+ // Ignore empty strings (common in URL construction: locale === 'en' ? '' : locale + '/')
39
+ if (text.trim() === "")
40
+ return null;
41
+ const pos = ctx.sourceFile.getLineAndCharacterOfPosition(node.getStart());
42
+ return {
43
+ file: ctx.filePath,
44
+ line: pos.line + 1,
45
+ column: pos.character + 1,
46
+ text,
47
+ type: "ternary-locale",
48
+ severity: "error", // This is an anti-pattern, so error
49
+ message: `Locale ternary pattern detected: "${truncate(text, 30)}"`,
50
+ suggestedKey: generateKeyFromContext(text, ctx.filePath),
51
+ };
52
+ }
53
+ //# sourceMappingURL=ternary-locale.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ternary-locale.js","sourceRoot":"","sources":["../../../src/rules/code/ternary-locale.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGvE;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAA8B,EAC9B,GAAgB;IAEhB,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IAEjC,8BAA8B;IAC9B,IAAI,CAAC,EAAE,CAAC,kBAAkB,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IAEnD,iCAAiC;IACjC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC;IAC5B,IAAI,kBAAkB,GAAG,KAAK,CAAC;IAE/B,qCAAqC;IACrC,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACpD,kBAAkB,GAAG,IAAI,CAAC;IAC5B,CAAC;IAED,kEAAkE;IAClE,IAAI,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACvE,kBAAkB,GAAG,IAAI,CAAC;IAC5B,CAAC;IAED,IAAI,CAAC,kBAAkB;QAAE,OAAO,IAAI,CAAC;IAErC,8CAA8C;IAC9C,MAAM,aAAa,GAAG,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxD,MAAM,cAAc,GAAG,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAE1D,IAAI,CAAC,aAAa,IAAI,CAAC,cAAc;QAAE,OAAO,IAAI,CAAC;IAEnD,uBAAuB;IACvB,MAAM,IAAI,GAAG,aAAa;QACxB,CAAC,CAAE,IAAI,CAAC,QAA6B,CAAC,IAAI;QAC1C,CAAC,CAAE,IAAI,CAAC,SAA8B,CAAC,IAAI,CAAC;IAE9C,yFAAyF;IACzF,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAEpC,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,CAAC,6BAA6B,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IAE1E,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,QAAQ;QAClB,IAAI,EAAE,GAAG,CAAC,IAAI,GAAG,CAAC;QAClB,MAAM,EAAE,GAAG,CAAC,SAAS,GAAG,CAAC;QACzB,IAAI;QACJ,IAAI,EAAE,gBAAgB;QACtB,QAAQ,EAAE,OAAO,EAAE,oCAAoC;QACvD,OAAO,EAAE,qCAAqC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG;QACnE,YAAY,EAAE,sBAAsB,CAAC,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC;KACzD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Toast message detection rule
3
+ *
4
+ * Detects hardcoded strings in toast function calls
5
+ * Works with: react-hot-toast, sonner, react-toastify
6
+ */
7
+ import ts from "typescript";
8
+ import type { Issue, RuleContext } from "../../analyzer/types.js";
9
+ /**
10
+ * Check for toast function calls with hardcoded strings
11
+ */
12
+ export declare function checkToastMessage(node: ts.CallExpression, ctx: RuleContext): Issue | null;
13
+ //# sourceMappingURL=toast-message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toast-message.d.ts","sourceRoot":"","sources":["../../../src/rules/code/toast-message.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AA2BlE;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,EAAE,CAAC,cAAc,EACvB,GAAG,EAAE,WAAW,GACf,KAAK,GAAG,IAAI,CAoBd"}