diff --git a/electron-builder.json b/electron-builder.json index ddf82a79..71c110d9 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -1,7 +1,7 @@ { "productName": "PicList", "appId": "com.kuingsmile.piclist", - "afterSign": "scripts/notarize.js", + "afterSign": "scripts/notarize.cjs", "directories": { "output": "release/${version}", "buildResources": "build" diff --git a/eslint.config.js b/eslint.config.js index c4a8093e..b81d7fc4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -11,7 +11,7 @@ export default tseslint.config( files: ['./src/*.{ts,tsx,cts,mts,js,cjs,mjs}', './scripts/*.{ts,js,mjs}', './test/*.{ts,js,mjs}'] }, { - ignores: ['**/node_modules/**', '**/out/**', '**/webpack.config.js', 'vitest.workspace.mjs'] + ignores: ['**/node_modules/**', '**/out/**', '**/webpack.config.js', 'vitest.workspace.mjs', '**/dist/**'] }, eslint.configs.recommended, ...tseslint.configs.recommended, diff --git a/package.json b/package.json index 3e626795..849318bd 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,13 @@ "build:linux": "electron-vite build && electron-builder --linux", "bump": "bump-version", "cz": "git-cz", - "dev": "electron-vite dev -- --trace-deprecation", + "dev": "electron-vite dev", "i18n": "node ./scripts/gen-i18n-types.js", - "i18n:check": "node ./scripts/find-unused-i18n.mjs", - "i18n:check:verbose": "node ./scripts/find-unused-i18n.mjs --verbose", - "i18n:clean": "node ./scripts/find-unused-i18n.mjs --delete", - "i18n:clean:dry-run": "node ./scripts/find-unused-i18n.mjs --delete --dry-run", - "link": "node ./scripts/link.js", + "i18n:check": "node ./scripts/find-unused-i18n.js", + "i18n:check:verbose": "node ./scripts/find-unused-i18n.js --verbose", + "i18n:clean": "node ./scripts/find-unused-i18n.js --delete", + "i18n:clean:dry-run": "node ./scripts/find-unused-i18n.js --delete --dry-run", + "link": "node ./scripts/link.cjs", "lint": "eslint --ext .js,.jsx,.ts,.tsx,.vue src/", "lint:dpdm": "dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/main/index.ts", "lint:fix": "eslint --fix --ext .js,.jsx,.ts,.tsx,.vue src/", @@ -38,9 +38,9 @@ "prebuild": "electron-vite build", "preview": "electron-vite preview", "release": "electron-vite build && electron-builder --publish always", - "sha256": "node ./scripts/gen-sha256.js", - "upload-beta": "node ./scripts/upload-beta.js", - "upload-dist": "node ./scripts/upload-dist-to-r2.js" + "sha256": "node ./scripts/gen-sha256.cjs", + "upload-beta": "node ./scripts/upload-beta.cjs", + "upload-dist": "node ./scripts/upload-dist-to-r2.cjs" }, "dependencies": { "@aws-sdk/client-s3": "^3.856.0", diff --git a/scripts/check-dep.js b/scripts/check-dep.js index ba21d407..574d8bb4 100644 --- a/scripts/check-dep.js +++ b/scripts/check-dep.js @@ -1,7 +1,7 @@ -const ncu = require('npm-check-updates') -const axios = require('axios') +import axios from 'axios' +import { run } from 'npm-check-updates' -async function getRepositoryInfo(packageName) { +async function getRepositoryInfo (packageName) { try { const { data } = await axios.get(`https://registry.npmjs.org/${packageName}`) const repository = data.repository @@ -17,8 +17,8 @@ async function getRepositoryInfo(packageName) { return null } -async function checkUpdates() { - const updated = await ncu.run({ +async function checkUpdates () { + const updated = await run({ packageFile: './package.json', upgrade: false }) diff --git a/scripts/config.js b/scripts/config.js index 358adccb..b6e57a1a 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -59,7 +59,7 @@ const win32 = [ } ] -module.exports = { +export default { darwin, linux, win32 diff --git a/scripts/find-unused-i18n.js b/scripts/find-unused-i18n.js index e69de29b..9d6e8f52 100644 --- a/scripts/find-unused-i18n.js +++ b/scripts/find-unused-i18n.js @@ -0,0 +1,500 @@ +#!/usr/bin/env node + +import { readdirSync, readFileSync, writeFileSync } from 'node:fs' +import { basename, dirname, extname, join, relative } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { dump, load } from 'js-yaml' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const LOCALE_DIR = join(__dirname, '../public/i18n') +const SRC_DIR = join(__dirname, '../src') + +// Parse command line arguments +const args = process.argv.slice(2) +const isVerbose = args.includes('--verbose') || args.includes('-v') +const shouldDelete = args.includes('--delete') || args.includes('-d') +const isDryRun = args.includes('--dry-run') + +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m' +} + +function colorize (text, color) { + return `${colors[color]}${text}${colors.reset}` +} + +console.log(`\nšŸ” Analyzing i18n keys in ${LOCALE_DIR} and source files in ${SRC_DIR}`) +if (shouldDelete) { + console.log(colorize(`šŸ—‘ļø Delete mode enabled ${isDryRun ? '(DRY RUN)' : ''}`, 'yellow')) +} +console.log('') + +function flattenKeys (obj, prefix = '') { + const keys = [] + + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + keys.push(...flattenKeys(value, fullKey)) + } else { + keys.push(fullKey) + } + } + + return keys +} + +function readLocaleFile (filePath) { + try { + const content = readFileSync(filePath, 'utf8') + return load(content) || {} + } catch (error) { + console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red')) + return {} + } +} + +function getAllI18nKeys () { + const localeFiles = readdirSync(LOCALE_DIR).filter(file => file.endsWith('.yml')) + const allKeys = new Set() + const localeData = {} + + console.log(colorize('\nšŸ“ Found locale files:', 'blue')) + + for (const file of localeFiles) { + const filePath = join(LOCALE_DIR, file) + const locale = basename(file, '.yml') + const data = readLocaleFile(filePath) + const keys = flattenKeys(data) + + localeData[locale] = { + file: filePath, + keys, + data + } + + keys.forEach(key => allKeys.add(key)) + + console.log(` ${colorize('āœ“', 'green')} ${file} (${keys.length} keys)`) + } + + return { + allKeys: Array.from(allKeys).sort(), + localeData + } +} + +function findFiles (dir, extensions = ['.vue', '.ts', '.js']) { + const files = [] + + function walk (currentDir) { + try { + const entries = readdirSync(currentDir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = join(currentDir, entry.name) + + if (entry.isDirectory()) { + if (!['node_modules', '.git', 'dist', 'build', 'target', 'dist_electron'].includes(entry.name)) { + walk(fullPath) + } + } else if (entry.isFile()) { + const ext = extname(entry.name) + if (extensions.includes(ext)) { + files.push(fullPath) + } + } + } + } catch (error) { + console.warn(colorize(`Warning: Cannot read directory ${currentDir}: ${error.message}`, 'yellow')) + } + } + + walk(dir) + return files +} + +function findKeyUsage (keys) { + const usage = {} + const dynamicPatterns = [] + + keys.forEach(key => { + usage[key] = { + used: false, + files: [], + patterns: [], + dynamicMatch: false + } + }) + + console.log(colorize('\nšŸ” Searching for key usage in source files...', 'blue')) + + const sourceFiles = findFiles(SRC_DIR) + + console.log(` Found ${sourceFiles.length} source files to analyze`) + + const searchPatterns = [ + /\$T\s*\(\s*['"`]([^'"`]+)['"`]/g, + /(?:^|[^a-zA-Z$])T\s*\(\s*['"`]([^'"`]+)['"`]/g, + /\{\{\s*\$T\s*\(\s*['"`]([^'"`]+)['"`]/g, + /i18nManager\.T\s*\(\s*['"`]([^'"`]+)['"`]/g + ] + + const dynamicPattern = /\$T\s*\(\s*`([^`]*\$\{[^}]+\}[^`]*)`/g + + sourceFiles.forEach(filePath => { + try { + const content = readFileSync(filePath, 'utf8') + const relativePath = relative(join(__dirname, '..'), filePath) + + searchPatterns.forEach((pattern, patternIndex) => { + let match + while ((match = pattern.exec(content)) !== null) { + const key = match[1] + if (usage[key]) { + usage[key].used = true + if (!usage[key].files.includes(relativePath)) { + usage[key].files.push(relativePath) + } + if (!usage[key].patterns.includes(patternIndex)) { + usage[key].patterns.push(patternIndex) + } + } + } + }) + + let dynamicMatch + while ((dynamicMatch = dynamicPattern.exec(content)) !== null) { + const templateString = dynamicMatch[1] + + const staticParts = templateString.split(/\$\{[^}]+\}/) + + const patternInfo = { + template: templateString, + file: relativePath, + staticParts + } + + if (!dynamicPatterns.some(p => p.template === templateString && p.file === relativePath)) { + dynamicPatterns.push(patternInfo) + } + + keys.forEach(key => { + if (matchesDynamicPattern(key, staticParts)) { + if (usage[key]) { + usage[key].used = true + usage[key].dynamicMatch = true + if (!usage[key].files.includes(relativePath)) { + usage[key].files.push(relativePath) + } + if (!usage[key].patterns.includes('dynamic')) { + usage[key].patterns.push('dynamic') + } + } + } + }) + } + } catch (error) { + console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red')) + } + }) + + usage._dynamicPatterns = dynamicPatterns + + return usage +} + +function matchesDynamicPattern (key, staticParts) { + if (staticParts.length === 0) return false + + let keyIndex = 0 + + for (let i = 0; i < staticParts.length; i++) { + const part = staticParts[i] + + if (part === '') { + if (i < staticParts.length - 1) { + const nextPart = staticParts[i + 1] + if (nextPart) { + const nextIndex = key.indexOf(nextPart, keyIndex) + if (nextIndex === -1) return false + keyIndex = nextIndex + } + } + continue + } + + if (i === 0) { + if (!key.startsWith(part)) return false + keyIndex = part.length + } else if (i === staticParts.length - 1) { + if (part && !key.endsWith(part)) return false + } else { + const index = key.indexOf(part, keyIndex) + if (index === -1) return false + keyIndex = index + part.length + } + } + + return true +} + +function findLocaleInconsistencies (localeData) { + const locales = Object.keys(localeData) + const inconsistencies = {} + + if (locales.length < 2) { + return inconsistencies + } + + locales.forEach(locale => { + const currentKeys = new Set(localeData[locale].keys) + inconsistencies[locale] = { + missing: [], + extra: [] + } + + locales.forEach(otherLocale => { + if (locale !== otherLocale) { + localeData[otherLocale].keys.forEach(key => { + if (!currentKeys.has(key) && !inconsistencies[locale].missing.includes(key)) { + inconsistencies[locale].missing.push(key) + } + }) + } + }) + + localeData[locale].keys.forEach(key => { + const existsInOthers = locales.some( + otherLocale => locale !== otherLocale && localeData[otherLocale].keys.includes(key) + ) + if (!existsInOthers) { + inconsistencies[locale].extra.push(key) + } + }) + }) + + return inconsistencies +} + +function deleteUnusedKeys (unusedKeys, localeData, isDryRun = false) { + const results = {} + + Object.entries(localeData).forEach(([locale, data]) => { + results[locale] = { + deleted: [], + errors: [] + } + + try { + const updatedData = { ...data.data } + + unusedKeys.forEach(key => { + if (key in updatedData) { + if (!isDryRun) { + delete updatedData[key] + } + results[locale].deleted.push(key) + } + }) + + if (!isDryRun && results[locale].deleted.length > 0) { + const yamlContent = dump(updatedData, { + indent: 2, + lineWidth: -1, + noRefs: true, + sortKeys: false + }) + writeFileSync(data.file, yamlContent, 'utf8') + } + } catch (error) { + results[locale].errors.push(`Failed to process ${locale}: ${error.message}`) + } + }) + + return results +} + +function main () { + console.log(colorize('🌐 PicList - I18n Usage Analyzer', 'cyan')) + console.log(colorize('==================================', 'cyan')) + + const { allKeys, localeData } = getAllI18nKeys() + + console.log(colorize(`\nšŸ“Š Total unique keys found: ${allKeys.length}`, 'yellow')) + const usage = findKeyUsage(allKeys) + const dynamicPatterns = usage._dynamicPatterns || [] + delete usage._dynamicPatterns + + const usedKeys = allKeys.filter(key => usage[key].used) + const unusedKeys = allKeys.filter(key => !usage[key].used) + const dynamicallyUsedKeys = usedKeys.filter(key => usage[key].dynamicMatch) + const staticUsedKeys = usedKeys.filter(key => !usage[key].dynamicMatch) + + const inconsistencies = findLocaleInconsistencies(localeData) + + console.log(colorize('\nšŸ“ˆ Usage Summary:', 'blue')) + console.log(` ${colorize('āœ“', 'green')} Used keys: ${usedKeys.length}`) + console.log(` ${colorize('→', 'cyan')} Static usage: ${staticUsedKeys.length}`) + console.log(` ${colorize('→', 'magenta')} Dynamic usage: ${dynamicallyUsedKeys.length}`) + console.log(` ${colorize('āœ—', 'red')} Unused keys: ${unusedKeys.length}`) + console.log(` ${colorize('šŸ“Š', 'yellow')} Usage rate: ${((usedKeys.length / allKeys.length) * 100).toFixed(1)}%`) + + if (dynamicPatterns.length > 0) { + console.log(colorize('\nšŸ”® Dynamic I18n Patterns Detected:', 'magenta')) + console.log(colorize('===================================', 'magenta')) + + dynamicPatterns.forEach((pattern, index) => { + console.log(colorize(`\n${index + 1}. Template: \`${pattern.template}\``, 'cyan')) + console.log(` File: ${pattern.file}`) + console.log(` Static parts: [${pattern.staticParts.map(p => `"${p}"`).join(', ')}]`) + + const matchingKeys = allKeys.filter(key => matchesDynamicPattern(key, pattern.staticParts)) + if (matchingKeys.length > 0) { + console.log( + ` ${colorize('Matches', 'green')} (${matchingKeys.length}): ${matchingKeys.slice(0, 5).join(', ')}${ + matchingKeys.length > 5 ? '...' : '' + }` + ) + } + }) + } + + if (unusedKeys.length > 0) { + console.log(colorize('\nšŸ—‘ļø Unused I18n Keys:', 'red')) + console.log(colorize('====================', 'red')) + + const groupedUnused = {} + unusedKeys.forEach(key => { + const namespace = key.includes('.') ? key.split('.')[0] : 'ROOT' + if (!groupedUnused[namespace]) { + groupedUnused[namespace] = [] + } + groupedUnused[namespace].push(key) + }) + + Object.entries(groupedUnused).forEach(([namespace, keys]) => { + console.log(colorize(`\n[${namespace}] - ${keys.length} unused keys:`, 'yellow')) + keys.forEach(key => { + console.log(` ${colorize('āœ—', 'red')} ${key}`) + }) + }) + } else { + console.log(colorize('\nšŸŽ‰ No unused keys found! All i18n keys are being used.', 'green')) + } + + const hasInconsistencies = Object.values(inconsistencies).some(inc => inc.missing.length > 0 || inc.extra.length > 0) + + if (hasInconsistencies) { + console.log(colorize('\nāš ļø Locale Inconsistencies:', 'yellow')) + console.log(colorize('=========================', 'yellow')) + + Object.entries(inconsistencies).forEach(([locale, data]) => { + if (data.missing.length > 0 || data.extra.length > 0) { + console.log(colorize(`\n[${locale}.yml]:`, 'cyan')) + + if (data.missing.length > 0) { + console.log(colorize(` Missing ${data.missing.length} keys:`, 'red')) + data.missing.forEach(key => { + console.log(` ${colorize('āœ—', 'red')} ${key}`) + }) + } + + if (data.extra.length > 0) { + console.log(colorize(` Extra ${data.extra.length} keys:`, 'blue')) + data.extra.forEach(key => { + console.log(` ${colorize('!', 'blue')} ${key}`) + }) + } + } + }) + } + + if (isVerbose) { + console.log(colorize('\nšŸ“‹ Sample Used Keys (first 10):', 'blue')) + console.log(colorize('=================================', 'blue')) + + usedKeys.slice(0, 10).forEach(key => { + const files = usage[key].files.slice(0, 3) + const moreFiles = usage[key].files.length > 3 ? ` (+${usage[key].files.length - 3} more)` : '' + const usageType = usage[key].dynamicMatch ? colorize('(dynamic)', 'magenta') : colorize('(static)', 'cyan') + console.log(` ${colorize('āœ“', 'green')} ${key} ${usageType}`) + console.log(` Used in: ${files.join(', ')}${moreFiles}`) + }) + + if (dynamicallyUsedKeys.length > 0) { + console.log(colorize('\nšŸ”® Dynamic Key Usage Details:', 'magenta')) + console.log(colorize('=============================', 'magenta')) + + dynamicallyUsedKeys.slice(0, 5).forEach(key => { + const files = usage[key].files.slice(0, 2) + console.log(` ${colorize('✨', 'magenta')} ${key}`) + console.log(` Files: ${files.join(', ')}`) + }) + + if (dynamicallyUsedKeys.length > 5) { + console.log(` ... and ${dynamicallyUsedKeys.length - 5} more dynamic keys`) + } + } + } + + if (shouldDelete && unusedKeys.length > 0) { + console.log(colorize('\nšŸ—‘ļø Deleting Unused Keys:', 'yellow')) + console.log(colorize('=========================', 'yellow')) + + if (isDryRun) { + console.log(colorize('šŸ” DRY RUN MODE - No files will be modified', 'cyan')) + } + + const deletionResults = deleteUnusedKeys(unusedKeys, localeData, isDryRun) + + Object.entries(deletionResults).forEach(([locale, result]) => { + console.log(colorize(`\n[${locale}.yml]:`, 'cyan')) + if (result.deleted.length > 0) { + console.log(colorize(` Deleted ${result.deleted.length} keys:`, 'green')) + result.deleted.forEach(key => { + console.log(` ${colorize('āœ“', 'green')} ${key}`) + }) + } + if (result.errors.length > 0) { + console.log(colorize(` Errors ${result.errors.length}:`, 'red')) + result.errors.forEach(error => { + console.log(` ${colorize('āœ—', 'red')} ${error}`) + }) + } + }) + + if (!isDryRun) { + console.log(colorize('\n✨ Deletion complete! Remember to regenerate type definitions.', 'green')) + console.log(colorize('šŸ’” Run `yarn i18n` to update TypeScript types', 'blue')) + } + } + + console.log(colorize('\n✨ Analysis complete!', 'cyan')) + + if (unusedKeys.length > 0 && !shouldDelete) { + console.log(colorize('\nšŸ’” Tips:', 'blue')) + console.log(colorize('- Run with --verbose (-v) flag to see usage details of used keys', 'blue')) + console.log(colorize('- Run with --delete (-d) flag to automatically remove unused keys', 'blue')) + console.log(colorize('- Run with --delete --dry-run to preview what would be deleted', 'blue')) + } else if (unusedKeys.length === 0) { + console.log(colorize('\nšŸ’” Tip: Run with --verbose (-v) flag to see usage details of used keys', 'blue')) + } + + if (unusedKeys.length > 0 && !shouldDelete) { + process.exit(1) + } +} + +main() diff --git a/scripts/find-unused-i18n.mjs b/scripts/find-unused-i18n.mjs deleted file mode 100644 index e932af95..00000000 --- a/scripts/find-unused-i18n.mjs +++ /dev/null @@ -1,499 +0,0 @@ -#!/usr/bin/env node - -import { readdirSync, readFileSync, writeFileSync } from 'node:fs' -import { basename, dirname, extname, join, relative } from 'node:path' -import { fileURLToPath } from 'node:url' -import { load, dump } from 'js-yaml' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) - -const LOCALE_DIR = join(__dirname, '../public/i18n') -const SRC_DIR = join(__dirname, '../src') - -// Parse command line arguments -const args = process.argv.slice(2) -const isVerbose = args.includes('--verbose') || args.includes('-v') -const shouldDelete = args.includes('--delete') || args.includes('-d') -const isDryRun = args.includes('--dry-run') - -const colors = { - reset: '\x1b[0m', - bright: '\x1b[1m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - magenta: '\x1b[35m', - cyan: '\x1b[36m' -} - -function colorize(text, color) { - return `${colors[color]}${text}${colors.reset}` -} - -console.log(`\nšŸ” Analyzing i18n keys in ${LOCALE_DIR} and source files in ${SRC_DIR}`) -if (shouldDelete) { - console.log(colorize(`šŸ—‘ļø Delete mode enabled ${isDryRun ? '(DRY RUN)' : ''}`, 'yellow')) -} -console.log('') - -function flattenKeys(obj, prefix = '') { - const keys = [] - - for (const [key, value] of Object.entries(obj)) { - const fullKey = prefix ? `${prefix}.${key}` : key - - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - keys.push(...flattenKeys(value, fullKey)) - } else { - keys.push(fullKey) - } - } - - return keys -} - -function readLocaleFile(filePath) { - try { - const content = readFileSync(filePath, 'utf8') - return load(content) || {} - } catch (error) { - console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red')) - return {} - } -} - -function getAllI18nKeys() { - const localeFiles = readdirSync(LOCALE_DIR).filter(file => file.endsWith('.yml')) - const allKeys = new Set() - const localeData = {} - - console.log(colorize('\nšŸ“ Found locale files:', 'blue')) - - for (const file of localeFiles) { - const filePath = join(LOCALE_DIR, file) - const locale = basename(file, '.yml') - const data = readLocaleFile(filePath) - const keys = flattenKeys(data) - - localeData[locale] = { - file: filePath, - keys, - data - } - - keys.forEach(key => allKeys.add(key)) - - console.log(` ${colorize('āœ“', 'green')} ${file} (${keys.length} keys)`) - } - - return { - allKeys: Array.from(allKeys).sort(), - localeData - } -} - -function findFiles(dir, extensions = ['.vue', '.ts', '.js']) { - const files = [] - - function walk(currentDir) { - try { - const entries = readdirSync(currentDir, { withFileTypes: true }) - - for (const entry of entries) { - const fullPath = join(currentDir, entry.name) - - if (entry.isDirectory()) { - if (!['node_modules', '.git', 'dist', 'build', 'target', 'dist_electron'].includes(entry.name)) { - walk(fullPath) - } - } else if (entry.isFile()) { - const ext = extname(entry.name) - if (extensions.includes(ext)) { - files.push(fullPath) - } - } - } - } catch (error) { - console.warn(colorize(`Warning: Cannot read directory ${currentDir}: ${error.message}`, 'yellow')) - } - } - - walk(dir) - return files -} - -function findKeyUsage(keys) { - const usage = {} - const dynamicPatterns = [] - - keys.forEach(key => { - usage[key] = { - used: false, - files: [], - patterns: [], - dynamicMatch: false - } - }) - - console.log(colorize('\nšŸ” Searching for key usage in source files...', 'blue')) - - const sourceFiles = findFiles(SRC_DIR) - - console.log(` Found ${sourceFiles.length} source files to analyze`) - - const searchPatterns = [ - /\$T\s*\(\s*['"`]([^'"`]+)['"`]/g, - /(?:^|[^a-zA-Z$])T\s*\(\s*['"`]([^'"`]+)['"`]/g, - /\{\{\s*\$T\s*\(\s*['"`]([^'"`]+)['"`]/g, - /i18nManager\.T\s*\(\s*['"`]([^'"`]+)['"`]/g - ] - - const dynamicPattern = /\$T\s*\(\s*`([^`]*\$\{[^}]+\}[^`]*)`/g - - sourceFiles.forEach(filePath => { - try { - const content = readFileSync(filePath, 'utf8') - const relativePath = relative(join(__dirname, '..'), filePath) - - searchPatterns.forEach((pattern, patternIndex) => { - let match - while ((match = pattern.exec(content)) !== null) { - const key = match[1] - if (usage[key]) { - usage[key].used = true - if (!usage[key].files.includes(relativePath)) { - usage[key].files.push(relativePath) - } - if (!usage[key].patterns.includes(patternIndex)) { - usage[key].patterns.push(patternIndex) - } - } - } - }) - - let dynamicMatch - while ((dynamicMatch = dynamicPattern.exec(content)) !== null) { - const templateString = dynamicMatch[1] - - const staticParts = templateString.split(/\$\{[^}]+\}/) - - const patternInfo = { - template: templateString, - file: relativePath, - staticParts - } - - if (!dynamicPatterns.some(p => p.template === templateString && p.file === relativePath)) { - dynamicPatterns.push(patternInfo) - } - - keys.forEach(key => { - if (matchesDynamicPattern(key, staticParts)) { - if (usage[key]) { - usage[key].used = true - usage[key].dynamicMatch = true - if (!usage[key].files.includes(relativePath)) { - usage[key].files.push(relativePath) - } - if (!usage[key].patterns.includes('dynamic')) { - usage[key].patterns.push('dynamic') - } - } - } - }) - } - } catch (error) { - console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red')) - } - }) - - usage._dynamicPatterns = dynamicPatterns - - return usage -} - -function matchesDynamicPattern(key, staticParts) { - if (staticParts.length === 0) return false - - let keyIndex = 0 - - for (let i = 0; i < staticParts.length; i++) { - const part = staticParts[i] - - if (part === '') { - if (i < staticParts.length - 1) { - const nextPart = staticParts[i + 1] - if (nextPart) { - const nextIndex = key.indexOf(nextPart, keyIndex) - if (nextIndex === -1) return false - keyIndex = nextIndex - } - } - continue - } - - if (i === 0) { - if (!key.startsWith(part)) return false - keyIndex = part.length - } else if (i === staticParts.length - 1) { - if (part && !key.endsWith(part)) return false - } else { - const index = key.indexOf(part, keyIndex) - if (index === -1) return false - keyIndex = index + part.length - } - } - - return true -} - -function findLocaleInconsistencies(localeData) { - const locales = Object.keys(localeData) - const inconsistencies = {} - - if (locales.length < 2) { - return inconsistencies - } - - locales.forEach(locale => { - const currentKeys = new Set(localeData[locale].keys) - inconsistencies[locale] = { - missing: [], - extra: [] - } - - locales.forEach(otherLocale => { - if (locale !== otherLocale) { - localeData[otherLocale].keys.forEach(key => { - if (!currentKeys.has(key) && !inconsistencies[locale].missing.includes(key)) { - inconsistencies[locale].missing.push(key) - } - }) - } - }) - - localeData[locale].keys.forEach(key => { - const existsInOthers = locales.some( - otherLocale => locale !== otherLocale && localeData[otherLocale].keys.includes(key) - ) - if (!existsInOthers) { - inconsistencies[locale].extra.push(key) - } - }) - }) - - return inconsistencies -} - -function deleteUnusedKeys(unusedKeys, localeData, isDryRun = false) { - const results = {} - - Object.entries(localeData).forEach(([locale, data]) => { - results[locale] = { - deleted: [], - errors: [] - } - - try { - const updatedData = { ...data.data } - - unusedKeys.forEach(key => { - if (key in updatedData) { - if (!isDryRun) { - delete updatedData[key] - } - results[locale].deleted.push(key) - } - }) - - if (!isDryRun && results[locale].deleted.length > 0) { - const yamlContent = dump(updatedData, { - indent: 2, - lineWidth: -1, - noRefs: true, - sortKeys: false - }) - writeFileSync(data.file, yamlContent, 'utf8') - } - } catch (error) { - results[locale].errors.push(`Failed to process ${locale}: ${error.message}`) - } - }) - - return results -} - -function main() { - console.log(colorize('🌐 PicList - I18n Usage Analyzer', 'cyan')) - console.log(colorize('==================================', 'cyan')) - - const { allKeys, localeData } = getAllI18nKeys() - - console.log(colorize(`\nšŸ“Š Total unique keys found: ${allKeys.length}`, 'yellow')) - const usage = findKeyUsage(allKeys) - const dynamicPatterns = usage._dynamicPatterns || [] - delete usage._dynamicPatterns - - const usedKeys = allKeys.filter(key => usage[key].used) - const unusedKeys = allKeys.filter(key => !usage[key].used) - const dynamicallyUsedKeys = usedKeys.filter(key => usage[key].dynamicMatch) - const staticUsedKeys = usedKeys.filter(key => !usage[key].dynamicMatch) - - const inconsistencies = findLocaleInconsistencies(localeData) - - console.log(colorize('\nšŸ“ˆ Usage Summary:', 'blue')) - console.log(` ${colorize('āœ“', 'green')} Used keys: ${usedKeys.length}`) - console.log(` ${colorize('→', 'cyan')} Static usage: ${staticUsedKeys.length}`) - console.log(` ${colorize('→', 'magenta')} Dynamic usage: ${dynamicallyUsedKeys.length}`) - console.log(` ${colorize('āœ—', 'red')} Unused keys: ${unusedKeys.length}`) - console.log(` ${colorize('šŸ“Š', 'yellow')} Usage rate: ${((usedKeys.length / allKeys.length) * 100).toFixed(1)}%`) - - if (dynamicPatterns.length > 0) { - console.log(colorize('\nšŸ”® Dynamic I18n Patterns Detected:', 'magenta')) - console.log(colorize('===================================', 'magenta')) - - dynamicPatterns.forEach((pattern, index) => { - console.log(colorize(`\n${index + 1}. Template: \`${pattern.template}\``, 'cyan')) - console.log(` File: ${pattern.file}`) - console.log(` Static parts: [${pattern.staticParts.map(p => `"${p}"`).join(', ')}]`) - - const matchingKeys = allKeys.filter(key => matchesDynamicPattern(key, pattern.staticParts)) - if (matchingKeys.length > 0) { - console.log( - ` ${colorize('Matches', 'green')} (${matchingKeys.length}): ${matchingKeys.slice(0, 5).join(', ')}${ - matchingKeys.length > 5 ? '...' : '' - }` - ) - } - }) - } - - if (unusedKeys.length > 0) { - console.log(colorize('\nšŸ—‘ļø Unused I18n Keys:', 'red')) - console.log(colorize('====================', 'red')) - - const groupedUnused = {} - unusedKeys.forEach(key => { - const namespace = key.includes('.') ? key.split('.')[0] : 'ROOT' - if (!groupedUnused[namespace]) { - groupedUnused[namespace] = [] - } - groupedUnused[namespace].push(key) - }) - - Object.entries(groupedUnused).forEach(([namespace, keys]) => { - console.log(colorize(`\n[${namespace}] - ${keys.length} unused keys:`, 'yellow')) - keys.forEach(key => { - console.log(` ${colorize('āœ—', 'red')} ${key}`) - }) - }) - } else { - console.log(colorize('\nšŸŽ‰ No unused keys found! All i18n keys are being used.', 'green')) - } - - const hasInconsistencies = Object.values(inconsistencies).some(inc => inc.missing.length > 0 || inc.extra.length > 0) - - if (hasInconsistencies) { - console.log(colorize('\nāš ļø Locale Inconsistencies:', 'yellow')) - console.log(colorize('=========================', 'yellow')) - - Object.entries(inconsistencies).forEach(([locale, data]) => { - if (data.missing.length > 0 || data.extra.length > 0) { - console.log(colorize(`\n[${locale}.yml]:`, 'cyan')) - - if (data.missing.length > 0) { - console.log(colorize(` Missing ${data.missing.length} keys:`, 'red')) - data.missing.forEach(key => { - console.log(` ${colorize('āœ—', 'red')} ${key}`) - }) - } - - if (data.extra.length > 0) { - console.log(colorize(` Extra ${data.extra.length} keys:`, 'blue')) - data.extra.forEach(key => { - console.log(` ${colorize('!', 'blue')} ${key}`) - }) - } - } - }) - } - - if (isVerbose) { - console.log(colorize('\nšŸ“‹ Sample Used Keys (first 10):', 'blue')) - console.log(colorize('=================================', 'blue')) - - usedKeys.slice(0, 10).forEach(key => { - const files = usage[key].files.slice(0, 3) - const moreFiles = usage[key].files.length > 3 ? ` (+${usage[key].files.length - 3} more)` : '' - const usageType = usage[key].dynamicMatch ? colorize('(dynamic)', 'magenta') : colorize('(static)', 'cyan') - console.log(` ${colorize('āœ“', 'green')} ${key} ${usageType}`) - console.log(` Used in: ${files.join(', ')}${moreFiles}`) - }) - - if (dynamicallyUsedKeys.length > 0) { - console.log(colorize('\nšŸ”® Dynamic Key Usage Details:', 'magenta')) - console.log(colorize('=============================', 'magenta')) - - dynamicallyUsedKeys.slice(0, 5).forEach(key => { - const files = usage[key].files.slice(0, 2) - console.log(` ${colorize('✨', 'magenta')} ${key}`) - console.log(` Files: ${files.join(', ')}`) - }) - - if (dynamicallyUsedKeys.length > 5) { - console.log(` ... and ${dynamicallyUsedKeys.length - 5} more dynamic keys`) - } - } - } - - if (shouldDelete && unusedKeys.length > 0) { - console.log(colorize('\nšŸ—‘ļø Deleting Unused Keys:', 'yellow')) - console.log(colorize('=========================', 'yellow')) - - if (isDryRun) { - console.log(colorize('šŸ” DRY RUN MODE - No files will be modified', 'cyan')) - } - - const deletionResults = deleteUnusedKeys(unusedKeys, localeData, isDryRun) - - Object.entries(deletionResults).forEach(([locale, result]) => { - console.log(colorize(`\n[${locale}.yml]:`, 'cyan')) - if (result.deleted.length > 0) { - console.log(colorize(` Deleted ${result.deleted.length} keys:`, 'green')) - result.deleted.forEach(key => { - console.log(` ${colorize('āœ“', 'green')} ${key}`) - }) - } - if (result.errors.length > 0) { - console.log(colorize(` Errors ${result.errors.length}:`, 'red')) - result.errors.forEach(error => { - console.log(` ${colorize('āœ—', 'red')} ${error}`) - }) - } - }) - - if (!isDryRun) { - console.log(colorize('\n✨ Deletion complete! Remember to regenerate type definitions.', 'green')) - console.log(colorize('šŸ’” Run `yarn i18n` to update TypeScript types', 'blue')) - } - } - - console.log(colorize('\n✨ Analysis complete!', 'cyan')) - - if (unusedKeys.length > 0 && !shouldDelete) { - console.log(colorize('\nšŸ’” Tips:', 'blue')) - console.log(colorize('- Run with --verbose (-v) flag to see usage details of used keys', 'blue')) - console.log(colorize('- Run with --delete (-d) flag to automatically remove unused keys', 'blue')) - console.log(colorize('- Run with --delete --dry-run to preview what would be deleted', 'blue')) - } else if (unusedKeys.length === 0) { - console.log(colorize('\nšŸ’” Tip: Run with --verbose (-v) flag to see usage details of used keys', 'blue')) - } - - if (unusedKeys.length > 0 && !shouldDelete) { - process.exit(1) - } -} - -main() diff --git a/scripts/gen-i18n-types.js b/scripts/gen-i18n-types.js index d0e29b14..15adba71 100644 --- a/scripts/gen-i18n-types.js +++ b/scripts/gen-i18n-types.js @@ -1,21 +1,27 @@ -const yaml = require('js-yaml') -const path = require('path') -const fs = require('fs') -const languageFileName = 'zh-CN.yml' // use zh-CN for type is OK -const i18nFolder = path.join(__dirname, '../public/i18n') -const typeFolder = path.join(__dirname, '../src/universal/types') -const languageFile = path.join(i18nFolder, languageFileName) +import { readFileSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' -const langFile = fs.readFileSync(languageFile, 'utf8') +import { load } from 'js-yaml' +const languageFileName = 'zh-CN.yml' -const obj = yaml.load(langFile) +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const i18nFolder = join(__dirname, '../public/i18n') +const typeFolder = join(__dirname, '../src/universal/types') +const languageFile = join(i18nFolder, languageFileName) + +const langFile = readFileSync(languageFile, 'utf8') + +const obj = load(langFile) const keys = Object.keys(obj) -const types = `interface ILocales { +const types = `export interface ILocales { ${keys.map(key => `${key}: string`).join('\n ')} } -type ILocalesKey = keyof ILocales +export type ILocalesKey = keyof ILocales ` -fs.writeFileSync(path.join(typeFolder, 'i18n.d.ts'), types) +writeFileSync(join(typeFolder, 'i18n.ts'), types) diff --git a/scripts/gen-sha256.js b/scripts/gen-sha256.cjs similarity index 92% rename from scripts/gen-sha256.js rename to scripts/gen-sha256.cjs index 388475d9..0dc49bd2 100644 --- a/scripts/gen-sha256.js +++ b/scripts/gen-sha256.cjs @@ -1,6 +1,6 @@ -const crypto = require('crypto') -const os = require('os') -const path = require('path') +const crypto = require('node:crypto') +const os = require('node:os') +const path = require('node:path') const axios = require('axios') const fs = require('fs-extra') const pkg = require('../package.json') @@ -24,7 +24,7 @@ const files = [ /** * Create progress bar string */ -function getProgressBar(current, total, length = 20) { +function getProgressBar (current, total, length = 20) { const progress = Math.round((current / total) * length) const percentage = Math.round((current / total) * 100) const bar = 'ā–ˆ'.repeat(progress) + 'ā–‘'.repeat(length - progress) @@ -34,7 +34,7 @@ function getProgressBar(current, total, length = 20) { /** * Format bytes to human-readable format */ -function formatBytes(bytes, decimals = 2) { +function formatBytes (bytes, decimals = 2) { if (bytes === 0) return '0 Bytes' const k = 1024 const sizes = ['Bytes', 'KB', 'MB', 'GB'] @@ -45,7 +45,7 @@ function formatBytes(bytes, decimals = 2) { /** * Download file and calculate SHA256 hash */ -async function downloadAndHash(fileInfo) { +async function downloadAndHash (fileInfo) { const { url, name } = fileInfo const filePath = path.join(DOWNLOAD_DIR, name) @@ -101,7 +101,7 @@ async function downloadAndHash(fileInfo) { /** * Main function */ -async function main() { +async function main () { console.log(`Generating SHA256 hashes for PicList v${version}`) console.log(`Download directory: ${DOWNLOAD_DIR}`) diff --git a/scripts/link.js b/scripts/link.cjs similarity index 100% rename from scripts/link.js rename to scripts/link.cjs diff --git a/scripts/notarize.js b/scripts/notarize.cjs similarity index 96% rename from scripts/notarize.js rename to scripts/notarize.cjs index 3435af2a..53c8ff37 100644 --- a/scripts/notarize.js +++ b/scripts/notarize.cjs @@ -5,7 +5,7 @@ require('dotenv').config() const { notarize } = require('@electron/notarize') const { ELECTRON_SKIP_NOTARIZATION, XCODE_APP_LOADER_EMAIL, XCODE_APP_LOADER_PASSWORD, XCODE_TEAM_ID } = process.env -async function main(context) { +async function main (context) { const { electronPlatformName, appOutDir } = context if ( diff --git a/scripts/upload-beta.js b/scripts/upload-beta.cjs similarity index 97% rename from scripts/upload-beta.js rename to scripts/upload-beta.cjs index 203fe3de..2185eef3 100644 --- a/scripts/upload-beta.js +++ b/scripts/upload-beta.cjs @@ -4,8 +4,8 @@ const S3Client = require('@aws-sdk/client-s3') const Upload = require('@aws-sdk/lib-storage') const pkg = require('../package.json') const configList = require('./config') -const fs = require('fs') -const path = require('path') +const fs = require('node:fs') +const path = require('node:path') const BUCKET = 'piclist-dl' const VERSION = pkg.version diff --git a/scripts/upload-dist-to-r2.js b/scripts/upload-dist-to-r2.cjs similarity index 98% rename from scripts/upload-dist-to-r2.js rename to scripts/upload-dist-to-r2.cjs index c05b4b0b..bbe3b262 100644 --- a/scripts/upload-dist-to-r2.js +++ b/scripts/upload-dist-to-r2.cjs @@ -5,8 +5,8 @@ const S3Client = require('@aws-sdk/client-s3') const Upload = require('@aws-sdk/lib-storage') const pkg = require('../package.json') const configList = require('./config') -const fs = require('fs') -const path = require('path') +const fs = require('node:fs') +const path = require('node:path') const yaml = require('js-yaml') const mime = require('mime-types')