@better-i18n/cli 0.1.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 (58) hide show
  1. package/README.md +218 -0
  2. package/dist/analyzer/file-collector.d.ts +21 -0
  3. package/dist/analyzer/file-collector.d.ts.map +1 -0
  4. package/dist/analyzer/file-collector.js +82 -0
  5. package/dist/analyzer/file-collector.js.map +1 -0
  6. package/dist/analyzer/index.d.ts +15 -0
  7. package/dist/analyzer/index.d.ts.map +1 -0
  8. package/dist/analyzer/index.js +66 -0
  9. package/dist/analyzer/index.js.map +1 -0
  10. package/dist/analyzer/rules/index.d.ts +7 -0
  11. package/dist/analyzer/rules/index.d.ts.map +1 -0
  12. package/dist/analyzer/rules/index.js +7 -0
  13. package/dist/analyzer/rules/index.js.map +1 -0
  14. package/dist/analyzer/rules/jsx-attribute.d.ts +12 -0
  15. package/dist/analyzer/rules/jsx-attribute.d.ts.map +1 -0
  16. package/dist/analyzer/rules/jsx-attribute.js +78 -0
  17. package/dist/analyzer/rules/jsx-attribute.js.map +1 -0
  18. package/dist/analyzer/rules/jsx-text.d.ts +12 -0
  19. package/dist/analyzer/rules/jsx-text.d.ts.map +1 -0
  20. package/dist/analyzer/rules/jsx-text.js +45 -0
  21. package/dist/analyzer/rules/jsx-text.js.map +1 -0
  22. package/dist/analyzer/rules/ternary-locale.d.ts +12 -0
  23. package/dist/analyzer/rules/ternary-locale.d.ts.map +1 -0
  24. package/dist/analyzer/rules/ternary-locale.js +49 -0
  25. package/dist/analyzer/rules/ternary-locale.js.map +1 -0
  26. package/dist/analyzer/types.d.ts +47 -0
  27. package/dist/analyzer/types.d.ts.map +1 -0
  28. package/dist/analyzer/types.js +5 -0
  29. package/dist/analyzer/types.js.map +1 -0
  30. package/dist/commands/scan.d.ts +6 -0
  31. package/dist/commands/scan.d.ts.map +1 -0
  32. package/dist/commands/scan.js +74 -0
  33. package/dist/commands/scan.js.map +1 -0
  34. package/dist/context/detector.d.ts +12 -0
  35. package/dist/context/detector.d.ts.map +1 -0
  36. package/dist/context/detector.js +180 -0
  37. package/dist/context/detector.js.map +1 -0
  38. package/dist/index.d.ts +9 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +25 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/reporters/eslint-style.d.ts +11 -0
  43. package/dist/reporters/eslint-style.d.ts.map +1 -0
  44. package/dist/reporters/eslint-style.js +61 -0
  45. package/dist/reporters/eslint-style.js.map +1 -0
  46. package/dist/reporters/json.d.ts +9 -0
  47. package/dist/reporters/json.d.ts.map +1 -0
  48. package/dist/reporters/json.js +35 -0
  49. package/dist/reporters/json.js.map +1 -0
  50. package/dist/utils/colors.d.ts +25 -0
  51. package/dist/utils/colors.d.ts.map +1 -0
  52. package/dist/utils/colors.js +40 -0
  53. package/dist/utils/colors.js.map +1 -0
  54. package/dist/utils/text.d.ts +12 -0
  55. package/dist/utils/text.d.ts.map +1 -0
  56. package/dist/utils/text.js +49 -0
  57. package/dist/utils/text.js.map +1 -0
  58. package/package.json +59 -0
package/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # @better-i18n/cli
2
+
3
+ > Detect hardcoded strings in your React/Next.js apps before they become i18n debt.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@better-i18n/cli.svg)](https://www.npmjs.com/package/@better-i18n/cli)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Why?
9
+
10
+ Hardcoded strings slip into codebases easily. Finding them manually is tedious. This CLI automatically scans your React/Next.js code and reports untranslated text—before it ships to production.
11
+
12
+ ```tsx
13
+ // ❌ These get flagged
14
+ <h1>Welcome to our app</h1>
15
+ <button>Click me</button>
16
+ <input placeholder="Enter your name" />
17
+
18
+ // ✅ These are fine
19
+ <h1>{t('welcome')}</h1>
20
+ <button>{t('actions.click')}</button>
21
+ <input placeholder={t('form.namePlaceholder')} />
22
+ ```
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ # Global install
28
+ npm install -g @better-i18n/cli
29
+
30
+ # Or use with npx (no install)
31
+ npx @better-i18n/cli scan
32
+
33
+ # Or add to your project
34
+ npm install -D @better-i18n/cli
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```bash
40
+ # Scan current directory
41
+ better-i18n scan
42
+
43
+ # That's it! The CLI auto-detects your i18n.config.ts
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ```bash
49
+ # Scan specific directory
50
+ better-i18n scan --dir ./src
51
+
52
+ # JSON output (for CI/tooling)
53
+ better-i18n scan --format json
54
+
55
+ # CI mode (exit code 1 if issues found)
56
+ better-i18n scan --ci
57
+
58
+ # Only scan git staged files (for pre-commit hooks)
59
+ better-i18n scan --staged
60
+
61
+ # Verbose output
62
+ better-i18n scan --verbose
63
+ ```
64
+
65
+ ## Example Output
66
+
67
+ ```
68
+ $ better-i18n scan
69
+
70
+ ✓ Project: acme/web-app
71
+ ✓ Found 45 files
72
+
73
+ src/components/Header.tsx
74
+ 12:8 warning "Welcome back" i18n/jsx-text
75
+ 15:18 warning "Profile picture" i18n/jsx-attribute
76
+
77
+ src/pages/login.tsx
78
+ 23:15 error locale === 'en' ? ... : ... i18n/ternary-locale
79
+
80
+ ✖ 3 problems (1 error, 2 warnings)
81
+
82
+ Scanned 45 files in 0.23s
83
+ ```
84
+
85
+ ## Detection Rules
86
+
87
+ | Rule | Severity | What it catches |
88
+ |------|----------|-----------------|
89
+ | `jsx-text` | warning | Hardcoded text inside JSX elements |
90
+ | `jsx-attribute` | warning | Hardcoded `title`, `alt`, `placeholder`, `aria-label` |
91
+ | `ternary-locale` | error | `locale === 'en' ? 'Hello' : 'Hola'` anti-pattern |
92
+
93
+ ## Configuration
94
+
95
+ The CLI automatically reads your `i18n.config.ts` file. No extra config needed!
96
+
97
+ ```ts
98
+ // i18n.config.ts
99
+ import { createI18n } from "@better-i18n/next";
100
+
101
+ export const { useTranslation, I18nProvider } = createI18n({
102
+ project: "your-org/your-project",
103
+ defaultLocale: "en",
104
+
105
+ // Optional: customize lint behavior
106
+ lint: {
107
+ include: ["src/**/*.tsx", "app/**/*.tsx"],
108
+ exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
109
+ rules: {
110
+ "jsx-text": "warning",
111
+ "jsx-attribute": "warning",
112
+ "ternary-locale": "error",
113
+ },
114
+ },
115
+ });
116
+ ```
117
+
118
+ ### Lint Options
119
+
120
+ | Option | Type | Description |
121
+ |--------|------|-------------|
122
+ | `include` | `string[]` | Glob patterns for files to scan |
123
+ | `exclude` | `string[]` | Glob patterns for files to ignore |
124
+ | `rules` | `object` | Rule severity: `"error"`, `"warning"`, or `"off"` |
125
+
126
+ ## CI/CD Integration
127
+
128
+ ### GitHub Actions
129
+
130
+ ```yaml
131
+ # .github/workflows/i18n-lint.yml
132
+ name: i18n Lint
133
+
134
+ on: [push, pull_request]
135
+
136
+ jobs:
137
+ lint:
138
+ runs-on: ubuntu-latest
139
+ steps:
140
+ - uses: actions/checkout@v4
141
+ - uses: actions/setup-node@v4
142
+ with:
143
+ node-version: '20'
144
+ - run: npx @better-i18n/cli scan --ci
145
+ ```
146
+
147
+ ### Pre-commit Hook
148
+
149
+ With [Husky](https://typicode.github.io/husky/):
150
+
151
+ ```bash
152
+ # .husky/pre-commit
153
+ npx @better-i18n/cli scan --staged --ci
154
+ ```
155
+
156
+ Or with [lint-staged](https://github.com/lint-staged/lint-staged):
157
+
158
+ ```json
159
+ {
160
+ "lint-staged": {
161
+ "*.{tsx,jsx}": ["better-i18n scan --ci"]
162
+ }
163
+ }
164
+ ```
165
+
166
+ ## JSON Output
167
+
168
+ Use `--format json` for programmatic access:
169
+
170
+ ```bash
171
+ better-i18n scan --format json
172
+ ```
173
+
174
+ ```json
175
+ {
176
+ "project": {
177
+ "workspaceId": "acme",
178
+ "projectSlug": "web-app",
179
+ "defaultLocale": "en"
180
+ },
181
+ "files": 45,
182
+ "issues": [
183
+ {
184
+ "file": "src/components/Header.tsx",
185
+ "line": 12,
186
+ "column": 8,
187
+ "text": "Welcome back",
188
+ "type": "jsx-text",
189
+ "severity": "warning",
190
+ "message": "Hardcoded text in JSX"
191
+ }
192
+ ],
193
+ "duration": 234
194
+ }
195
+ ```
196
+
197
+ ## What Gets Ignored
198
+
199
+ The CLI is smart about ignoring non-translatable content:
200
+
201
+ - ✅ CSS class names: `className="flex items-center"`
202
+ - ✅ URLs: `href="https://example.com"`
203
+ - ✅ Numbers and constants
204
+ - ✅ Import paths
205
+ - ✅ Code identifiers
206
+ - ✅ Single characters and punctuation
207
+
208
+ ## Part of Better i18n
209
+
210
+ This CLI is part of the [Better i18n](https://better-i18n.com) ecosystem:
211
+
212
+ - **[@better-i18n/next](https://www.npmjs.com/package/@better-i18n/next)** - Next.js i18n SDK
213
+ - **[@better-i18n/cli](https://www.npmjs.com/package/@better-i18n/cli)** - This CLI
214
+ - **[Dashboard](https://better-i18n.com)** - Visual translation management
215
+
216
+ ## License
217
+
218
+ MIT © [Better i18n](https://better-i18n.com)
@@ -0,0 +1,21 @@
1
+ /**
2
+ * File collector
3
+ *
4
+ * Collect TypeScript/JavaScript files for analysis
5
+ */
6
+ export interface CollectOptions {
7
+ rootDir: string;
8
+ include?: string[];
9
+ exclude?: string[];
10
+ extensions?: string[];
11
+ staged?: boolean;
12
+ }
13
+ /**
14
+ * Collect files to analyze
15
+ */
16
+ export declare function collectFiles(options: CollectOptions): Promise<string[]>;
17
+ /**
18
+ * Check if file matches exclude patterns
19
+ */
20
+ export declare function shouldExcludeFile(filePath: string, patterns: string[]): boolean;
21
+ //# sourceMappingURL=file-collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-collector.d.ts","sourceRoot":"","sources":["../../src/analyzer/file-collector.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAeD;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAyB7E;AA+BD;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAAE,GACjB,OAAO,CAmBT"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * File collector
3
+ *
4
+ * Collect TypeScript/JavaScript files for analysis
5
+ */
6
+ import { existsSync, readdirSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ const DEFAULT_INCLUDE = ["src", "app", "components", "pages"];
9
+ const DEFAULT_EXCLUDE = [
10
+ "node_modules",
11
+ ".git",
12
+ ".next",
13
+ "dist",
14
+ "build",
15
+ "out",
16
+ "__tests__",
17
+ "__mocks__",
18
+ ];
19
+ const DEFAULT_EXTENSIONS = [".tsx", ".jsx"];
20
+ /**
21
+ * Collect files to analyze
22
+ */
23
+ export async function collectFiles(options) {
24
+ const { rootDir, include = DEFAULT_INCLUDE, exclude = DEFAULT_EXCLUDE, extensions = DEFAULT_EXTENSIONS, } = options;
25
+ const files = [];
26
+ const excludeSet = new Set(exclude);
27
+ // Check which include directories exist
28
+ const existingDirs = include.filter((dir) => existsSync(join(rootDir, dir)));
29
+ // If no include dirs exist, scan root
30
+ const dirsToScan = existingDirs.length > 0 ? existingDirs : ["."];
31
+ for (const dir of dirsToScan) {
32
+ const fullDir = join(rootDir, dir);
33
+ collectFromDir(fullDir, rootDir, files, excludeSet, extensions);
34
+ }
35
+ return files;
36
+ }
37
+ function collectFromDir(dir, rootDir, files, excludeSet, extensions) {
38
+ try {
39
+ const entries = readdirSync(dir, { withFileTypes: true });
40
+ for (const entry of entries) {
41
+ if (excludeSet.has(entry.name))
42
+ continue;
43
+ const fullPath = join(dir, entry.name);
44
+ if (entry.isDirectory()) {
45
+ collectFromDir(fullPath, rootDir, files, excludeSet, extensions);
46
+ }
47
+ else if (entry.isFile()) {
48
+ const ext = entry.name.slice(entry.name.lastIndexOf("."));
49
+ if (extensions.includes(ext)) {
50
+ files.push(fullPath);
51
+ }
52
+ }
53
+ }
54
+ }
55
+ catch {
56
+ // Ignore permission errors
57
+ }
58
+ }
59
+ /**
60
+ * Check if file matches exclude patterns
61
+ */
62
+ export function shouldExcludeFile(filePath, patterns) {
63
+ const fileName = filePath.split("/").pop() || "";
64
+ for (const pattern of patterns) {
65
+ if (pattern.startsWith("**/*.")) {
66
+ const ext = pattern.slice(4);
67
+ if (fileName.endsWith(ext))
68
+ return true;
69
+ }
70
+ else if (pattern.includes("*")) {
71
+ // Simple glob matching
72
+ const regex = new RegExp("^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$");
73
+ if (regex.test(fileName))
74
+ return true;
75
+ }
76
+ else if (filePath.includes(pattern)) {
77
+ return true;
78
+ }
79
+ }
80
+ return false;
81
+ }
82
+ //# sourceMappingURL=file-collector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-collector.js","sourceRoot":"","sources":["../../src/analyzer/file-collector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Main analyzer module
3
+ *
4
+ * Parses TypeScript/JSX files and applies detection rules
5
+ */
6
+ import type { Issue, LintConfig } from "./types.js";
7
+ /**
8
+ * Analyze a single file for hardcoded strings
9
+ */
10
+ export declare function analyzeFile(filePath: string, config?: LintConfig): Promise<Issue[]>;
11
+ /**
12
+ * Analyze source text (useful for testing)
13
+ */
14
+ export declare function analyzeSourceText(sourceText: string, filePath: string, config?: LintConfig): Issue[];
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analyzer/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAe,MAAM,YAAY,CAAC;AAEjE;;GAEG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,KAAK,EAAE,CAAC,CAGlB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,UAAU,GAClB,KAAK,EAAE,CA2CT"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Main analyzer module
3
+ *
4
+ * Parses TypeScript/JSX files and applies detection rules
5
+ */
6
+ import { readFileSync } from "node:fs";
7
+ import ts from "typescript";
8
+ import { checkJsxAttribute } from "./rules/jsx-attribute.js";
9
+ import { checkJsxText } from "./rules/jsx-text.js";
10
+ import { checkTernaryLocale } from "./rules/ternary-locale.js";
11
+ /**
12
+ * Analyze a single file for hardcoded strings
13
+ */
14
+ export async function analyzeFile(filePath, config) {
15
+ const sourceText = readFileSync(filePath, "utf-8");
16
+ return analyzeSourceText(sourceText, filePath, config);
17
+ }
18
+ /**
19
+ * Analyze source text (useful for testing)
20
+ */
21
+ export function analyzeSourceText(sourceText, filePath, config) {
22
+ const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filePath));
23
+ const issues = [];
24
+ const ctx = { filePath, sourceFile };
25
+ // Check if rules are enabled
26
+ const rules = config?.rules || {};
27
+ const jsxTextEnabled = rules["jsx-text"] !== "off";
28
+ const jsxAttrEnabled = rules["jsx-attribute"] !== "off";
29
+ const ternaryEnabled = rules["ternary-locale"] !== "off";
30
+ function visit(node) {
31
+ // JSX Text
32
+ if (jsxTextEnabled && ts.isJsxText(node)) {
33
+ const issue = checkJsxText(node, ctx);
34
+ if (issue)
35
+ issues.push(issue);
36
+ }
37
+ // JSX Attribute
38
+ if (jsxAttrEnabled && ts.isJsxAttribute(node)) {
39
+ const issue = checkJsxAttribute(node, ctx);
40
+ if (issue)
41
+ issues.push(issue);
42
+ }
43
+ // Ternary with locale
44
+ if (ternaryEnabled && ts.isConditionalExpression(node)) {
45
+ const issue = checkTernaryLocale(node, ctx);
46
+ if (issue)
47
+ issues.push(issue);
48
+ }
49
+ ts.forEachChild(node, visit);
50
+ }
51
+ visit(sourceFile);
52
+ return issues;
53
+ }
54
+ /**
55
+ * Get TypeScript script kind based on file extension
56
+ */
57
+ function getScriptKind(filePath) {
58
+ if (filePath.endsWith(".tsx"))
59
+ return ts.ScriptKind.TSX;
60
+ if (filePath.endsWith(".jsx"))
61
+ return ts.ScriptKind.JSX;
62
+ if (filePath.endsWith(".ts"))
63
+ return ts.ScriptKind.TS;
64
+ return ts.ScriptKind.JS;
65
+ }
66
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/analyzer/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAG/D;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,QAAgB,EAChB,MAAmB;IAEnB,MAAM,UAAU,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACnD,OAAO,iBAAiB,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AACzD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAC/B,UAAkB,EAClB,QAAgB,EAChB,MAAmB;IAEnB,MAAM,UAAU,GAAG,EAAE,CAAC,gBAAgB,CACpC,QAAQ,EACR,UAAU,EACV,EAAE,CAAC,YAAY,CAAC,MAAM,EACtB,IAAI,EACJ,aAAa,CAAC,QAAQ,CAAC,CACxB,CAAC;IAEF,MAAM,MAAM,GAAY,EAAE,CAAC;IAC3B,MAAM,GAAG,GAAgB,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;IAElD,6BAA6B;IAC7B,MAAM,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC;IAClC,MAAM,cAAc,GAAG,KAAK,CAAC,UAAU,CAAC,KAAK,KAAK,CAAC;IACnD,MAAM,cAAc,GAAG,KAAK,CAAC,eAAe,CAAC,KAAK,KAAK,CAAC;IACxD,MAAM,cAAc,GAAG,KAAK,CAAC,gBAAgB,CAAC,KAAK,KAAK,CAAC;IAEzD,SAAS,KAAK,CAAC,IAAa;QAC1B,WAAW;QACX,IAAI,cAAc,IAAI,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;YACzC,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACtC,IAAI,KAAK;gBAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;QAED,gBAAgB;QAChB,IAAI,cAAc,IAAI,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9C,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAC3C,IAAI,KAAK;gBAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;QAED,sBAAsB;QACtB,IAAI,cAAc,IAAI,EAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC,EAAE,CAAC;YACvD,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAC5C,IAAI,KAAK;gBAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;QAED,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,CAAC;IAElB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAS,aAAa,CAAC,QAAgB;IACrC,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;IACxD,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;IACxD,IAAI,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;IACtD,OAAO,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Rules index - exports all detection rules
3
+ */
4
+ export { checkJsxAttribute } from "./jsx-attribute.js";
5
+ export { checkJsxText } from "./jsx-text.js";
6
+ export { checkTernaryLocale } from "./ternary-locale.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/analyzer/rules/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Rules index - exports all detection rules
3
+ */
4
+ export { checkJsxAttribute } from "./jsx-attribute.js";
5
+ export { checkJsxText } from "./jsx-text.js";
6
+ export { checkTernaryLocale } from "./ternary-locale.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/analyzer/rules/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,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 "../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/analyzer/rules/jsx-attribute.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA8BtD;;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/analyzer/rules/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 "../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/analyzer/rules/jsx-text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAetD;;GAEG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,EAAE,CAAC,OAAO,EAChB,GAAG,EAAE,WAAW,GACf,KAAK,GAAG,IAAI,CAwBd"}
@@ -0,0 +1,45 @@
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
+ ];
19
+ /**
20
+ * Check JSX text node for hardcoded strings
21
+ */
22
+ export function checkJsxText(node, ctx) {
23
+ const text = node.text.trim();
24
+ // Skip empty or very short
25
+ if (!text || text.length <= 2)
26
+ return null;
27
+ // Skip ignored patterns
28
+ for (const pattern of IGNORE_PATTERNS) {
29
+ if (pattern.test(text))
30
+ return null;
31
+ }
32
+ // Get position
33
+ const pos = ctx.sourceFile.getLineAndCharacterOfPosition(node.getStart());
34
+ return {
35
+ file: ctx.filePath,
36
+ line: pos.line + 1,
37
+ column: pos.character + 1,
38
+ text,
39
+ type: "jsx-text",
40
+ severity: "warning",
41
+ message: `Hardcoded text: "${truncate(text, 40)}"`,
42
+ suggestedKey: generateKeyFromContext(text, ctx.filePath),
43
+ };
44
+ }
45
+ //# sourceMappingURL=jsx-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsx-text.js","sourceRoot":"","sources":["../../../src/analyzer/rules/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,oCAAoC,EAAE,eAAe;IACrD,gBAAgB,EAAE,+BAA+B;IACjD,WAAW,EAAE,iBAAiB;IAC9B,cAAc,EAAE,OAAO;IACvB,KAAK,EAAE,QAAQ;IACf,WAAW,EAAE,yCAAyC;CACvD,CAAC;AAEF;;GAEG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAgB,EAChB,GAAgB;IAEhB,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,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 "../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/analyzer/rules/ternary-locale.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,EAAE,CAAC,qBAAqB,EAC9B,GAAG,EAAE,WAAW,GACf,KAAK,GAAG,IAAI,CA4Cd"}