@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.
- package/README.md +218 -0
- package/dist/analyzer/file-collector.d.ts +21 -0
- package/dist/analyzer/file-collector.d.ts.map +1 -0
- package/dist/analyzer/file-collector.js +82 -0
- package/dist/analyzer/file-collector.js.map +1 -0
- package/dist/analyzer/index.d.ts +15 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +66 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/analyzer/rules/index.d.ts +7 -0
- package/dist/analyzer/rules/index.d.ts.map +1 -0
- package/dist/analyzer/rules/index.js +7 -0
- package/dist/analyzer/rules/index.js.map +1 -0
- package/dist/analyzer/rules/jsx-attribute.d.ts +12 -0
- package/dist/analyzer/rules/jsx-attribute.d.ts.map +1 -0
- package/dist/analyzer/rules/jsx-attribute.js +78 -0
- package/dist/analyzer/rules/jsx-attribute.js.map +1 -0
- package/dist/analyzer/rules/jsx-text.d.ts +12 -0
- package/dist/analyzer/rules/jsx-text.d.ts.map +1 -0
- package/dist/analyzer/rules/jsx-text.js +45 -0
- package/dist/analyzer/rules/jsx-text.js.map +1 -0
- package/dist/analyzer/rules/ternary-locale.d.ts +12 -0
- package/dist/analyzer/rules/ternary-locale.d.ts.map +1 -0
- package/dist/analyzer/rules/ternary-locale.js +49 -0
- package/dist/analyzer/rules/ternary-locale.js.map +1 -0
- package/dist/analyzer/types.d.ts +47 -0
- package/dist/analyzer/types.d.ts.map +1 -0
- package/dist/analyzer/types.js +5 -0
- package/dist/analyzer/types.js.map +1 -0
- package/dist/commands/scan.d.ts +6 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +74 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/context/detector.d.ts +12 -0
- package/dist/context/detector.d.ts.map +1 -0
- package/dist/context/detector.js +180 -0
- package/dist/context/detector.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/reporters/eslint-style.d.ts +11 -0
- package/dist/reporters/eslint-style.d.ts.map +1 -0
- package/dist/reporters/eslint-style.js +61 -0
- package/dist/reporters/eslint-style.js.map +1 -0
- package/dist/reporters/json.d.ts +9 -0
- package/dist/reporters/json.d.ts.map +1 -0
- package/dist/reporters/json.js +35 -0
- package/dist/reporters/json.js.map +1 -0
- package/dist/utils/colors.d.ts +25 -0
- package/dist/utils/colors.d.ts.map +1 -0
- package/dist/utils/colors.js +40 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/utils/text.d.ts +12 -0
- package/dist/utils/text.d.ts.map +1 -0
- package/dist/utils/text.js +49 -0
- package/dist/utils/text.js.map +1 -0
- 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
|
+
[](https://www.npmjs.com/package/@better-i18n/cli)
|
|
6
|
+
[](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":"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"}
|
|
@@ -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 @@
|
|
|
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"}
|