@intl-party/eslint-plugin 1.0.0 → 1.0.2

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 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,30 @@
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
+ }
11
+
12
+ interface NoHardcodedStringsOptions {
13
+ attributes?: string[];
14
+ ignorePattern?: string;
15
+ minLength?: number;
16
+ allowedStrings?: string[];
17
+ }
18
+
3
19
  declare const plugin: {
4
20
  meta: {
5
21
  name: string;
6
22
  version: string;
7
23
  };
8
24
  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>;
25
+ "no-hardcoded-strings": _typescript_eslint_utils_ts_eslint.RuleModule<"hardcodedString" | "hardcodedStringInAttribute", [NoHardcodedStringsOptions], _typescript_eslint_utils_ts_eslint.RuleListener>;
26
+ "no-missing-keys": _typescript_eslint_utils_ts_eslint.RuleModule<"missingTranslationKey" | "invalidTranslationKey", [NoMissingKeysOptions], _typescript_eslint_utils_ts_eslint.RuleListener>;
27
+ "prefer-translation-hooks": _typescript_eslint_utils_ts_eslint.RuleModule<"preferUseTranslations" | "preferScopedTranslations", [PreferTranslationHooksOptions], _typescript_eslint_utils_ts_eslint.RuleListener>;
12
28
  };
13
29
  configs: {
14
30
  recommended: {
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ var noHardcodedStrings = import_utils.ESLintUtils.RuleCreator(
34
34
  type: "problem",
35
35
  docs: {
36
36
  description: "Disallow hardcoded strings in JSX elements and specific attributes",
37
- recommended: "warn"
37
+ recommended: "recommended"
38
38
  },
39
39
  fixable: "code",
40
40
  schema: [
@@ -77,7 +77,7 @@ var noHardcodedStrings = import_utils.ESLintUtils.RuleCreator(
77
77
  }
78
78
  },
79
79
  defaultOptions: [{}],
80
- create(context, [options = {}]) {
80
+ create(context, [options]) {
81
81
  const {
82
82
  attributes = [
83
83
  "placeholder",
@@ -89,7 +89,7 @@ var noHardcodedStrings = import_utils.ESLintUtils.RuleCreator(
89
89
  ignorePattern,
90
90
  minLength = 3,
91
91
  allowedStrings = []
92
- } = options;
92
+ } = options || {};
93
93
  const ignoreRegex = ignorePattern ? new RegExp(ignorePattern) : null;
94
94
  function isHardcodedString(value) {
95
95
  if (value.length < minLength) return false;
@@ -117,17 +117,18 @@ var noHardcodedStrings = import_utils.ESLintUtils.RuleCreator(
117
117
  }
118
118
  function checkJSXAttribute(node) {
119
119
  if (node.name.type === "JSXIdentifier" && attributes.includes(node.name.name) && node.value?.type === "Literal" && typeof node.value.value === "string" && isHardcodedString(node.value.value)) {
120
+ const literalValue = node.value.value;
120
121
  context.report({
121
122
  node: node.value,
122
123
  messageId: "hardcodedStringInAttribute",
123
124
  data: {
124
- text: node.value.value,
125
+ text: literalValue,
125
126
  attribute: node.name.name
126
127
  },
127
128
  fix(fixer) {
128
129
  return fixer.replaceText(
129
130
  node.value,
130
- `{t('${generateTranslationKey(node.value.value)}')}`
131
+ `{t('${generateTranslationKey(literalValue)}')}`
131
132
  );
132
133
  }
133
134
  });
@@ -169,7 +170,7 @@ var noMissingKeys = import_utils2.ESLintUtils.RuleCreator(
169
170
  type: "problem",
170
171
  docs: {
171
172
  description: "Ensure all translation keys exist in translation files",
172
- recommended: "error"
173
+ recommended: "recommended"
173
174
  },
174
175
  schema: [
175
176
  {
@@ -195,8 +196,8 @@ var noMissingKeys = import_utils2.ESLintUtils.RuleCreator(
195
196
  }
196
197
  },
197
198
  defaultOptions: [{}],
198
- create(context, [options = {}]) {
199
- const { translationFiles = [], defaultLocale = "en" } = options;
199
+ create(context, [options]) {
200
+ const { translationFiles = [], defaultLocale = "en" } = options || {};
200
201
  const translationKeys = /* @__PURE__ */ new Set();
201
202
  function isValidTranslationKey(key) {
202
203
  return /^[a-zA-Z][a-zA-Z0-9._-]*$/.test(key);
@@ -237,7 +238,7 @@ var preferTranslationHooks = import_utils3.ESLintUtils.RuleCreator(
237
238
  type: "suggestion",
238
239
  docs: {
239
240
  description: "Prefer using translation hooks over direct i18n instance usage in React components",
240
- recommended: "warn"
241
+ recommended: "recommended"
241
242
  },
242
243
  fixable: "code",
243
244
  schema: [
@@ -259,8 +260,8 @@ var preferTranslationHooks = import_utils3.ESLintUtils.RuleCreator(
259
260
  }
260
261
  },
261
262
  defaultOptions: [{}],
262
- create(context, [options = {}]) {
263
- const { allowDirectUsage = false } = options;
263
+ create(context, [options]) {
264
+ const { allowDirectUsage = false } = options || {};
264
265
  function checkMemberExpression(node) {
265
266
  if (node.object.type === "Identifier" && node.object.name === "i18n" && node.property.type === "Identifier" && node.property.name === "t" && !allowDirectUsage) {
266
267
  context.report({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intl-party/eslint-plugin",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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",
@@ -43,8 +43,8 @@
43
43
  "scripts": {
44
44
  "build": "tsup src/index.ts --format cjs --dts",
45
45
  "dev": "tsup src/index.ts --format cjs --dts --watch",
46
- "test": "vitest",
47
- "test:watch": "vitest --watch",
46
+ "test": "vitest --run",
47
+ "test:watch": "vitest",
48
48
  "lint": "eslint src --ext .ts",
49
49
  "typecheck": "tsc --noEmit",
50
50
  "clean": "rm -rf dist"