feat: update code

This commit is contained in:
pomelo-nwu
2025-11-18 17:48:45 +08:00
parent 32982a16e6
commit 406e6b5e1f
3 changed files with 194 additions and 74 deletions

View File

@@ -428,10 +428,7 @@ export default {
'Approval mode changed to: {{mode}} (saved to {{scope}} settings{{location}})', 'Approval mode changed to: {{mode}} (saved to {{scope}} settings{{location}})',
'Usage: /approval-mode <mode> [--session|--user|--project]': 'Usage: /approval-mode <mode> [--session|--user|--project]':
'Usage: /approval-mode <mode> [--session|--user|--project]', 'Usage: /approval-mode <mode> [--session|--user|--project]',
'Invalid approval mode: {{mode}}': 'Invalid approval mode: {{mode}}',
'Multiple scope flags provided': 'Multiple scope flags provided',
'Invalid arguments provided': 'Invalid arguments provided',
'Missing approval mode': 'Missing approval mode',
'Scope subcommands do not accept additional arguments.': 'Scope subcommands do not accept additional arguments.':
'Scope subcommands do not accept additional arguments.', 'Scope subcommands do not accept additional arguments.',
'Plan mode - Analyze only, do not modify files or execute commands': 'Plan mode - Analyze only, do not modify files or execute commands':
@@ -1076,5 +1073,4 @@ export default {
'Have you tried turning it off and on again? (The loading screen, not me.)': 'Have you tried turning it off and on again? (The loading screen, not me.)':
'Have you tried turning it off and on again? (The loading screen, not me.)', 'Have you tried turning it off and on again? (The loading screen, not me.)',
'Constructing additional pylons...': 'Constructing additional pylons...', 'Constructing additional pylons...': 'Constructing additional pylons...',
"New line? That's Ctrl+J.": "New line? That's Ctrl+J.",
}; };

View File

@@ -411,10 +411,7 @@ export default {
'审批模式已更改为:{{mode}}(已保存到{{scope}}设置{{location}}', '审批模式已更改为:{{mode}}(已保存到{{scope}}设置{{location}}',
'Usage: /approval-mode <mode> [--session|--user|--project]': 'Usage: /approval-mode <mode> [--session|--user|--project]':
'用法:/approval-mode <mode> [--session|--user|--project]', '用法:/approval-mode <mode> [--session|--user|--project]',
'Invalid approval mode: {{mode}}': '无效的审批模式:{{mode}}',
'Multiple scope flags provided': '提供了多个作用域标志',
'Invalid arguments provided': '提供了无效的参数',
'Missing approval mode': '缺少审批模式',
'Scope subcommands do not accept additional arguments.': 'Scope subcommands do not accept additional arguments.':
'作用域子命令不接受额外参数', '作用域子命令不接受额外参数',
'Plan mode - Analyze only, do not modify files or execute commands': 'Plan mode - Analyze only, do not modify files or execute commands':
@@ -1002,5 +999,4 @@ export default {
'Have you tried turning it off and on again? (The loading screen, not me.)': 'Have you tried turning it off and on again? (The loading screen, not me.)':
'你试过把它关掉再打开吗?(加载屏幕,不是我。)', '你试过把它关掉再打开吗?(加载屏幕,不是我。)',
'Constructing additional pylons...': '正在建造额外的能量塔...', 'Constructing additional pylons...': '正在建造额外的能量塔...',
"New line? That's Ctrl+J.": '新行?那是 Ctrl+J。',
}; };

View File

@@ -8,11 +8,13 @@
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { glob } from 'glob'; import { glob } from 'glob';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url); // Get __dirname for ESM modules
const __dirname = path.dirname(__filename); // @ts-expect-error - import.meta is supported in NodeNext module system at runtime
const __dirname = dirname(fileURLToPath(import.meta.url));
interface CheckResult { interface CheckResult {
success: boolean; success: boolean;
@@ -22,6 +24,7 @@ interface CheckResult {
totalKeys: number; totalKeys: number;
translatedKeys: number; translatedKeys: number;
unusedKeys: string[]; unusedKeys: string[];
unusedKeysOnlyInLocales?: string[]; // 新增:只在 locales 中存在的未使用键
}; };
} }
@@ -112,29 +115,34 @@ async function extractUsedKeys(sourceDir: string): Promise<Set<string>> {
for (const file of files) { for (const file of files) {
const filePath = path.join(sourceDir, file); const filePath = path.join(sourceDir, file);
const content = fs.readFileSync(filePath, 'utf-8'); try {
const content = fs.readFileSync(filePath, 'utf-8');
// Find all t( calls // Find all t( calls
const tCallRegex = /t\s*\(/g; const tCallRegex = /t\s*\(/g;
let match; let match;
while ((match = tCallRegex.exec(content)) !== null) { while ((match = tCallRegex.exec(content)) !== null) {
const startPos = match.index + match[0].length; const startPos = match.index + match[0].length;
let pos = startPos; let pos = startPos;
// Skip whitespace // Skip whitespace
while (pos < content.length && /\s/.test(content[pos])) { while (pos < content.length && /\s/.test(content[pos])) {
pos++; pos++;
} }
if (pos >= content.length) continue; if (pos >= content.length) continue;
const char = content[pos]; const char = content[pos];
if (char === "'" || char === '"') { if (char === "'" || char === '"') {
const result = extractStringLiteral(content, pos, char); const result = extractStringLiteral(content, pos, char);
if (result) { if (result) {
usedKeys.add(result.value); usedKeys.add(result.value);
}
} }
} }
} catch {
// Skip files that can't be read
continue;
} }
} }
@@ -190,15 +198,93 @@ function checkKeyMatching(
* Find unused translation keys * Find unused translation keys
*/ */
function findUnusedKeys(allKeys: Set<string>, usedKeys: Set<string>): string[] { function findUnusedKeys(allKeys: Set<string>, usedKeys: Set<string>): string[] {
const unused: string[] = []; return Array.from(allKeys)
.filter((key) => !usedKeys.has(key))
.sort();
}
for (const key of allKeys) { /**
if (!usedKeys.has(key)) { * Save keys that exist only in locale files to a JSON file
unused.push(key); * @param keysOnlyInLocales Array of keys that exist only in locale files
* @param outputPath Path to save the JSON file
*/
function saveKeysOnlyInLocalesToJson(
keysOnlyInLocales: string[],
outputPath: string,
): void {
try {
const data = {
generatedAt: new Date().toISOString(),
keys: keysOnlyInLocales,
count: keysOnlyInLocales.length,
};
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2));
console.log(`Keys that exist only in locale files saved to: ${outputPath}`);
} catch (error) {
console.error(`Failed to save keys to JSON file: ${error}`);
}
}
/**
* Check if unused keys exist only in locale files and nowhere else in the codebase
* Optimized to search all keys in a single pass instead of multiple grep calls
* @param unusedKeys The list of unused keys to check
* @param sourceDir The source directory to search in
* @param localesDir The locales directory to exclude from search
* @returns Array of keys that exist only in locale files
*/
async function findKeysOnlyInLocales(
unusedKeys: string[],
sourceDir: string,
localesDir: string,
): Promise<string[]> {
if (unusedKeys.length === 0) {
return [];
}
const keysOnlyInLocales: string[] = [];
const localesDirName = path.basename(localesDir);
// Find all TypeScript/TSX files (excluding locales, node_modules, dist, and test files)
const files = await glob('**/*.{ts,tsx}', {
cwd: sourceDir,
ignore: [
'**/node_modules/**',
'**/dist/**',
'**/*.test.ts',
'**/*.test.tsx',
`**/${localesDirName}/**`,
],
});
// Read all files and check for key usage
const foundKeys = new Set<string>();
for (const file of files) {
const filePath = path.join(sourceDir, file);
try {
const content = fs.readFileSync(filePath, 'utf-8');
// Check each unused key in the file content
for (const key of unusedKeys) {
if (!foundKeys.has(key) && content.includes(key)) {
foundKeys.add(key);
}
}
} catch {
// Skip files that can't be read
continue;
} }
} }
return unused.sort(); // Keys that were not found in any source files exist only in locales
for (const key of unusedKeys) {
if (!foundKeys.has(key)) {
keysOnlyInLocales.push(key);
}
}
return keysOnlyInLocales;
} }
/** /**
@@ -261,6 +347,12 @@ async function checkI18n(): Promise<CheckResult> {
const enKeys = new Set(Object.keys(enTranslations)); const enKeys = new Set(Object.keys(enTranslations));
const unusedKeys = findUnusedKeys(enKeys, usedKeys); const unusedKeys = findUnusedKeys(enKeys, usedKeys);
// Find keys that exist only in locales (and nowhere else in the codebase)
const unusedKeysOnlyInLocales =
unusedKeys.length > 0
? await findKeysOnlyInLocales(unusedKeys, sourceDir, localesDir)
: [];
if (unusedKeys.length > 0) { if (unusedKeys.length > 0) {
warnings.push(`Found ${unusedKeys.length} unused translation keys`); warnings.push(`Found ${unusedKeys.length} unused translation keys`);
} }
@@ -276,54 +368,90 @@ async function checkI18n(): Promise<CheckResult> {
totalKeys, totalKeys,
translatedKeys, translatedKeys,
unusedKeys, unusedKeys,
unusedKeysOnlyInLocales,
}, },
}; };
} }
// Run checks // Run checks
checkI18n() async function main() {
.then((result) => { const result = await checkI18n();
console.log('\n=== i18n Check Results ===\n');
console.log(`Total keys: ${result.stats.totalKeys}`); console.log('\n=== i18n Check Results ===\n');
console.log(`Translated keys: ${result.stats.translatedKeys}`);
console.log(
`Translation coverage: ${((result.stats.translatedKeys / result.stats.totalKeys) * 100).toFixed(1)}%\n`,
);
if (result.warnings.length > 0) { console.log(`Total keys: ${result.stats.totalKeys}`);
console.log('⚠️ Warnings:'); console.log(`Translated keys: ${result.stats.translatedKeys}`);
result.warnings.forEach((warning) => console.log(` - ${warning}`)); const coverage =
if ( result.stats.totalKeys > 0
result.stats.unusedKeys.length > 0 && ? ((result.stats.translatedKeys / result.stats.totalKeys) * 100).toFixed(
result.stats.unusedKeys.length <= 10 1,
) { )
console.log('\nUnused keys:'); : '0.0';
result.stats.unusedKeys.forEach((key) => console.log(` - "${key}"`)); console.log(`Translation coverage: ${coverage}%\n`);
} else if (result.stats.unusedKeys.length > 10) {
console.log( if (result.warnings.length > 0) {
`\nUnused keys (showing first 10 of ${result.stats.unusedKeys.length}):`, console.log('⚠️ Warnings:');
); result.warnings.forEach((warning) => console.log(` - ${warning}`));
result.stats.unusedKeys
.slice(0, 10) // Show unused keys
.forEach((key) => console.log(` - "${key}"`)); if (
} result.stats.unusedKeys.length > 0 &&
console.log(); result.stats.unusedKeys.length <= 10
) {
console.log('\nUnused keys:');
result.stats.unusedKeys.forEach((key) => console.log(` - "${key}"`));
} else if (result.stats.unusedKeys.length > 10) {
console.log(
`\nUnused keys (showing first 10 of ${result.stats.unusedKeys.length}):`,
);
result.stats.unusedKeys
.slice(0, 10)
.forEach((key) => console.log(` - "${key}"`));
} }
if (result.errors.length > 0) { // Show keys that exist only in locales files
console.log('❌ Errors:'); if (
result.errors.forEach((error) => console.log(` - ${error}`)); result.stats.unusedKeysOnlyInLocales &&
console.log(); result.stats.unusedKeysOnlyInLocales.length > 0
process.exit(1); ) {
console.log(
'\n⚠ The following keys exist ONLY in locale files and nowhere else in the codebase:',
);
console.log(
' Please review these keys - they might be safe to remove.',
);
result.stats.unusedKeysOnlyInLocales.forEach((key) =>
console.log(` - "${key}"`),
);
// Save these keys to a JSON file
const outputPath = path.join(
__dirname,
'unused-keys-only-in-locales.json',
);
saveKeysOnlyInLocalesToJson(
result.stats.unusedKeysOnlyInLocales,
outputPath,
);
} }
if (result.success) { console.log();
console.log('✅ All checks passed!\n'); }
process.exit(0);
} if (result.errors.length > 0) {
}) console.log('❌ Errors:');
.catch((error) => { result.errors.forEach((error) => console.log(` - ${error}`));
console.error('❌ Fatal error:', error); console.log();
process.exit(1); process.exit(1);
}); }
if (result.success) {
console.log('✅ All checks passed!\n');
process.exit(0);
}
}
main().catch((error) => {
console.error('❌ Fatal error:', error);
process.exit(1);
});