From a81f0ed9ebd1c7db2a96465f63be22ff8eb5293d Mon Sep 17 00:00:00 2001 From: Kuingsmile <96409857+Kuingsmile@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:13:30 +0800 Subject: [PATCH] :package: Chore(custom): add prettier rules --- eslint.config.mjs | 24 +- package.json | 23 +- scripts/find-unused-i18n.js | 808 +++++++++--------- src/App.vue | 11 +- .../{Navigation.vue => NavigationPage.vue} | 95 +- src/components/dashboard/CoreMonitorCard.vue | 19 +- .../dashboard/DocumentationCard.vue | 27 +- src/components/dashboard/QuickActionsCard.vue | 38 +- .../dashboard/ServiceManagementCard.vue | 40 +- .../dashboard/UpdateManagerCard.vue | 25 +- .../dashboard/VersionManagerCard.vue | 17 +- src/components/ui/{Card.vue => CardPage.vue} | 1 + src/components/ui/ConfirmDialog.vue | 8 +- src/components/ui/LanguageSwitcher.vue | 9 +- src/components/ui/ThemeSwitcher.vue | 7 +- src/components/ui/TitleBar.vue | 3 +- src/components/ui/UpdateNotification.vue | 10 +- src/components/ui/WindowControls.vue | 9 +- src/views/HomeView.vue | 8 +- src/views/LogView.vue | 63 +- src/views/MountView.vue | 80 +- src/views/SettingsView.vue | 45 +- src/views/UpdateView.vue | 7 +- src/vite-env.d.ts | 14 +- yarn.lock | 534 ++++++++---- 25 files changed, 1102 insertions(+), 823 deletions(-) rename src/components/{Navigation.vue => NavigationPage.vue} (98%) rename src/components/ui/{Card.vue => CardPage.vue} (99%) diff --git a/eslint.config.mjs b/eslint.config.mjs index f7075f5..bb003d1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,7 +1,8 @@ -// @ts-check import eslint from '@eslint/js' +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' import simpleImportSort from 'eslint-plugin-simple-import-sort' import eslintPluginUnicorn from 'eslint-plugin-unicorn' +import pluginVue from 'eslint-plugin-vue' import globals from 'globals' import tseslint from 'typescript-eslint' @@ -24,6 +25,8 @@ export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommended, ...tseslint.configs.stylistic, + ...pluginVue.configs['flat/recommended'], + eslintPluginPrettierRecommended, { plugins: { 'simple-import-sort': simpleImportSort, @@ -39,7 +42,10 @@ export default tseslint.config( parserOptions: { warnOnUnsupportedTypeScriptVersion: false }, - globals: globals.node + globals: { + ...globals.node, + ...globals.browser + } } }, { @@ -102,5 +108,19 @@ export default tseslint.config( { name: 'exports' } ] } + }, + { + files: ['*.vue', '**/*.vue'], + rules: { + 'no-undef': 'off' + }, + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: globals.browser, + parserOptions: { + parser: tseslint.parser + } + } } ) diff --git a/package.json b/package.json index 80e44a6..eb19ecd 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,9 @@ "tauri:dev": "cross-env RUST_BACKTRACE=1 tauri dev", "tauri": "tauri", "nowatch": "tauri dev --no-watch", - "lint": "eslint src/**/*.ts", - "lint:fix": "eslint src/**/*.ts --fix", + "lint": "eslint --ext .js,.jsx,.ts,.tsx,.vue src/ scripts/", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx,.vue src/ scripts/ --fix", + "lint:dpdm": "dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/main.ts", "i18n:check": "node scripts/find-unused-i18n.js", "i18n:check:verbose": "node scripts/find-unused-i18n.js --verbose", "cz": "git-cz", @@ -63,36 +64,42 @@ "@tauri-apps/plugin-fs": "^2.4.2", "@tauri-apps/plugin-opener": "^2.5.0", "@tauri-apps/plugin-process": "^2.3.0", - "@tauri-apps/plugin-shell": "^2.3.0", + "@tauri-apps/plugin-shell": "^2.3.1", "@tauri-apps/plugin-store": "^2.4.0", "chrono-node": "^2.8.4", - "lucide-vue-next": "^0.541.0", + "lucide-vue-next": "^0.542.0", "pinia": "^3.0.3", "vue": "^3.5.19", "vue-i18n": "11.1.11", "vue-router": "^4.5.1" }, "devDependencies": { - "@tauri-apps/cli": "^2.8.2", + "@tauri-apps/cli": "^2.8.3", "@types/node": "^24.3.0", - "@typescript-eslint/eslint-plugin": "^8.40.0", - "@typescript-eslint/parser": "^8.40.0", + "@typescript-eslint/eslint-plugin": "^8.41.0", + "@typescript-eslint/parser": "^8.41.0", "@vitejs/plugin-vue": "^6.0.1", "adm-zip": "^0.5.16", "cross-env": "^7.0.3", + "dpdm": "^3.14.0", "eslint": "^9.34.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unicorn": "^60.0.0", + "eslint-plugin-vue": "^10.4.0", "fs-extra": "^11.3.1", "https-proxy-agent": "^7.0.6", "husky": "^9.1.7", "lint-staged": "^16.1.5", "node-bump-version": "^2.0.0", "node-fetch": "^3.3.2", + "prettier": "^3.6.2", "tar": "^7.4.3", "typescript": "^5.8.3", - "typescript-eslint": "^8.40.0", + "typescript-eslint": "^8.41.0", "vite": "^7.1.3", + "vue-eslint-parser": "^10.2.0", "vue-tsc": "^3.0.6" } } diff --git a/scripts/find-unused-i18n.js b/scripts/find-unused-i18n.js index 271fd4d..760b86b 100644 --- a/scripts/find-unused-i18n.js +++ b/scripts/find-unused-i18n.js @@ -1,404 +1,404 @@ -#!/usr/bin/env node - -import { readdirSync, readFileSync } from 'node:fs' -import { basename, dirname, extname, join, relative } from 'node:path' -import { fileURLToPath } from 'node:url' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) - -const LOCALE_DIR = join(__dirname, '../src/i18n/locales') -const SRC_DIR = join(__dirname, '../src') - -console.log(`\nš Analyzing i18n keys in ${LOCALE_DIR} and source files in ${SRC_DIR}\n`) - -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}` -} - -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 JSON.parse(content) - } catch (error) { - console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red')) - return {} - } -} - -function getAllI18nKeys() { - const localeFiles = readdirSync(LOCALE_DIR).filter(file => file.endsWith('.json')) - 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, '.json') - 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) { - 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'].includes(entry.name)) { - walk(fullPath) - } - } else if (entry.isFile()) { - const ext = extname(entry.name) - if (extensions.includes(ext)) { - files.push(fullPath) - } - } - } - } - - 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 - ] - - 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 main() { - console.log(colorize('š OpenList Desktop - 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.split('.')[0] - 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}.json]:`, '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 (process.argv.includes('--verbose') || process.argv.includes('-v')) { - 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) // Show first 3 files - 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`) - } - } - } - - console.log(colorize('\n⨠Analysis complete!', 'cyan')) - - if (unusedKeys.length > 0) { - console.log(colorize('\nš” Tip: Run with --verbose (-v) flag to see usage details of used keys', 'blue')) - } -} - -main() +#!/usr/bin/env node + +import { readdirSync, readFileSync } from 'node:fs' +import { basename, dirname, extname, join, relative } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const LOCALE_DIR = join(__dirname, '../src/i18n/locales') +const SRC_DIR = join(__dirname, '../src') + +console.log(`\nš Analyzing i18n keys in ${LOCALE_DIR} and source files in ${SRC_DIR}\n`) + +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}` +} + +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 JSON.parse(content) + } catch (error) { + console.error(colorize(`Error reading ${filePath}: ${error.message}`, 'red')) + return {} + } +} + +function getAllI18nKeys() { + const localeFiles = readdirSync(LOCALE_DIR).filter(file => file.endsWith('.json')) + 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, '.json') + 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) { + 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'].includes(entry.name)) { + walk(fullPath) + } + } else if (entry.isFile()) { + const ext = extname(entry.name) + if (extensions.includes(ext)) { + files.push(fullPath) + } + } + } + } + + 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 + ] + + 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 main() { + console.log(colorize('š OpenList Desktop - 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.split('.')[0] + 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}.json]:`, '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 (process.argv.includes('--verbose') || process.argv.includes('-v')) { + 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) // Show first 3 files + 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`) + } + } + } + + console.log(colorize('\n⨠Analysis complete!', 'cyan')) + + if (unusedKeys.length > 0) { + console.log(colorize('\nš” Tip: Run with --verbose (-v) flag to see usage details of used keys', 'blue')) + } +} + +main() diff --git a/src/App.vue b/src/App.vue index b89bcdc..a74d463 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,12 +2,12 @@ import { onMounted, onUnmounted, ref } from 'vue' import { useRouter } from 'vue-router' -import { useAppStore } from './stores/app' +import { TauriAPI } from './api/tauri' +import Navigation from './components/NavigationPage.vue' +import TitleBar from './components/ui/TitleBar.vue' import { useTranslation } from './composables/useI18n' import { useTray } from './composables/useTray' -import { TauriAPI } from './api/tauri' -import Navigation from './components/Navigation.vue' -import TitleBar from './components/ui/TitleBar.vue' +import { useAppStore } from './stores/app' const appStore = useAppStore() const { t } = useTranslation() @@ -365,7 +365,8 @@ body { .loading-backdrop { position: absolute; inset: 0; - background: radial-gradient(circle at 25% 25%, rgba(120, 119, 198, 0.3) 0%, transparent 50%), + background: + radial-gradient(circle at 25% 25%, rgba(120, 119, 198, 0.3) 0%, transparent 50%), radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.1) 0%, transparent 50%); } diff --git a/src/components/Navigation.vue b/src/components/NavigationPage.vue similarity index 98% rename from src/components/Navigation.vue rename to src/components/NavigationPage.vue index 1f25183..0ff0e78 100644 --- a/src/components/Navigation.vue +++ b/src/components/NavigationPage.vue @@ -1,49 +1,3 @@ - - @@ -82,9 +36,9 @@ const openLink = async (url: string) => { @@ -92,6 +46,53 @@ const openLink = async (url: string) => { + +