mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: update code
This commit is contained in:
@@ -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.",
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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。',
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user