@intl-party/eslint-plugin 1.0.0 → 1.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/LICENSE +2 -2
- package/README.md +450 -0
- package/dist/index.d.ts +21 -3
- package/dist/index.js +391 -18
- package/package.json +14 -6
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2025-2026 IntlParty Team
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# @intl-party/eslint-plugin
|
|
2
|
+
|
|
3
|
+
ESLint plugin for IntlParty - enforce best practices and catch common i18n issues in your code.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚫 **No hardcoded strings** - Detect untranslated user-facing text
|
|
8
|
+
- 🔍 **Missing translation keys** - Catch references to non-existent translation keys
|
|
9
|
+
- ⚛️ **React hooks enforcement** - Prefer translation hooks over direct i18n usage
|
|
10
|
+
- 📝 **Consistent patterns** - Enforce consistent translation patterns across your codebase
|
|
11
|
+
- ⚙️ **Configurable rules** - Customize rules to fit your project needs
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install --save-dev @intl-party/eslint-plugin
|
|
17
|
+
# or
|
|
18
|
+
pnpm add -D @intl-party/eslint-plugin
|
|
19
|
+
# or
|
|
20
|
+
yarn add --dev @intl-party/eslint-plugin
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
### Basic Setup
|
|
26
|
+
|
|
27
|
+
Add the plugin to your ESLint configuration:
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
// .eslintrc.js
|
|
31
|
+
module.exports = {
|
|
32
|
+
plugins: ["@intl-party"],
|
|
33
|
+
extends: ["@intl-party/recommended"],
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Manual Configuration
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
// .eslintrc.js
|
|
41
|
+
module.exports = {
|
|
42
|
+
plugins: ["@intl-party"],
|
|
43
|
+
rules: {
|
|
44
|
+
"@intl-party/no-hardcoded-strings": "error",
|
|
45
|
+
"@intl-party/no-missing-keys": "error",
|
|
46
|
+
"@intl-party/prefer-translation-hooks": "warn",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### TypeScript Configuration
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
// .eslintrc.js
|
|
55
|
+
module.exports = {
|
|
56
|
+
extends: ["@intl-party/recommended", "@intl-party/typescript"],
|
|
57
|
+
parserOptions: {
|
|
58
|
+
project: "./tsconfig.json",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Rules
|
|
64
|
+
|
|
65
|
+
### `@intl-party/no-hardcoded-strings`
|
|
66
|
+
|
|
67
|
+
Prevents hardcoded user-facing strings that should be translated.
|
|
68
|
+
|
|
69
|
+
#### ❌ Incorrect
|
|
70
|
+
|
|
71
|
+
```jsx
|
|
72
|
+
function Welcome() {
|
|
73
|
+
return <h1>Welcome to our app!</h1>; // Hardcoded string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function Button() {
|
|
77
|
+
return <button>Click here</button>; // Hardcoded string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const message = "Hello world"; // Hardcoded string
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### ✅ Correct
|
|
84
|
+
|
|
85
|
+
```jsx
|
|
86
|
+
function Welcome() {
|
|
87
|
+
const t = useTranslations("common");
|
|
88
|
+
return <h1>{t("welcome")}</h1>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function Button() {
|
|
92
|
+
const t = useTranslations("common");
|
|
93
|
+
return <button>{t("clickHere")}</button>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const message = t("hello");
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### Configuration
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
{
|
|
103
|
+
"@intl-party/no-hardcoded-strings": ["error", {
|
|
104
|
+
"ignorePatterns": [
|
|
105
|
+
"^\\d+$", // Numbers
|
|
106
|
+
"^[A-Z_]+$", // Constants
|
|
107
|
+
"^https?://", // URLs
|
|
108
|
+
"className", // CSS classes
|
|
109
|
+
"data-*" // Data attributes
|
|
110
|
+
],
|
|
111
|
+
"ignoreElements": ["script", "style"],
|
|
112
|
+
"ignoreAttributes": ["className", "id", "data-testid"]
|
|
113
|
+
}]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `@intl-party/no-missing-keys`
|
|
118
|
+
|
|
119
|
+
Catches references to translation keys that don't exist in your translation files.
|
|
120
|
+
|
|
121
|
+
#### ❌ Incorrect
|
|
122
|
+
|
|
123
|
+
```jsx
|
|
124
|
+
function Component() {
|
|
125
|
+
const t = useTranslations("common");
|
|
126
|
+
return <h1>{t("nonExistentKey")}</h1>; // Key doesn't exist
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### ✅ Correct
|
|
131
|
+
|
|
132
|
+
```jsx
|
|
133
|
+
function Component() {
|
|
134
|
+
const t = useTranslations("common");
|
|
135
|
+
return <h1>{t("welcome")}</h1>; // Key exists in common namespace
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### Configuration
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
{
|
|
143
|
+
"@intl-party/no-missing-keys": ["error", {
|
|
144
|
+
"translationsPath": "./messages",
|
|
145
|
+
"defaultLocale": "en",
|
|
146
|
+
"namespaces": ["common", "navigation"],
|
|
147
|
+
"checkDynamicKeys": false
|
|
148
|
+
}]
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `@intl-party/prefer-translation-hooks`
|
|
153
|
+
|
|
154
|
+
Encourages using translation hooks instead of direct i18n instance usage in React components.
|
|
155
|
+
|
|
156
|
+
#### ❌ Incorrect
|
|
157
|
+
|
|
158
|
+
```jsx
|
|
159
|
+
function Component() {
|
|
160
|
+
const { i18n } = useI18nContext();
|
|
161
|
+
return <h1>{i18n.t("welcome")}</h1>; // Direct i18n usage
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### ✅ Correct
|
|
166
|
+
|
|
167
|
+
```jsx
|
|
168
|
+
function Component() {
|
|
169
|
+
const t = useTranslations("common");
|
|
170
|
+
return <h1>{t("welcome")}</h1>; // Using translation hook
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
#### Configuration
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
{
|
|
178
|
+
"@intl-party/prefer-translation-hooks": ["warn", {
|
|
179
|
+
"allowedMethods": ["formatDate", "formatNumber"],
|
|
180
|
+
"ignoreServerComponents": true
|
|
181
|
+
}]
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Configuration Presets
|
|
186
|
+
|
|
187
|
+
### Recommended Preset
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
// Balanced rules for most projects
|
|
191
|
+
{
|
|
192
|
+
"extends": ["@intl-party/recommended"]
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Includes:
|
|
197
|
+
|
|
198
|
+
- `@intl-party/no-hardcoded-strings`: `error`
|
|
199
|
+
- `@intl-party/no-missing-keys`: `error`
|
|
200
|
+
- `@intl-party/prefer-translation-hooks`: `warn`
|
|
201
|
+
|
|
202
|
+
### Strict Preset
|
|
203
|
+
|
|
204
|
+
```javascript
|
|
205
|
+
// Stricter rules for high-quality i18n
|
|
206
|
+
{
|
|
207
|
+
"extends": ["@intl-party/strict"]
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Includes all recommended rules plus:
|
|
212
|
+
|
|
213
|
+
- Stricter hardcoded string detection
|
|
214
|
+
- Enforcement of namespace consistency
|
|
215
|
+
- Required translation comments
|
|
216
|
+
|
|
217
|
+
### TypeScript Preset
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
// Additional rules for TypeScript projects
|
|
221
|
+
{
|
|
222
|
+
"extends": [
|
|
223
|
+
"@intl-party/recommended",
|
|
224
|
+
"@intl-party/typescript"
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Includes type-aware rules and TypeScript-specific checks.
|
|
230
|
+
|
|
231
|
+
## Advanced Configuration
|
|
232
|
+
|
|
233
|
+
### Project-Specific Settings
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
// .eslintrc.js
|
|
237
|
+
module.exports = {
|
|
238
|
+
plugins: ["@intl-party"],
|
|
239
|
+
settings: {
|
|
240
|
+
"intl-party": {
|
|
241
|
+
// Path to translation files
|
|
242
|
+
translationsPath: "./src/locales",
|
|
243
|
+
|
|
244
|
+
// Default locale for key validation
|
|
245
|
+
defaultLocale: "en",
|
|
246
|
+
|
|
247
|
+
// Available namespaces
|
|
248
|
+
namespaces: ["common", "navigation", "forms"],
|
|
249
|
+
|
|
250
|
+
// Translation file pattern
|
|
251
|
+
filePattern: "{locale}/{namespace}.json",
|
|
252
|
+
|
|
253
|
+
// Ignore patterns for hardcoded strings
|
|
254
|
+
ignorePatterns: [
|
|
255
|
+
"^[A-Z_]+$", // Constants
|
|
256
|
+
"^\\d+$", // Numbers
|
|
257
|
+
"^https?://", // URLs
|
|
258
|
+
"^mailto:", // Email links
|
|
259
|
+
"^tel:", // Phone links
|
|
260
|
+
],
|
|
261
|
+
|
|
262
|
+
// Elements to ignore for hardcoded strings
|
|
263
|
+
ignoreElements: ["script", "style", "code", "pre"],
|
|
264
|
+
|
|
265
|
+
// Attributes to ignore
|
|
266
|
+
ignoreAttributes: ["className", "id", "data-*", "aria-*"],
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
rules: {
|
|
270
|
+
"@intl-party/no-hardcoded-strings": "error",
|
|
271
|
+
"@intl-party/no-missing-keys": "error",
|
|
272
|
+
"@intl-party/prefer-translation-hooks": "warn",
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Framework-Specific Configuration
|
|
278
|
+
|
|
279
|
+
#### Next.js
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
// .eslintrc.js
|
|
283
|
+
module.exports = {
|
|
284
|
+
extends: ["next/core-web-vitals", "@intl-party/recommended"],
|
|
285
|
+
settings: {
|
|
286
|
+
"intl-party": {
|
|
287
|
+
translationsPath: "./messages",
|
|
288
|
+
framework: "nextjs",
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### React
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
// .eslintrc.js
|
|
298
|
+
module.exports = {
|
|
299
|
+
extends: ["react-app", "@intl-party/recommended"],
|
|
300
|
+
settings: {
|
|
301
|
+
"intl-party": {
|
|
302
|
+
translationsPath: "./src/translations",
|
|
303
|
+
framework: "react",
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Integration with Build Tools
|
|
310
|
+
|
|
311
|
+
### CI/CD Integration
|
|
312
|
+
|
|
313
|
+
```yaml
|
|
314
|
+
# .github/workflows/lint.yml
|
|
315
|
+
name: Lint
|
|
316
|
+
on: [push, pull_request]
|
|
317
|
+
|
|
318
|
+
jobs:
|
|
319
|
+
lint:
|
|
320
|
+
runs-on: ubuntu-latest
|
|
321
|
+
steps:
|
|
322
|
+
- uses: actions/checkout@v3
|
|
323
|
+
- uses: actions/setup-node@v3
|
|
324
|
+
- run: npm ci
|
|
325
|
+
- run: npm run lint
|
|
326
|
+
- run: npm run lint:i18n # Custom script for i18n-specific linting
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Pre-commit Hooks
|
|
330
|
+
|
|
331
|
+
```json
|
|
332
|
+
// package.json
|
|
333
|
+
{
|
|
334
|
+
"husky": {
|
|
335
|
+
"hooks": {
|
|
336
|
+
"pre-commit": "lint-staged"
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
"lint-staged": {
|
|
340
|
+
"**/*.{js,jsx,ts,tsx}": [
|
|
341
|
+
"eslint --fix",
|
|
342
|
+
"eslint --ext .js,.jsx,.ts,.tsx --config .eslintrc.i18n.js"
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Custom Scripts
|
|
349
|
+
|
|
350
|
+
```json
|
|
351
|
+
// package.json
|
|
352
|
+
{
|
|
353
|
+
"scripts": {
|
|
354
|
+
"lint": "eslint src/",
|
|
355
|
+
"lint:i18n": "eslint src/ --config .eslintrc.i18n.js",
|
|
356
|
+
"lint:fix": "eslint src/ --fix"
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Troubleshooting
|
|
362
|
+
|
|
363
|
+
### Common Issues
|
|
364
|
+
|
|
365
|
+
1. **False positives for hardcoded strings**
|
|
366
|
+
- Add patterns to `ignorePatterns` in rule configuration
|
|
367
|
+
- Use `eslint-disable-next-line` comments for specific cases
|
|
368
|
+
|
|
369
|
+
2. **Missing key errors for dynamic keys**
|
|
370
|
+
- Set `checkDynamicKeys: false` in rule configuration
|
|
371
|
+
- Use template strings for predictable patterns
|
|
372
|
+
|
|
373
|
+
3. **Performance issues with large translation files**
|
|
374
|
+
- Use `translationsPath` setting to optimize file loading
|
|
375
|
+
- Consider splitting large translation files
|
|
376
|
+
|
|
377
|
+
### Debug Mode
|
|
378
|
+
|
|
379
|
+
Enable debug logging to troubleshoot rule issues:
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
DEBUG=eslint-plugin-intl-party eslint src/
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Custom Rule Configuration
|
|
386
|
+
|
|
387
|
+
```javascript
|
|
388
|
+
// For specific files or patterns
|
|
389
|
+
{
|
|
390
|
+
"overrides": [
|
|
391
|
+
{
|
|
392
|
+
"files": ["**/*.test.{js,jsx,ts,tsx}"],
|
|
393
|
+
"rules": {
|
|
394
|
+
"@intl-party/no-hardcoded-strings": "off"
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
"files": ["**/admin/**"],
|
|
399
|
+
"rules": {
|
|
400
|
+
"@intl-party/no-missing-keys": "warn"
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
]
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Examples
|
|
408
|
+
|
|
409
|
+
### Real-world Configuration
|
|
410
|
+
|
|
411
|
+
```javascript
|
|
412
|
+
// .eslintrc.js for a Next.js project
|
|
413
|
+
module.exports = {
|
|
414
|
+
extends: ["next/core-web-vitals", "@intl-party/recommended"],
|
|
415
|
+
settings: {
|
|
416
|
+
"intl-party": {
|
|
417
|
+
translationsPath: "./messages",
|
|
418
|
+
defaultLocale: "en",
|
|
419
|
+
namespaces: ["common", "navigation", "forms", "errors"],
|
|
420
|
+
ignorePatterns: [
|
|
421
|
+
"^[A-Z_]+$",
|
|
422
|
+
"^\\d+(\\.\\d+)?$",
|
|
423
|
+
"^#[0-9a-fA-F]{3,6}$",
|
|
424
|
+
"^rgb\\(",
|
|
425
|
+
"^https?://",
|
|
426
|
+
"^mailto:",
|
|
427
|
+
"^\\+\\d",
|
|
428
|
+
],
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
overrides: [
|
|
432
|
+
{
|
|
433
|
+
files: ["**/*.test.{js,jsx,ts,tsx}", "**/*.stories.{js,jsx,ts,tsx}"],
|
|
434
|
+
rules: {
|
|
435
|
+
"@intl-party/no-hardcoded-strings": "off",
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
files: ["**/admin/**", "**/cms/**"],
|
|
440
|
+
rules: {
|
|
441
|
+
"@intl-party/no-hardcoded-strings": "warn",
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
};
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## License
|
|
449
|
+
|
|
450
|
+
MIT © IntlParty
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
|
|
2
2
|
|
|
3
|
+
interface PreferTranslationHooksOptions {
|
|
4
|
+
allowDirectUsage?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface NoMissingKeysOptions {
|
|
8
|
+
translationFiles?: string[];
|
|
9
|
+
defaultLocale?: string;
|
|
10
|
+
configPath?: string;
|
|
11
|
+
cacheTimeout?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface NoHardcodedStringsOptions {
|
|
15
|
+
attributes?: string[];
|
|
16
|
+
ignorePattern?: string;
|
|
17
|
+
minLength?: number;
|
|
18
|
+
allowedStrings?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
declare const plugin: {
|
|
4
22
|
meta: {
|
|
5
23
|
name: string;
|
|
6
24
|
version: string;
|
|
7
25
|
};
|
|
8
26
|
rules: {
|
|
9
|
-
"no-hardcoded-strings": _typescript_eslint_utils_ts_eslint.RuleModule<"hardcodedString" | "hardcodedStringInAttribute", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
10
|
-
"no-missing-keys": _typescript_eslint_utils_ts_eslint.RuleModule<"missingTranslationKey" | "invalidTranslationKey", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
11
|
-
"prefer-translation-hooks": _typescript_eslint_utils_ts_eslint.RuleModule<"preferUseTranslations" | "preferScopedTranslations", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
27
|
+
"no-hardcoded-strings": _typescript_eslint_utils_ts_eslint.RuleModule<"hardcodedString" | "hardcodedStringInAttribute", [NoHardcodedStringsOptions], _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
28
|
+
"no-missing-keys": _typescript_eslint_utils_ts_eslint.RuleModule<"missingTranslationKey" | "invalidTranslationKey", [NoMissingKeysOptions], _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
29
|
+
"prefer-translation-hooks": _typescript_eslint_utils_ts_eslint.RuleModule<"preferUseTranslations" | "preferScopedTranslations", [PreferTranslationHooksOptions], _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
12
30
|
};
|
|
13
31
|
configs: {
|
|
14
32
|
recommended: {
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -27,14 +37,14 @@ module.exports = __toCommonJS(index_exports);
|
|
|
27
37
|
// src/rules/no-hardcoded-strings.ts
|
|
28
38
|
var import_utils = require("@typescript-eslint/utils");
|
|
29
39
|
var noHardcodedStrings = import_utils.ESLintUtils.RuleCreator(
|
|
30
|
-
(name) => `https://github.com/
|
|
40
|
+
(name) => `https://github.com/RodrigoEspinosa/intl-party/blob/main/packages/eslint-plugin/docs/rules/${name}.md`
|
|
31
41
|
)({
|
|
32
42
|
name: "no-hardcoded-strings",
|
|
33
43
|
meta: {
|
|
34
44
|
type: "problem",
|
|
35
45
|
docs: {
|
|
36
46
|
description: "Disallow hardcoded strings in JSX elements and specific attributes",
|
|
37
|
-
recommended: "
|
|
47
|
+
recommended: "recommended"
|
|
38
48
|
},
|
|
39
49
|
fixable: "code",
|
|
40
50
|
schema: [
|
|
@@ -77,7 +87,7 @@ var noHardcodedStrings = import_utils.ESLintUtils.RuleCreator(
|
|
|
77
87
|
}
|
|
78
88
|
},
|
|
79
89
|
defaultOptions: [{}],
|
|
80
|
-
create(context, [options
|
|
90
|
+
create(context, [options]) {
|
|
81
91
|
const {
|
|
82
92
|
attributes = [
|
|
83
93
|
"placeholder",
|
|
@@ -89,7 +99,7 @@ var noHardcodedStrings = import_utils.ESLintUtils.RuleCreator(
|
|
|
89
99
|
ignorePattern,
|
|
90
100
|
minLength = 3,
|
|
91
101
|
allowedStrings = []
|
|
92
|
-
} = options;
|
|
102
|
+
} = options || {};
|
|
93
103
|
const ignoreRegex = ignorePattern ? new RegExp(ignorePattern) : null;
|
|
94
104
|
function isHardcodedString(value) {
|
|
95
105
|
if (value.length < minLength) return false;
|
|
@@ -117,17 +127,18 @@ var noHardcodedStrings = import_utils.ESLintUtils.RuleCreator(
|
|
|
117
127
|
}
|
|
118
128
|
function checkJSXAttribute(node) {
|
|
119
129
|
if (node.name.type === "JSXIdentifier" && attributes.includes(node.name.name) && node.value?.type === "Literal" && typeof node.value.value === "string" && isHardcodedString(node.value.value)) {
|
|
130
|
+
const literalValue = node.value.value;
|
|
120
131
|
context.report({
|
|
121
132
|
node: node.value,
|
|
122
133
|
messageId: "hardcodedStringInAttribute",
|
|
123
134
|
data: {
|
|
124
|
-
text:
|
|
135
|
+
text: literalValue,
|
|
125
136
|
attribute: node.name.name
|
|
126
137
|
},
|
|
127
138
|
fix(fixer) {
|
|
128
139
|
return fixer.replaceText(
|
|
129
140
|
node.value,
|
|
130
|
-
`{t('${generateTranslationKey(
|
|
141
|
+
`{t('${generateTranslationKey(literalValue)}')}`
|
|
131
142
|
);
|
|
132
143
|
}
|
|
133
144
|
});
|
|
@@ -161,15 +172,304 @@ var noHardcodedStrings = import_utils.ESLintUtils.RuleCreator(
|
|
|
161
172
|
|
|
162
173
|
// src/rules/no-missing-keys.ts
|
|
163
174
|
var import_utils2 = require("@typescript-eslint/utils");
|
|
175
|
+
|
|
176
|
+
// src/utils/translation-utils.ts
|
|
177
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
178
|
+
var import_node_path = __toESM(require("path"));
|
|
179
|
+
var translationCache = /* @__PURE__ */ new Map();
|
|
180
|
+
var TranslationUtils = class {
|
|
181
|
+
constructor(options = {}) {
|
|
182
|
+
this.options = {
|
|
183
|
+
defaultLocale: "en",
|
|
184
|
+
cacheTimeout: 5 * 60 * 1e3,
|
|
185
|
+
// 5 minutes
|
|
186
|
+
...options
|
|
187
|
+
};
|
|
188
|
+
this.cacheTimeout = this.options.cacheTimeout;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Load translations from configuration or provided files
|
|
192
|
+
*/
|
|
193
|
+
async loadTranslations() {
|
|
194
|
+
const cacheKey = this.getCacheKey();
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
const cached = translationCache.get(cacheKey);
|
|
197
|
+
if (cached && now - cached.timestamp < this.cacheTimeout) {
|
|
198
|
+
return cached.translations;
|
|
199
|
+
}
|
|
200
|
+
let translations;
|
|
201
|
+
try {
|
|
202
|
+
translations = await this.loadFromConfig();
|
|
203
|
+
} catch {
|
|
204
|
+
translations = await this.loadFromFiles();
|
|
205
|
+
}
|
|
206
|
+
translationCache.set(cacheKey, {
|
|
207
|
+
translations,
|
|
208
|
+
timestamp: now,
|
|
209
|
+
locales: Object.keys(translations),
|
|
210
|
+
namespaces: this.extractNamespaces(translations)
|
|
211
|
+
});
|
|
212
|
+
return translations;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get all available translation keys for a specific locale and namespace
|
|
216
|
+
*/
|
|
217
|
+
async getTranslationKeys(locale, namespace) {
|
|
218
|
+
const translations = await this.loadTranslations();
|
|
219
|
+
const keys = /* @__PURE__ */ new Set();
|
|
220
|
+
if (namespace) {
|
|
221
|
+
const namespaceTranslations = translations[locale]?.[namespace] || {};
|
|
222
|
+
this.collectKeys(namespaceTranslations, "", keys);
|
|
223
|
+
} else {
|
|
224
|
+
const localeTranslations = translations[locale] || {};
|
|
225
|
+
for (const nsTranslations of Object.values(localeTranslations)) {
|
|
226
|
+
this.collectKeys(nsTranslations, "", keys);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return keys;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Check if a translation key exists
|
|
233
|
+
*/
|
|
234
|
+
async hasTranslationKey(locale, key, namespace) {
|
|
235
|
+
const keys = await this.getTranslationKeys(locale, namespace);
|
|
236
|
+
return keys.has(key);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Get all available locales
|
|
240
|
+
*/
|
|
241
|
+
async getLocales() {
|
|
242
|
+
const translations = await this.loadTranslations();
|
|
243
|
+
return Object.keys(translations);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get all available namespaces for a locale
|
|
247
|
+
*/
|
|
248
|
+
async getNamespaces(locale) {
|
|
249
|
+
const translations = await this.loadTranslations();
|
|
250
|
+
return Object.keys(translations[locale] || {});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Validate translation key format
|
|
254
|
+
*/
|
|
255
|
+
isValidTranslationKey(key) {
|
|
256
|
+
return /^[a-zA-Z][a-zA-Z0-9._-]*$/.test(key);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Extract namespace from a translation key
|
|
260
|
+
*/
|
|
261
|
+
extractNamespace(key) {
|
|
262
|
+
if (key.includes(".")) {
|
|
263
|
+
return key.split(".")[0];
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get the base key (without namespace) from a translation key
|
|
269
|
+
*/
|
|
270
|
+
getBaseKey(key) {
|
|
271
|
+
if (key.includes(".")) {
|
|
272
|
+
return key.split(".").slice(1).join(".");
|
|
273
|
+
}
|
|
274
|
+
return key;
|
|
275
|
+
}
|
|
276
|
+
getCacheKey() {
|
|
277
|
+
return `${this.options.configPath || "default"}-${this.options.defaultLocale}`;
|
|
278
|
+
}
|
|
279
|
+
async loadFromConfig() {
|
|
280
|
+
const configFiles = [
|
|
281
|
+
this.options.configPath,
|
|
282
|
+
"intl-party.config.js",
|
|
283
|
+
"intl-party.config.ts",
|
|
284
|
+
"intl-party.config.json"
|
|
285
|
+
].filter(Boolean);
|
|
286
|
+
for (const configFile of configFiles) {
|
|
287
|
+
if (configFile && await this.pathExists(configFile)) {
|
|
288
|
+
try {
|
|
289
|
+
let config;
|
|
290
|
+
if (configFile.endsWith(".json")) {
|
|
291
|
+
const content = await import_promises.default.readFile(configFile, "utf-8");
|
|
292
|
+
config = JSON.parse(content);
|
|
293
|
+
} else {
|
|
294
|
+
delete require.cache[import_node_path.default.resolve(configFile)];
|
|
295
|
+
config = require(import_node_path.default.resolve(configFile));
|
|
296
|
+
if (config.default) {
|
|
297
|
+
config = config.default;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return await this.loadFromConfigObject(config);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
throw new Error("No valid configuration found");
|
|
307
|
+
}
|
|
308
|
+
async loadFromConfigObject(config) {
|
|
309
|
+
const {
|
|
310
|
+
locales = ["en"],
|
|
311
|
+
defaultLocale = "en",
|
|
312
|
+
messages = "./messages"
|
|
313
|
+
} = config;
|
|
314
|
+
const translations = {};
|
|
315
|
+
for (const locale of locales) {
|
|
316
|
+
translations[locale] = {};
|
|
317
|
+
const messagesPath = typeof messages === "string" ? messages : "./messages";
|
|
318
|
+
if (await this.pathExists(messagesPath)) {
|
|
319
|
+
const localePath = import_node_path.default.join(messagesPath, locale);
|
|
320
|
+
if (await this.pathExists(localePath)) {
|
|
321
|
+
const files = await import_promises.default.readdir(localePath);
|
|
322
|
+
for (const file of files) {
|
|
323
|
+
if (file.endsWith(".json")) {
|
|
324
|
+
const namespace = import_node_path.default.basename(file, ".json");
|
|
325
|
+
const filePath = import_node_path.default.join(localePath, file);
|
|
326
|
+
try {
|
|
327
|
+
const content = await this.readJson(filePath);
|
|
328
|
+
translations[locale][namespace] = content;
|
|
329
|
+
} catch {
|
|
330
|
+
translations[locale][namespace] = {};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return translations;
|
|
338
|
+
}
|
|
339
|
+
async loadFromFiles() {
|
|
340
|
+
const { translationFiles = [], defaultLocale = "en" } = this.options;
|
|
341
|
+
if (translationFiles.length === 0) {
|
|
342
|
+
return await this.autoDetectTranslations();
|
|
343
|
+
}
|
|
344
|
+
const translations = {};
|
|
345
|
+
for (const filePath of translationFiles) {
|
|
346
|
+
try {
|
|
347
|
+
const content = await this.readJson(filePath);
|
|
348
|
+
const { locale, namespace } = this.parseFilePath(filePath);
|
|
349
|
+
if (!translations[locale]) {
|
|
350
|
+
translations[locale] = {};
|
|
351
|
+
}
|
|
352
|
+
translations[locale][namespace] = content;
|
|
353
|
+
} catch {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return translations;
|
|
358
|
+
}
|
|
359
|
+
async autoDetectTranslations() {
|
|
360
|
+
const translations = {};
|
|
361
|
+
const commonPaths = [
|
|
362
|
+
"messages",
|
|
363
|
+
"locales",
|
|
364
|
+
"i18n",
|
|
365
|
+
"public/locales",
|
|
366
|
+
"src/locales",
|
|
367
|
+
"src/translations"
|
|
368
|
+
];
|
|
369
|
+
for (const basePath of commonPaths) {
|
|
370
|
+
if (await this.pathExists(basePath)) {
|
|
371
|
+
try {
|
|
372
|
+
const entries = await import_promises.default.readdir(basePath);
|
|
373
|
+
for (const entry of entries) {
|
|
374
|
+
const entryPath = import_node_path.default.join(basePath, entry);
|
|
375
|
+
const stat = await import_promises.default.stat(entryPath);
|
|
376
|
+
if (stat.isDirectory()) {
|
|
377
|
+
const locale = entry;
|
|
378
|
+
translations[locale] = {};
|
|
379
|
+
const files = await import_promises.default.readdir(entryPath);
|
|
380
|
+
for (const file of files) {
|
|
381
|
+
if (file.endsWith(".json")) {
|
|
382
|
+
const namespace = import_node_path.default.basename(file, ".json");
|
|
383
|
+
const filePath = import_node_path.default.join(entryPath, file);
|
|
384
|
+
try {
|
|
385
|
+
const content = await this.readJson(filePath);
|
|
386
|
+
translations[locale][namespace] = content;
|
|
387
|
+
} catch {
|
|
388
|
+
translations[locale][namespace] = {};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (Object.keys(translations).length > 0) {
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
} catch {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return translations;
|
|
403
|
+
}
|
|
404
|
+
parseFilePath(filePath) {
|
|
405
|
+
const parts = filePath.split(import_node_path.default.sep);
|
|
406
|
+
const fileName = parts[parts.length - 1];
|
|
407
|
+
const namespace = import_node_path.default.basename(fileName, ".json");
|
|
408
|
+
let locale = this.options.defaultLocale;
|
|
409
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
410
|
+
const part = parts[i];
|
|
411
|
+
if (/^[a-z]{2}(-[A-Z]{2})?$/.test(part)) {
|
|
412
|
+
locale = part;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return { locale, namespace };
|
|
417
|
+
}
|
|
418
|
+
collectKeys(obj, prefix, keys) {
|
|
419
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
420
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
421
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
422
|
+
this.collectKeys(value, fullKey, keys);
|
|
423
|
+
} else {
|
|
424
|
+
keys.add(fullKey);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
extractNamespaces(translations) {
|
|
429
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
430
|
+
for (const localeTranslations of Object.values(translations)) {
|
|
431
|
+
for (const namespace of Object.keys(localeTranslations)) {
|
|
432
|
+
namespaces.add(namespace);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return Array.from(namespaces);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Clear the translation cache
|
|
439
|
+
*/
|
|
440
|
+
clearCache() {
|
|
441
|
+
translationCache.clear();
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Check if a path exists (replacement for fs-extra's pathExists)
|
|
445
|
+
*/
|
|
446
|
+
async pathExists(filePath) {
|
|
447
|
+
try {
|
|
448
|
+
await import_promises.default.access(filePath);
|
|
449
|
+
return true;
|
|
450
|
+
} catch {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Read and parse JSON file (replacement for fs-extra's readJson)
|
|
456
|
+
*/
|
|
457
|
+
async readJson(filePath) {
|
|
458
|
+
const content = await import_promises.default.readFile(filePath, "utf-8");
|
|
459
|
+
return JSON.parse(content);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/rules/no-missing-keys.ts
|
|
164
464
|
var noMissingKeys = import_utils2.ESLintUtils.RuleCreator(
|
|
165
|
-
(name) => `https://github.com/
|
|
465
|
+
(name) => `https://github.com/RodrigoEspinosa/intl-party/blob/main/packages/eslint-plugin/docs/rules/${name}.md`
|
|
166
466
|
)({
|
|
167
467
|
name: "no-missing-keys",
|
|
168
468
|
meta: {
|
|
169
469
|
type: "problem",
|
|
170
470
|
docs: {
|
|
171
471
|
description: "Ensure all translation keys exist in translation files",
|
|
172
|
-
recommended: "
|
|
472
|
+
recommended: "recommended"
|
|
173
473
|
},
|
|
174
474
|
schema: [
|
|
175
475
|
{
|
|
@@ -184,6 +484,15 @@ var noMissingKeys = import_utils2.ESLintUtils.RuleCreator(
|
|
|
184
484
|
type: "string",
|
|
185
485
|
default: "en",
|
|
186
486
|
description: "Default locale to check keys against"
|
|
487
|
+
},
|
|
488
|
+
configPath: {
|
|
489
|
+
type: "string",
|
|
490
|
+
description: "Path to intl-party configuration file"
|
|
491
|
+
},
|
|
492
|
+
cacheTimeout: {
|
|
493
|
+
type: "number",
|
|
494
|
+
default: 3e5,
|
|
495
|
+
description: "Cache timeout in milliseconds (default: 5 minutes)"
|
|
187
496
|
}
|
|
188
497
|
},
|
|
189
498
|
additionalProperties: false
|
|
@@ -195,13 +504,24 @@ var noMissingKeys = import_utils2.ESLintUtils.RuleCreator(
|
|
|
195
504
|
}
|
|
196
505
|
},
|
|
197
506
|
defaultOptions: [{}],
|
|
198
|
-
create(context, [options
|
|
199
|
-
const {
|
|
200
|
-
|
|
507
|
+
create(context, [options]) {
|
|
508
|
+
const {
|
|
509
|
+
translationFiles = [],
|
|
510
|
+
defaultLocale = "en",
|
|
511
|
+
configPath,
|
|
512
|
+
cacheTimeout = 3e5
|
|
513
|
+
} = options || {};
|
|
514
|
+
const translationUtils = new TranslationUtils({
|
|
515
|
+
translationFiles,
|
|
516
|
+
defaultLocale,
|
|
517
|
+
configPath,
|
|
518
|
+
cacheTimeout
|
|
519
|
+
});
|
|
520
|
+
const fileTranslationCache = /* @__PURE__ */ new Map();
|
|
201
521
|
function isValidTranslationKey(key) {
|
|
202
|
-
return
|
|
522
|
+
return translationUtils.isValidTranslationKey(key);
|
|
203
523
|
}
|
|
204
|
-
function checkTranslationCall(node) {
|
|
524
|
+
async function checkTranslationCall(node) {
|
|
205
525
|
if (node.callee.type === "Identifier" && node.callee.name === "t" && node.arguments.length > 0 && node.arguments[0].type === "Literal" && typeof node.arguments[0].value === "string") {
|
|
206
526
|
const key = node.arguments[0].value;
|
|
207
527
|
if (!isValidTranslationKey(key)) {
|
|
@@ -212,16 +532,69 @@ var noMissingKeys = import_utils2.ESLintUtils.RuleCreator(
|
|
|
212
532
|
});
|
|
213
533
|
return;
|
|
214
534
|
}
|
|
535
|
+
try {
|
|
536
|
+
const namespace = translationUtils.extractNamespace(key);
|
|
537
|
+
const baseKey = translationUtils.getBaseKey(key);
|
|
538
|
+
const hasKey = await translationUtils.hasTranslationKey(
|
|
539
|
+
defaultLocale,
|
|
540
|
+
namespace ? baseKey : key,
|
|
541
|
+
namespace || void 0
|
|
542
|
+
);
|
|
543
|
+
if (!hasKey) {
|
|
544
|
+
context.report({
|
|
545
|
+
node: node.arguments[0],
|
|
546
|
+
messageId: "missingTranslationKey",
|
|
547
|
+
data: { key }
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
} catch (error) {
|
|
551
|
+
}
|
|
215
552
|
}
|
|
216
553
|
}
|
|
217
554
|
function checkUseTranslationsCall(node) {
|
|
218
|
-
if (node.callee.type === "Identifier" && node.callee.name === "useTranslations") {
|
|
555
|
+
if (node.callee.type === "Identifier" && node.callee.name === "useTranslations" && node.arguments.length > 0 && node.arguments[0].type === "Literal" && typeof node.arguments[0].value === "string") {
|
|
556
|
+
const namespace = node.arguments[0].value;
|
|
557
|
+
translationUtils.getNamespaces(defaultLocale).then((namespaces) => {
|
|
558
|
+
if (!namespaces.includes(namespace)) {
|
|
559
|
+
context.report({
|
|
560
|
+
node: node.arguments[0],
|
|
561
|
+
messageId: "missingTranslationKey",
|
|
562
|
+
data: { key: namespace }
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}).catch(() => {
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function checkTemplateLiteral(node) {
|
|
570
|
+
for (const quasi of node.quasis) {
|
|
571
|
+
const text = quasi.value.raw;
|
|
572
|
+
const keyMatches = text.match(/[a-zA-Z][a-zA-Z0-9._-]*/g);
|
|
573
|
+
if (keyMatches) {
|
|
574
|
+
for (const potentialKey of keyMatches) {
|
|
575
|
+
if (isValidTranslationKey(potentialKey)) {
|
|
576
|
+
translationUtils.hasTranslationKey(defaultLocale, potentialKey).then((hasKey) => {
|
|
577
|
+
if (!hasKey) {
|
|
578
|
+
context.report({
|
|
579
|
+
node: quasi,
|
|
580
|
+
messageId: "missingTranslationKey",
|
|
581
|
+
data: { key: potentialKey }
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}).catch(() => {
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
219
589
|
}
|
|
220
590
|
}
|
|
221
591
|
return {
|
|
222
592
|
CallExpression(node) {
|
|
223
593
|
checkTranslationCall(node);
|
|
224
594
|
checkUseTranslationsCall(node);
|
|
595
|
+
},
|
|
596
|
+
TemplateLiteral(node) {
|
|
597
|
+
checkTemplateLiteral(node);
|
|
225
598
|
}
|
|
226
599
|
};
|
|
227
600
|
}
|
|
@@ -230,14 +603,14 @@ var noMissingKeys = import_utils2.ESLintUtils.RuleCreator(
|
|
|
230
603
|
// src/rules/prefer-translation-hooks.ts
|
|
231
604
|
var import_utils3 = require("@typescript-eslint/utils");
|
|
232
605
|
var preferTranslationHooks = import_utils3.ESLintUtils.RuleCreator(
|
|
233
|
-
(name) => `https://github.com/
|
|
606
|
+
(name) => `https://github.com/RodrigoEspinosa/intl-party/blob/main/packages/eslint-plugin/docs/rules/${name}.md`
|
|
234
607
|
)({
|
|
235
608
|
name: "prefer-translation-hooks",
|
|
236
609
|
meta: {
|
|
237
610
|
type: "suggestion",
|
|
238
611
|
docs: {
|
|
239
612
|
description: "Prefer using translation hooks over direct i18n instance usage in React components",
|
|
240
|
-
recommended: "
|
|
613
|
+
recommended: "recommended"
|
|
241
614
|
},
|
|
242
615
|
fixable: "code",
|
|
243
616
|
schema: [
|
|
@@ -259,8 +632,8 @@ var preferTranslationHooks = import_utils3.ESLintUtils.RuleCreator(
|
|
|
259
632
|
}
|
|
260
633
|
},
|
|
261
634
|
defaultOptions: [{}],
|
|
262
|
-
create(context, [options
|
|
263
|
-
const { allowDirectUsage = false } = options;
|
|
635
|
+
create(context, [options]) {
|
|
636
|
+
const { allowDirectUsage = false } = options || {};
|
|
264
637
|
function checkMemberExpression(node) {
|
|
265
638
|
if (node.object.type === "Identifier" && node.object.name === "i18n" && node.property.type === "Identifier" && node.property.name === "t" && !allowDirectUsage) {
|
|
266
639
|
context.report({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intl-party/eslint-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "ESLint plugin for IntlParty - detect hardcoded strings and enforce i18n best practices",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -15,10 +15,15 @@
|
|
|
15
15
|
"typescript",
|
|
16
16
|
"hardcoded-strings"
|
|
17
17
|
],
|
|
18
|
-
"author": "
|
|
18
|
+
"author": "RodrigoEspinosa",
|
|
19
19
|
"license": "MIT",
|
|
20
|
+
"homepage": "https://github.com/RodrigoEspinosa/intl-party#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/RodrigoEspinosa/intl-party/issues"
|
|
23
|
+
},
|
|
20
24
|
"dependencies": {
|
|
21
|
-
"@typescript-eslint/utils": "^6.15.0"
|
|
25
|
+
"@typescript-eslint/utils": "^6.15.0",
|
|
26
|
+
"fs-extra": "^11.2.0"
|
|
22
27
|
},
|
|
23
28
|
"devDependencies": {
|
|
24
29
|
"@types/eslint": "^8.56.0",
|
|
@@ -37,14 +42,17 @@
|
|
|
37
42
|
},
|
|
38
43
|
"repository": {
|
|
39
44
|
"type": "git",
|
|
40
|
-
"url": "https://github.com/
|
|
45
|
+
"url": "https://github.com/RodrigoEspinosa/intl-party.git",
|
|
41
46
|
"directory": "packages/eslint-plugin"
|
|
42
47
|
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18.0.0"
|
|
50
|
+
},
|
|
43
51
|
"scripts": {
|
|
44
52
|
"build": "tsup src/index.ts --format cjs --dts",
|
|
45
53
|
"dev": "tsup src/index.ts --format cjs --dts --watch",
|
|
46
|
-
"test": "vitest",
|
|
47
|
-
"test:watch": "vitest
|
|
54
|
+
"test": "vitest --run",
|
|
55
|
+
"test:watch": "vitest",
|
|
48
56
|
"lint": "eslint src --ext .ts",
|
|
49
57
|
"typecheck": "tsc --noEmit",
|
|
50
58
|
"clean": "rm -rf dist"
|