From 28494b712bc9307ee8b1efa9751a59fd6d97ac20 Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot Date: Thu, 8 Jan 2026 10:02:02 +0100 Subject: [PATCH 1/6] Add ESLint rules for TypeScript strict typing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add @typescript-eslint/no-explicit-any (warn) to discourage 'any' type - Add @typescript-eslint/consistent-type-assertions (warn) to discourage type assertions while allowing 'as const' and object literals as parameters - Rules are scoped to .ts and .tsx files via overrides šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/eslint-config-custom/index.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/eslint-config-custom/index.js b/packages/eslint-config-custom/index.js index 1cd25321e..4d72e79b2 100644 --- a/packages/eslint-config-custom/index.js +++ b/packages/eslint-config-custom/index.js @@ -29,6 +29,24 @@ module.exports = { } ] }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + rules: { + // TypeScript strict typing rules - require proper types instead of "any" + '@typescript-eslint/no-explicit-any': 'warn', + // Discourage type assertions (as Type) - prefer proper typing + // Allows 'as const' and assertions in object literals passed as parameters + '@typescript-eslint/consistent-type-assertions': [ + 'warn', + { + assertionStyle: 'as', + objectLiteralTypeAssertions: 'allow-as-parameter' + } + ] + } + } + ], env: { browser: true, es2021: true, -- GitLab From db1a858e46167fe27d558878e9b903be6d3472cd Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot Date: Thu, 8 Jan 2026 10:02:11 +0100 Subject: [PATCH 2/6] Add translation validation scripts for blog app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two validation scripts to ensure translation consistency: 1. check-blog-translations.js - validates cross-locale key sync - Compares all locales against English reference - Reports missing and extra keys per locale - Checks for missing translation files 2. check-blog-translation-usage.js - validates translation key usage - Scans source files for t('key') and - Reports keys used in code that don't exist in translations - Optional --unused flag to show potentially unused keys Scripts are in global scripts/ folder and called via: - pnpm --filter @hive/blog lint:translations - pnpm --filter @hive/blog lint:translations:usage šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/blog/package.json | 2 + scripts/check-blog-translation-usage.js | 257 ++++++++++++++++++++++++ scripts/check-blog-translations.js | 214 ++++++++++++++++++++ 3 files changed, 473 insertions(+) create mode 100755 scripts/check-blog-translation-usage.js create mode 100755 scripts/check-blog-translations.js diff --git a/apps/blog/package.json b/apps/blog/package.json index 5019b5c17..b1a4cbd25 100644 --- a/apps/blog/package.json +++ b/apps/blog/package.json @@ -15,6 +15,8 @@ "postbuild": "pnpm run copy:worker && pnpm run copy:assets", "start": "react-env -- next start -p 3000", "lint": "eslint .", + "lint:translations": "node ../../scripts/check-blog-translations.js", + "lint:translations:usage": "node ../../scripts/check-blog-translation-usage.js", "pw:test:local": "playwright test --config=playwright.local3000.config.ts --update-snapshots", "pw:test:local:chromium": "playwright test --project=chromium --config=playwright.local3000.config.ts --headed --update-snapshots", "pw:test:local:firefox": "playwright test --project=firefox --config=playwright.local3000.config.ts --headed --update-snapshots", diff --git a/scripts/check-blog-translation-usage.js b/scripts/check-blog-translation-usage.js new file mode 100755 index 000000000..36332337a --- /dev/null +++ b/scripts/check-blog-translation-usage.js @@ -0,0 +1,257 @@ +#!/usr/bin/env node + +/** + * Translation Usage Validator + * + * Scans source files for translation key usage and validates: + * - All used translation keys exist in the reference locale (English) + * - Reports unused translation keys (optional, with --unused flag) + * + * Usage: + * node scripts/check-blog-translation-usage.js # Check for missing keys + * node scripts/check-blog-translation-usage.js --unused # Also report unused keys + */ + +const fs = require('fs'); +const path = require('path'); + +const BLOG_DIR = path.join(__dirname, '../apps/blog'); +const LOCALES_DIR = path.join(BLOG_DIR, 'locales'); +const REFERENCE_LOCALE = 'en'; +const SOURCE_DIRS = ['app', 'components', 'features', 'lib', 'pages']; +const SOURCE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js']; + +// Patterns to match translation function calls +// Matches: t('key'), t("key"), t(`key`), +const TRANSLATION_PATTERNS = [ + /\bt\(\s*['"`]([^'"`\n]+?)['"`]\s*(?:,|\))/g, + /i18nKey\s*=\s*['"`]([^'"`\n]+?)['"`]/g +]; + +/** + * Recursively get all keys from a nested object + * @param {object} obj + * @param {string} prefix + * @returns {Set} + */ +function getAllKeys(obj, prefix = '') { + const keys = new Set(); + + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key; + + if (value && typeof value === 'object' && !Array.isArray(value)) { + const nestedKeys = getAllKeys(value, fullKey); + nestedKeys.forEach((k) => keys.add(k)); + } else { + keys.add(fullKey); + } + } + + return keys; +} + +/** + * Load translation keys from reference locale + * @returns {Set} + */ +function loadTranslationKeys() { + const keys = new Set(); + const localeDir = path.join(LOCALES_DIR, REFERENCE_LOCALE); + + if (!fs.existsSync(localeDir)) { + console.error(`Reference locale directory not found: ${localeDir}`); + process.exit(1); + } + + const files = fs.readdirSync(localeDir).filter((f) => f.endsWith('.json')); + + for (const file of files) { + const filePath = path.join(localeDir, file); + try { + const content = JSON.parse(fs.readFileSync(filePath, 'utf8')); + const fileKeys = getAllKeys(content); + fileKeys.forEach((k) => keys.add(k)); + } catch (error) { + console.error(`Error loading ${filePath}: ${error.message}`); + } + } + + return keys; +} + +/** + * Recursively find all source files + * @param {string} dir + * @returns {string[]} + */ +function findSourceFiles(dir) { + const files = []; + + if (!fs.existsSync(dir)) return files; + + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + // Skip node_modules, .next, etc. + if (!entry.name.startsWith('.') && entry.name !== 'node_modules') { + files.push(...findSourceFiles(fullPath)); + } + } else if (SOURCE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) { + files.push(fullPath); + } + } + + return files; +} + +/** + * Extract translation keys from a source file + * @param {string} filePath + * @returns {{ keys: Map, file: string }} + */ +function extractKeysFromFile(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n'); + const keys = new Map(); // key -> [line numbers] + + for (let lineNum = 0; lineNum < lines.length; lineNum++) { + const line = lines[lineNum]; + + for (const pattern of TRANSLATION_PATTERNS) { + // Reset regex state + pattern.lastIndex = 0; + let match; + + while ((match = pattern.exec(line)) !== null) { + const key = match[1]; + + // Skip dynamic keys (containing variables) + if (key.includes('${') || key.includes('{') || key.includes('+')) { + continue; + } + + if (!keys.has(key)) { + keys.set(key, []); + } + keys.get(key).push(lineNum + 1); + } + } + } + + return { keys, file: filePath }; +} + +/** + * Main validation function + */ +function validateTranslationUsage() { + const showUnused = process.argv.includes('--unused'); + + console.log('\nšŸ“‹ Translation Usage Validator (Blog)'); + console.log(` Reference locale: ${REFERENCE_LOCALE}`); + console.log(` Scanning: ${SOURCE_DIRS.join(', ')}\n`); + + // Load all valid translation keys + const validKeys = loadTranslationKeys(); + console.log(` Found ${validKeys.size} translation keys in ${REFERENCE_LOCALE}\n`); + + // Find all source files + const sourceFiles = []; + for (const dir of SOURCE_DIRS) { + sourceFiles.push(...findSourceFiles(path.join(BLOG_DIR, dir))); + } + console.log(` Scanning ${sourceFiles.length} source files...\n`); + + // Extract and validate keys + const usedKeys = new Set(); + const missingKeys = new Map(); // key -> [{file, lines}] + let totalUsages = 0; + + for (const filePath of sourceFiles) { + const { keys } = extractKeysFromFile(filePath); + const relativePath = path.relative(BLOG_DIR, filePath); + + for (const [key, lineNumbers] of keys) { + totalUsages += lineNumbers.length; + usedKeys.add(key); + + if (!validKeys.has(key)) { + if (!missingKeys.has(key)) { + missingKeys.set(key, []); + } + missingKeys.get(key).push({ file: relativePath, lines: lineNumbers }); + } + } + } + + // Report missing keys + let hasErrors = false; + + if (missingKeys.size > 0) { + hasErrors = true; + console.log(`āŒ Missing translation keys (${missingKeys.size} keys not found in ${REFERENCE_LOCALE}):\n`); + + const sortedKeys = [...missingKeys.entries()].sort((a, b) => a[0].localeCompare(b[0])); + + for (const [key, locations] of sortedKeys) { + console.log(` "${key}"`); + for (const { file, lines } of locations) { + console.log(` └─ ${file}:${lines.join(', ')}`); + } + } + console.log(''); + } else { + console.log(`āœ… All ${totalUsages} translation usages reference valid keys\n`); + } + + // Report unused keys (optional) + if (showUnused) { + const unusedKeys = [...validKeys].filter((k) => !usedKeys.has(k)); + + if (unusedKeys.length > 0) { + console.log(`āš ļø Potentially unused translation keys (${unusedKeys.length} keys):\n`); + console.log(' Note: Some keys may be used dynamically and not detected.\n'); + + // Group by top-level namespace + const grouped = {}; + for (const key of unusedKeys) { + const namespace = key.split('.')[0]; + if (!grouped[namespace]) { + grouped[namespace] = []; + } + grouped[namespace].push(key); + } + + for (const [namespace, keys] of Object.entries(grouped)) { + console.log(` ${namespace}/ (${keys.length} keys)`); + keys.slice(0, 5).forEach((k) => console.log(` - ${k}`)); + if (keys.length > 5) { + console.log(` ... and ${keys.length - 5} more`); + } + } + console.log(''); + } else { + console.log(`āœ… All translation keys appear to be used\n`); + } + } + + // Summary + console.log('─'.repeat(50)); + console.log(`\n Total translation keys: ${validKeys.size}`); + console.log(` Keys used in code: ${usedKeys.size}`); + console.log(` Total usages found: ${totalUsages}`); + + if (hasErrors) { + console.log(`\nāŒ Validation failed! Add missing keys to apps/blog/locales/${REFERENCE_LOCALE}/common_blog.json\n`); + process.exit(1); + } else { + console.log(`\nāœ… Translation usage validation passed!\n`); + process.exit(0); + } +} + +validateTranslationUsage(); diff --git a/scripts/check-blog-translations.js b/scripts/check-blog-translations.js new file mode 100755 index 000000000..55a0857f3 --- /dev/null +++ b/scripts/check-blog-translations.js @@ -0,0 +1,214 @@ +#!/usr/bin/env node + +/** + * Translation Keys Validator + * + * Compares translation JSON files across all locales to find: + * - Missing keys (present in reference locale but missing in others) + * - Extra keys (present in other locales but not in reference) + * + * Usage: node scripts/check-blog-translations.js + * + * Reference locale: English (en) + */ + +const fs = require('fs'); +const path = require('path'); + +const LOCALES_DIR = path.join(__dirname, '../apps/blog/locales'); +const REFERENCE_LOCALE = 'en'; + +/** + * Recursively extract all keys from a nested object + * @param {object} obj - The object to extract keys from + * @param {string} prefix - Current key path prefix + * @returns {Set} Set of dot-notation key paths + */ +function getAllKeys(obj, prefix = '') { + const keys = new Set(); + + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key; + + if (value && typeof value === 'object' && !Array.isArray(value)) { + const nestedKeys = getAllKeys(value, fullKey); + nestedKeys.forEach((k) => keys.add(k)); + } else { + keys.add(fullKey); + } + } + + return keys; +} + +/** + * Load and parse a JSON file + * @param {string} filePath - Path to the JSON file + * @returns {object|null} Parsed JSON or null if error + */ +function loadJson(filePath) { + try { + const content = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(content); + } catch (error) { + console.error(`Error loading ${filePath}: ${error.message}`); + return null; + } +} + +/** + * Get all locale directories + * @returns {string[]} Array of locale codes + */ +function getLocales() { + return fs + .readdirSync(LOCALES_DIR, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); +} + +/** + * Get all JSON files in a locale directory + * @param {string} locale - Locale code + * @returns {string[]} Array of JSON filenames + */ +function getJsonFiles(locale) { + const localeDir = path.join(LOCALES_DIR, locale); + return fs.readdirSync(localeDir).filter((file) => file.endsWith('.json')); +} + +/** + * Compare keys between reference and target locale + * @param {Set} referenceKeys - Keys from reference locale + * @param {Set} targetKeys - Keys from target locale + * @returns {{ missing: string[], extra: string[] }} + */ +function compareKeys(referenceKeys, targetKeys) { + const missing = []; + const extra = []; + + referenceKeys.forEach((key) => { + if (!targetKeys.has(key)) { + missing.push(key); + } + }); + + targetKeys.forEach((key) => { + if (!referenceKeys.has(key)) { + extra.push(key); + } + }); + + return { missing, extra }; +} + +/** + * Main validation function + */ +function validateTranslations() { + const locales = getLocales(); + + if (!locales.includes(REFERENCE_LOCALE)) { + console.error(`Reference locale '${REFERENCE_LOCALE}' not found!`); + process.exit(1); + } + + const referenceFiles = getJsonFiles(REFERENCE_LOCALE); + let hasErrors = false; + let totalMissing = 0; + let totalExtra = 0; + + console.log(`\nšŸ“‹ Translation Keys Validator (Blog)`); + console.log(` Reference locale: ${REFERENCE_LOCALE}`); + console.log(` Checking locales: ${locales.filter((l) => l !== REFERENCE_LOCALE).join(', ')}\n`); + + for (const jsonFile of referenceFiles) { + const referenceFilePath = path.join(LOCALES_DIR, REFERENCE_LOCALE, jsonFile); + const referenceData = loadJson(referenceFilePath); + + if (!referenceData) { + hasErrors = true; + continue; + } + + const referenceKeys = getAllKeys(referenceData); + console.log(`šŸ“„ ${jsonFile} (${referenceKeys.size} keys in ${REFERENCE_LOCALE})`); + + for (const locale of locales) { + if (locale === REFERENCE_LOCALE) continue; + + const targetFilePath = path.join(LOCALES_DIR, locale, jsonFile); + + if (!fs.existsSync(targetFilePath)) { + console.log(` āŒ ${locale}: File missing!`); + hasErrors = true; + continue; + } + + const targetData = loadJson(targetFilePath); + if (!targetData) { + hasErrors = true; + continue; + } + + const targetKeys = getAllKeys(targetData); + const { missing, extra } = compareKeys(referenceKeys, targetKeys); + + if (missing.length === 0 && extra.length === 0) { + console.log(` āœ… ${locale}: OK (${targetKeys.size} keys)`); + } else { + hasErrors = true; + totalMissing += missing.length; + totalExtra += extra.length; + + console.log(` āš ļø ${locale}: ${missing.length} missing, ${extra.length} extra`); + + if (missing.length > 0) { + console.log(` Missing keys:`); + missing.slice(0, 10).forEach((key) => console.log(` - ${key}`)); + if (missing.length > 10) { + console.log(` ... and ${missing.length - 10} more`); + } + } + + if (extra.length > 0) { + console.log(` Extra keys (not in ${REFERENCE_LOCALE}):`); + extra.slice(0, 5).forEach((key) => console.log(` + ${key}`)); + if (extra.length > 5) { + console.log(` ... and ${extra.length - 5} more`); + } + } + } + } + console.log(''); + } + + // Check for files that exist in other locales but not in reference + for (const locale of locales) { + if (locale === REFERENCE_LOCALE) continue; + + const localeFiles = getJsonFiles(locale); + const extraFiles = localeFiles.filter((f) => !referenceFiles.includes(f)); + + if (extraFiles.length > 0) { + console.log(`āš ļø ${locale} has extra files not in ${REFERENCE_LOCALE}: ${extraFiles.join(', ')}`); + hasErrors = true; + } + } + + // Summary + console.log('\n' + '─'.repeat(50)); + if (hasErrors) { + console.log(`\nāŒ Validation failed!`); + console.log(` Total missing keys: ${totalMissing}`); + console.log(` Total extra keys: ${totalExtra}`); + console.log(`\n Fix missing keys by adding them to the respective locale files.`); + console.log(` Use English (${REFERENCE_LOCALE}) as the reference.\n`); + process.exit(1); + } else { + console.log(`\nāœ… All translations are in sync!\n`); + process.exit(0); + } +} + +validateTranslations(); -- GitLab From 8fdbae4d13eca8f0107283c4d6b6216b0c8f7f88 Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot Date: Thu, 8 Jan 2026 10:02:17 +0100 Subject: [PATCH 3/6] Fix invalid translation key in AI search results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change t('search.no_results') to t('search_page.no_results') The key 'search.no_results' doesn't exist in translations. The correct key is 'search_page.no_results' which contains "Nothing was found." message. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/blog/features/search/ai-result.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/blog/features/search/ai-result.tsx b/apps/blog/features/search/ai-result.tsx index 73b63d612..71e08589e 100644 --- a/apps/blog/features/search/ai-result.tsx +++ b/apps/blog/features/search/ai-result.tsx @@ -146,7 +146,7 @@ const AIResult = ({ query, nsfwPreferences }: { query: string; nsfwPreferences: } if (!searchResults || searchResults.length === 0) { - return
{t('search.no_results')}
; + return
{t('search_page.no_results')}
; } return ( -- GitLab From 503d3c6489f6adb97254b13a36528d7c76882d06 Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot Date: Thu, 8 Jan 2026 10:02:23 +0100 Subject: [PATCH 4/6] Document ESLint rules and translation validation in CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add documentation for: - TypeScript strict typing rules (no-explicit-any, consistent-type-assertions) - Examples of good/bad code patterns for any and type assertions - Translation rules - always use t() instead of inline strings - Translation validation commands and how to fix issues - Scripts location and usage instructions šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index c7e341ce6..f3c858df1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -220,3 +220,145 @@ apps/blog/ - Commented-out code (delete it, git has history) - Console.logs in production code (use logger) - Ignoring TypeScript errors with `@ts-ignore` + +--- + +## ESLint Rules for Code Quality + +### TypeScript Strict Typing (warnings) +The project enforces strict typing via ESLint. Run `pnpm lint` after changes to check. + +**Enabled rules:** +1. **`@typescript-eslint/no-explicit-any`** - Disallows `any` type + - Bad: `const data: any = ...` + - Good: `const data: UserProfile = ...` or use `unknown` with type guards + +2. **`@typescript-eslint/consistent-type-assertions`** - Disallows type assertions (`as Type`) + - Bad: `const user = data as User` + - Good: Define proper types upfront, use type guards, or generics + +### How to Fix Common Issues + +**Instead of `any`:** +```typescript +// Bad +function handleError(error: any) { ... } + +// Good - use unknown with type guard +function handleError(error: unknown) { + if (error instanceof Error) { + logger.error(error, 'Operation failed'); + } +} + +// Good - use specific type +function handleError(error: Error) { ... } +``` + +**Instead of `as Type`:** +```typescript +// Bad +const config = JSON.parse(data) as AppConfig; + +// Good - use type guard +function isAppConfig(obj: unknown): obj is AppConfig { + return typeof obj === 'object' && obj !== null && 'apiUrl' in obj; +} +const parsed = JSON.parse(data); +if (isAppConfig(parsed)) { + const config = parsed; // typed as AppConfig +} + +// Good - use zod schema validation +const configSchema = z.object({ apiUrl: z.string() }); +const config = configSchema.parse(JSON.parse(data)); +``` + +### Running Lint +```bash +# Lint specific package +pnpm --filter @hive/blog lint + +# Lint all packages +pnpm lint +``` + +### Translation Rules + +**IMPORTANT: Never use inline strings for user-facing text!** + +All user-visible text must use translation keys via the `t()` function. This ensures: +- Proper internationalization (i18n) support +- Consistent text management +- Easy translation to other languages + +**Bad - inline strings:** +```tsx +// DON'T do this + +

Loading...

+No results found +{error &&
Something went wrong
} +``` + +**Good - translation keys:** +```tsx +// DO this + +

{t('global.loading')}

+{t('search_page.no_results')} +{error &&
{t('global.something_went_wrong')}
} +``` + +**Usage pattern:** +```tsx +import { useTranslation } from '@/blog/i18n/client'; + +function MyComponent() { + const { t } = useTranslation('common_blog'); + + return
{t('namespace.key_name')}
; +} +``` + +### Translation Keys Validation + +#### 1. Cross-locale key sync +Validates that all locales have the same keys as English reference. + +```bash +pnpm --filter @hive/blog lint:translations +``` + +**What it checks:** +- Missing keys (present in English but missing in other locales) +- Extra keys (present in other locales but not in English reference) +- Missing translation files + +#### 2. Translation usage validation +Validates that all `t('key')` and `` calls in code reference existing translation keys. + +```bash +# Check for missing keys +pnpm --filter @hive/blog lint:translations:usage + +# Also show potentially unused keys (-- passes flag to the script) +pnpm --filter @hive/blog lint:translations:usage -- --unused +``` + +**What it checks:** +- All `t('key')` function calls reference existing keys +- All `` components reference existing keys +- Reports file and line number for invalid keys +- Optionally reports unused translation keys + +**Reference locale:** English (`en`) is the source of truth. + +**Fixing issues:** +1. Missing keys in code: Add the key to `apps/blog/locales/en/common_blog.json` +2. Missing keys in locales: Copy from English and translate +3. Extra keys: Remove outdated keys or add to English if valid + +**Scripts location:** `scripts/check-blog-translations.js`, `scripts/check-blog-translation-usage.js` + +**Translations location:** `apps/blog/locales/[lang]/common_blog.json` -- GitLab From e7583989bd94f416aad90be536eaa8d3f550da21 Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot Date: Thu, 8 Jan 2026 11:59:33 +0100 Subject: [PATCH 5/6] Add additional ESLint rules for code quality and consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit General rules (all files): - eqeqeq: enforce === instead of == - prefer-const: prefer const over let - no-console: discourage console.log, use logger TypeScript rules (.ts, .tsx): - ban-ts-comment: require description for @ts-ignore (min 10 chars) - no-non-null-assertion: discourage ! operator - no-unused-vars: detect unused variables (ignore _prefixed) - naming-convention: enforce camelCase for variables/functions, PascalCase for types, with flexibility for API responses All rules set to 'warn' for gradual adoption. Refs #72 šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/eslint-config-custom/index.js | 93 +++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/packages/eslint-config-custom/index.js b/packages/eslint-config-custom/index.js index 4d72e79b2..3dbe84781 100644 --- a/packages/eslint-config-custom/index.js +++ b/packages/eslint-config-custom/index.js @@ -27,7 +27,13 @@ module.exports = { } ] } - ] + ], + // Enforce === instead of == for type-safe comparisons + eqeqeq: ['warn', 'always', { null: 'ignore' }], + // Prefer const over let when variable is never reassigned + 'prefer-const': 'warn', + // Discourage console.log in production code - use logger instead + 'no-console': ['warn', { allow: ['warn', 'error'] }] }, overrides: [ { @@ -43,6 +49,91 @@ module.exports = { assertionStyle: 'as', objectLiteralTypeAssertions: 'allow-as-parameter' } + ], + // Discourage @ts-ignore - prefer proper typing or @ts-expect-error with explanation + '@typescript-eslint/ban-ts-comment': [ + 'warn', + { + 'ts-ignore': 'allow-with-description', + 'ts-nocheck': 'allow-with-description', + 'ts-expect-error': 'allow-with-description', + minimumDescriptionLength: 10 + } + ], + // Discourage non-null assertion operator (!) - prefer proper null checks + '@typescript-eslint/no-non-null-assertion': 'warn', + // Detect unused variables (TypeScript version) + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + } + ], + // Enforce consistent naming conventions (camelCase for variables/functions) + '@typescript-eslint/naming-convention': [ + 'warn', + // Default: camelCase for everything + { + selector: 'default', + format: ['camelCase'], + leadingUnderscore: 'allow' + }, + // Imports: allow PascalCase (React components) and camelCase + { + selector: 'import', + format: ['camelCase', 'PascalCase'] + }, + // Variables: camelCase or UPPER_CASE (for constants) or PascalCase (React components) + { + selector: 'variable', + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + leadingUnderscore: 'allow' + }, + // Functions: camelCase or PascalCase (React components) + { + selector: 'function', + format: ['camelCase', 'PascalCase'] + }, + // Parameters: camelCase + { + selector: 'parameter', + format: ['camelCase'], + leadingUnderscore: 'allow' + }, + // Properties: camelCase or snake_case (for API responses) or UPPER_CASE + { + selector: 'property', + format: ['camelCase', 'snake_case', 'UPPER_CASE', 'PascalCase'], + leadingUnderscore: 'allow' + }, + // Allow any format for properties that require quotes (HTTP headers, data-*, CSS classes with dashes) + { + selector: 'property', + modifiers: ['requiresQuotes'], + format: null + }, + // Object literal properties: allow more formats for flexibility with external APIs + { + selector: 'objectLiteralProperty', + format: null + }, + // Type properties: allow more formats for external API types (CSP reports, etc.) + { + selector: 'typeProperty', + format: null + }, + // Types/Interfaces: PascalCase + { + selector: 'typeLike', + format: ['PascalCase'] + }, + // Enum members: PascalCase or UPPER_CASE + { + selector: 'enumMember', + format: ['PascalCase', 'UPPER_CASE'] + } ] } } -- GitLab From f8ac6c34923d092813837c36cdc15b5bf403fe53 Mon Sep 17 00:00:00 2001 From: Krzysztof Kocot Date: Thu, 8 Jan 2026 11:59:39 +0100 Subject: [PATCH 6/6] Update CLAUDE.md with comprehensive ESLint rules documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reorganize ESLint section with tables for better readability - Document all TypeScript strict typing rules - Add naming conventions section with examples - Add code quality rules (eqeqeq, prefer-const, no-console) - Include fix examples for @ts-ignore and non-null assertions - Add --fix command to lint instructions Refs #72 šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f3c858df1..da1e1239a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -225,17 +225,50 @@ apps/blog/ ## ESLint Rules for Code Quality -### TypeScript Strict Typing (warnings) -The project enforces strict typing via ESLint. Run `pnpm lint` after changes to check. +The project enforces code quality and consistency via ESLint. Run `pnpm lint` after changes to check. All rules are set to **warn** to allow gradual adoption. -**Enabled rules:** -1. **`@typescript-eslint/no-explicit-any`** - Disallows `any` type - - Bad: `const data: any = ...` - - Good: `const data: UserProfile = ...` or use `unknown` with type guards +### TypeScript Strict Typing -2. **`@typescript-eslint/consistent-type-assertions`** - Disallows type assertions (`as Type`) - - Bad: `const user = data as User` - - Good: Define proper types upfront, use type guards, or generics +| Rule | Purpose | +|------|---------| +| `@typescript-eslint/no-explicit-any` | Disallows `any` type - use `unknown` with type guards | +| `@typescript-eslint/consistent-type-assertions` | Discourages `as Type` - prefer proper typing | +| `@typescript-eslint/no-non-null-assertion` | Discourages `!` operator - prefer proper null checks | +| `@typescript-eslint/no-unused-vars` | Detects unused variables (prefix with `_` to ignore) | +| `@typescript-eslint/ban-ts-comment` | Requires description for `@ts-ignore` (min 10 chars) | + +### Naming Conventions + +| Rule | Convention | +|------|------------| +| Variables | `camelCase`, `UPPER_CASE` (constants), `PascalCase` (React components) | +| Functions | `camelCase`, `PascalCase` (React components) | +| Parameters | `camelCase` (prefix `_` for intentionally unused) | +| Types/Interfaces | `PascalCase` | +| Enum members | `PascalCase` or `UPPER_CASE` | + +**Example violations:** +```typescript +// Bad - snake_case variable +const user_profile = fetchProfile(); + +// Good - camelCase +const userProfile = fetchProfile(); + +// Bad - unused parameter +function handleClick(event) { doSomething(); } + +// Good - prefix with underscore +function handleClick(_event) { doSomething(); } +``` + +### Code Quality + +| Rule | Purpose | +|------|---------| +| `eqeqeq` | Enforce `===` instead of `==` (type-safe comparisons) | +| `prefer-const` | Prefer `const` over `let` when never reassigned | +| `no-console` | Discourage `console.log` - use logger instead | ### How to Fix Common Issues @@ -274,6 +307,35 @@ const configSchema = z.object({ apiUrl: z.string() }); const config = configSchema.parse(JSON.parse(data)); ``` +**Instead of `@ts-ignore`:** +```typescript +// Bad - no explanation +// @ts-ignore +someCode(); + +// Good - with explanation +// @ts-ignore - Third-party library types are incorrect for this overload +someCode(); + +// Better - use @ts-expect-error (fails if error disappears) +// @ts-expect-error - Third-party library types are incorrect +someCode(); +``` + +**Instead of non-null assertion:** +```typescript +// Bad +const name = user!.name; + +// Good - explicit check +if (user) { + const name = user.name; +} + +// Good - optional chaining with fallback +const name = user?.name ?? 'Anonymous'; +``` + ### Running Lint ```bash # Lint specific package @@ -281,6 +343,9 @@ pnpm --filter @hive/blog lint # Lint all packages pnpm lint + +# Auto-fix fixable issues +pnpm --filter @hive/blog lint -- --fix ``` ### Translation Rules -- GitLab