mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-06 20:42:57 +08:00
📦 Chore(custom): add i18n check scripts and action
This commit is contained in:
326
.github/workflows/i18n-check.yml
vendored
Normal file
326
.github/workflows/i18n-check.yml
vendored
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
name: 'I18n Check - Find Unused Translation Keys'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
auto_fix:
|
||||||
|
description: 'Automatically remove unused keys and create PR'
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, dev ]
|
||||||
|
paths:
|
||||||
|
- 'src/**/*.vue'
|
||||||
|
- 'src/**/*.ts'
|
||||||
|
- 'src/**/*.js'
|
||||||
|
- 'public/i18n/**/*.yml'
|
||||||
|
- 'scripts/find-unused-i18n.mjs'
|
||||||
|
- '.github/workflows/i18n-check.yml'
|
||||||
|
push:
|
||||||
|
branches: [ main, dev ]
|
||||||
|
paths:
|
||||||
|
- 'public/i18n/**/*.yml'
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
i18n-check:
|
||||||
|
name: Check for Unused I18n Keys
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run i18n unused keys check
|
||||||
|
id: i18n-check
|
||||||
|
run: |
|
||||||
|
echo "Running i18n unused keys analysis..."
|
||||||
|
|
||||||
|
# Run the script and capture output
|
||||||
|
OUTPUT=$(node scripts/find-unused-i18n.mjs 2>&1)
|
||||||
|
EXIT_CODE=$?
|
||||||
|
|
||||||
|
# Save the output to a file for the job summary
|
||||||
|
echo "$OUTPUT" > i18n-check-output.txt
|
||||||
|
|
||||||
|
# Also output to console
|
||||||
|
echo "$OUTPUT"
|
||||||
|
|
||||||
|
# Check if there are unused keys by looking for the specific pattern in output
|
||||||
|
if echo "$OUTPUT" | grep -q "🗑️ Unused I18n Keys:"; then
|
||||||
|
echo "unused_keys_found=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "❌ Found unused i18n keys!"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "unused_keys_found=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "✅ No unused i18n keys found!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate Job Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## 🌐 I18n Keys Analysis Report" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [ -f i18n-check-output.txt ]; then
|
||||||
|
echo "### 📊 Analysis Results" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
cat i18n-check-output.txt >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ steps.i18n-check.outputs.unused_keys_found }}" = "true" ]; then
|
||||||
|
echo "### ❌ Action Required" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Unused i18n keys were found. Consider:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- Removing unused keys from locale files" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- Verifying that the keys are actually unused" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- Adding usage for keys that should be kept" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "### ✅ All Clear" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "No unused i18n keys found. Great job maintaining clean translations! 🎉" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### 💡 Tips" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- Run \`yarn i18n:check\` locally to check for unused keys" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- Run \`yarn i18n:check:verbose\` for detailed usage information" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- This check runs automatically on PRs that modify Vue, TS, JS, or i18n files" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: Upload analysis results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: i18n-analysis-results
|
||||||
|
path: i18n-check-output.txt
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Comment on PR (if unused keys found)
|
||||||
|
if: github.event_name == 'pull_request' && steps.i18n-check.outputs.unused_keys_found == 'true'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
try {
|
||||||
|
output = fs.readFileSync('i18n-check-output.txt', 'utf8');
|
||||||
|
} catch (error) {
|
||||||
|
output = 'Unable to read analysis output';
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = `## 🌐 I18n Analysis Report
|
||||||
|
|
||||||
|
❌ **Unused i18n keys were found in this PR**
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>📊 Click to view detailed analysis</summary>
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
${output}
|
||||||
|
\`\`\`
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### 🔧 Action Required
|
||||||
|
|
||||||
|
Please review the unused keys listed above and consider:
|
||||||
|
- **Removing** keys that are truly unused
|
||||||
|
- **Verifying** that the detection is correct (some dynamic key usage might not be detected)
|
||||||
|
- **Adding usage** for keys that should be kept
|
||||||
|
|
||||||
|
### 💡 Local Testing
|
||||||
|
|
||||||
|
You can run this analysis locally using:
|
||||||
|
\`\`\`bash
|
||||||
|
yarn i18n:check # Basic check
|
||||||
|
yarn i18n:check:verbose # Detailed output with usage examples
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
*This comment was automatically generated by the I18n Check workflow.*`;
|
||||||
|
|
||||||
|
github.rest.issues.createComment({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: body
|
||||||
|
});
|
||||||
|
|
||||||
|
locale-consistency-check:
|
||||||
|
name: Check Locale File Consistency
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Check locale files consistency
|
||||||
|
run: |
|
||||||
|
echo "Checking if all locale files have the same structure..."
|
||||||
|
|
||||||
|
# Run the i18n script in verbose mode to also check for inconsistencies
|
||||||
|
node scripts/find-unused-i18n.mjs --verbose > locale-check.txt 2>&1
|
||||||
|
|
||||||
|
# Check if there are inconsistencies reported
|
||||||
|
if grep -q "⚠️ Locale Inconsistencies:" locale-check.txt; then
|
||||||
|
echo "❌ Found locale inconsistencies!"
|
||||||
|
cat locale-check.txt
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "✅ All locale files are consistent!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate Consistency Report
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## 🔄 Locale Consistency Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if grep -q "⚠️ Locale Inconsistencies:" locale-check.txt 2>/dev/null; then
|
||||||
|
echo "### ❌ Inconsistencies Found" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Some keys exist in one locale but not others:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
cat locale-check.txt >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "### ✅ All Locale Files Consistent" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "All locale files have matching key structures. Perfect! 🎉" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
auto-fix:
|
||||||
|
name: Auto-fix Unused I18n Keys
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.auto_fix == 'true'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Check for unused keys before deletion
|
||||||
|
id: check-before
|
||||||
|
run: |
|
||||||
|
echo "Checking for unused keys..."
|
||||||
|
|
||||||
|
# Check if there are unused keys
|
||||||
|
if node scripts/find-unused-i18n.mjs | grep -q "🗑️ Unused I18n Keys:"; then
|
||||||
|
echo "unused_keys_found=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "✅ Found unused keys to clean up"
|
||||||
|
else
|
||||||
|
echo "unused_keys_found=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "ℹ️ No unused keys found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Delete unused keys
|
||||||
|
if: steps.check-before.outputs.unused_keys_found == 'true'
|
||||||
|
run: |
|
||||||
|
echo "🗑️ Removing unused i18n keys..."
|
||||||
|
node scripts/find-unused-i18n.mjs --delete
|
||||||
|
|
||||||
|
- name: Regenerate TypeScript types
|
||||||
|
if: steps.check-before.outputs.unused_keys_found == 'true'
|
||||||
|
run: |
|
||||||
|
echo "🔄 Regenerating TypeScript types..."
|
||||||
|
yarn i18n
|
||||||
|
|
||||||
|
- name: Create Pull Request
|
||||||
|
if: steps.check-before.outputs.unused_keys_found == 'true'
|
||||||
|
uses: peter-evans/create-pull-request@v5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
commit-message: 'feat(i18n): remove unused translation keys'
|
||||||
|
title: '🗑️ Remove unused i18n translation keys'
|
||||||
|
body: |
|
||||||
|
## 🗑️ Automated I18n Cleanup
|
||||||
|
|
||||||
|
This PR automatically removes unused translation keys that were detected by the i18n analysis script.
|
||||||
|
|
||||||
|
### ✨ Changes Made
|
||||||
|
|
||||||
|
- Removed unused translation keys from all locale files
|
||||||
|
- Regenerated TypeScript type definitions
|
||||||
|
|
||||||
|
### 🔍 Analysis Details
|
||||||
|
|
||||||
|
The unused keys were identified by scanning the entire codebase for i18n usage patterns including:
|
||||||
|
- `$T('KEY')` - Template usage
|
||||||
|
- `T('KEY')` - Script usage
|
||||||
|
- `i18nManager.T('KEY')` - Direct manager usage
|
||||||
|
- Dynamic patterns with template literals
|
||||||
|
|
||||||
|
### ⚠️ Review Required
|
||||||
|
|
||||||
|
Please review the changes to ensure:
|
||||||
|
- No keys were removed that are used in ways not detected by the script
|
||||||
|
- All locale files maintain consistency
|
||||||
|
- The TypeScript types were properly regenerated
|
||||||
|
|
||||||
|
---
|
||||||
|
*This PR was automatically created by the I18n Auto-fix workflow.*
|
||||||
|
branch: feat/cleanup-unused-i18n-keys
|
||||||
|
delete-branch: true
|
||||||
|
|
||||||
|
- name: Generate Summary
|
||||||
|
run: |
|
||||||
|
if [ "${{ steps.check-before.outputs.unused_keys_found }}" = "true" ]; then
|
||||||
|
echo "## 🗑️ I18n Auto-fix Completed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "✅ Successfully removed unused translation keys and created a pull request." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### 📝 Next Steps" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "1. Review the created pull request" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "2. Verify that no important keys were removed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "3. Test the application to ensure everything works correctly" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "4. Merge the pull request when ready" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "## ℹ️ No Action Needed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "No unused i18n keys were found. Your translations are already clean! 🎉" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -21,6 +21,10 @@
|
|||||||
"electron:build": "vue-cli-service electron:build",
|
"electron:build": "vue-cli-service electron:build",
|
||||||
"electron:serve": "vue-cli-service electron:serve",
|
"electron:serve": "vue-cli-service electron:serve",
|
||||||
"i18n": "node ./scripts/gen-i18n-types.js",
|
"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",
|
"link": "node ./scripts/link.js",
|
||||||
"lint": "vue-cli-service lint",
|
"lint": "vue-cli-service lint",
|
||||||
"lint:dpdm": "dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/background.ts",
|
"lint:dpdm": "dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/background.ts",
|
||||||
@@ -60,6 +64,7 @@
|
|||||||
"got": "^12.6.0",
|
"got": "^12.6.0",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"hpagent": "^1.2.0",
|
"hpagent": "^1.2.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"lowdb": "^1.0.0",
|
"lowdb": "^1.0.0",
|
||||||
"marked": "^9.1.5",
|
"marked": "^9.1.5",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
|
|||||||
@@ -245,10 +245,10 @@ UPLOADER_CONFIG_NAME: Configuration Name
|
|||||||
UPLOADER_CONFIG_PLACEHOLDER: Please Enter Configuration Name
|
UPLOADER_CONFIG_PLACEHOLDER: Please Enter Configuration Name
|
||||||
SELECTED_SETTING_HINT: Selected
|
SELECTED_SETTING_HINT: Selected
|
||||||
SETTINGS_MAIN_WINDOW_SIZE: Default Main Window Size
|
SETTINGS_MAIN_WINDOW_SIZE: Default Main Window Size
|
||||||
SETTINGS_MAIN_WINDOW_WIDTH: Default Main Window Width
|
SETTINGS_MAIN_WINDOW_SIZE_WIDTH: Default Main Window Width
|
||||||
SETTINGS_MAIN_WINDOW_WIDTH_HINT: 'Default: 1200'
|
SETTINGS_MAIN_WINDOW_WIDTH_HINT: 'Default: 1200'
|
||||||
SETTINGS_MAIN_WINDOW_WIDTH_RULE: Window Height must be greater than 100
|
SETTINGS_MAIN_WINDOW_WIDTH_RULE: Window Width must be greater than 100
|
||||||
SETTINGS_MAIN_WINDOW_HEIGHT: Default Main Window Height
|
SETTINGS_MAIN_WINDOW_SIZE_HEIGHT: Default Main Window Height
|
||||||
SETTINGS_MAIN_WINDOW_HEIGHT_HINT: 'Default: 800'
|
SETTINGS_MAIN_WINDOW_HEIGHT_HINT: 'Default: 800'
|
||||||
SETTINGS_MAIN_WINDOW_HEIGHT_RULE: Window Height must be greater than 100
|
SETTINGS_MAIN_WINDOW_HEIGHT_RULE: Window Height must be greater than 100
|
||||||
SETTINGS_RAW_PICGO_SIZE: Raw PicGo Size
|
SETTINGS_RAW_PICGO_SIZE: Raw PicGo Size
|
||||||
|
|||||||
499
scripts/find-unused-i18n.mjs
Normal file
499
scripts/find-unused-i18n.mjs
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
#!/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()
|
||||||
Reference in New Issue
Block a user