From 286add7fff20edf9da4d5d02ff3dafc2b75acbfd Mon Sep 17 00:00:00 2001 From: goddonebianu Date: Wed, 18 Mar 2026 07:59:21 +0800 Subject: [PATCH] fix: rewrite translation check scripts with proper regex - Fix regex to handle 'await getTranslations' pattern - Add word boundary to prevent false matches like 'get("q")' - Improve namespace detection for dotted namespaces - Reduce false positives in both scripts --- scripts/find-missing-translations.ts | 362 ++++++++++++-------------- scripts/find-unused-translations.ts | 367 ++++++++++----------------- 2 files changed, 286 insertions(+), 443 deletions(-) diff --git a/scripts/find-missing-translations.ts b/scripts/find-missing-translations.ts index 583f666..04cdf6e 100644 --- a/scripts/find-missing-translations.ts +++ b/scripts/find-missing-translations.ts @@ -1,12 +1,6 @@ /** * 查找缺失的翻译键 - * - * 扫描代码中使用的翻译键,与翻译文件对比,找出缺失的键 - * * 用法: npx tsx scripts/find-missing-translations.ts [locale] - * 示例: - * npx tsx scripts/find-missing-translations.ts # 检查所有语言 - * npx tsx scripts/find-missing-translations.ts en-US # 只检查 en-US */ import * as fs from "fs"; @@ -14,38 +8,92 @@ import * as path from "path"; const SRC_DIR = "./src"; const MESSAGES_DIR = "./messages"; +const ALL_LOCALES = ["en-US", "zh-CN", "ja-JP", "ko-KR", "de-DE", "fr-FR", "it-IT", "ug-CN"]; -// 所有支持的语言 -const ALL_LOCALES = [ - "en-US", - "zh-CN", - "ja-JP", - "ko-KR", - "de-DE", - "fr-FR", - "it-IT", - "ug-CN", -]; - -interface FileTranslationUsage { +interface TranslationUsage { file: string; + line: number; namespace: string; - keys: Set; + key: string; + isDynamic: boolean; +} + +function parseStringLiteral(s: string): string | null { + s = s.trim(); + if (s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1); + if (s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1); + if (s.startsWith("`") && s.endsWith("`")) { + return s.includes("${") || s.includes("+") ? null : s.slice(1, -1); + } + return null; +} + +function extractTranslationBindings(content: string): Map { + const bindings = new Map(); + + const patterns = [ + /(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?(?:useTranslations|getTranslations)\s*\(\s*([^)]*)\s*\)/g, + /(?:const|let|var)\s*\{\s*(\w+)\s*\}\s*=\s*await\s+getTranslations\s*\(\s*([^)]*)\s*\)/g, + ]; + + for (const pattern of patterns) { + let match; + while ((match = pattern.exec(content)) !== null) { + const varName = match[1]; + const arg = match[2].trim(); + if (arg === "") { + bindings.set(varName, "__ROOT__"); + } else { + const ns = parseStringLiteral(arg); + if (ns !== null) bindings.set(varName, ns); + } + } + } + + return bindings; +} + +function extractTranslationCalls( + content: string, + filePath: string, + bindings: Map +): TranslationUsage[] { + const usages: TranslationUsage[] = []; + const lines = content.split("\n"); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const lineNum = i + 1; + + for (const [varName, namespace] of bindings) { + const callPattern = new RegExp( + `\\b${varName}\\s*\\(\\s*("[^"]*"|'[^']*'|\`[^\`]*\`)(?:\\s*,|\\s*\\))`, + "g" + ); + + let match; + while ((match = callPattern.exec(line)) !== null) { + const arg = match[1]; + const key = parseStringLiteral(arg); + + if (key !== null) { + usages.push({ file: filePath, line: lineNum, namespace, key, isDynamic: false }); + } else { + usages.push({ file: filePath, line: lineNum, namespace, key: arg, isDynamic: true }); + } + } + } + } + + return usages; } -/** - * 递归获取目录下所有文件 - */ function getAllFiles(dir: string, extensions: string[]): string[] { const files: string[] = []; - if (!fs.existsSync(dir)) return files; - const entries = fs.readdirSync(dir, { withFileTypes: true }); - - for (const entry of entries) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { files.push(...getAllFiles(fullPath, extensions)); } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) { @@ -56,236 +104,142 @@ function getAllFiles(dir: string, extensions: string[]): string[] { return files; } -/** - * 从代码文件中提取翻译使用情况 - */ -function extractTranslationsFromFile(filePath: string): FileTranslationUsage[] { - const content = fs.readFileSync(filePath, "utf-8"); - const usages: FileTranslationUsage[] = []; +function navigateToObject(obj: Record, path: string): unknown { + if (typeof obj[path] !== "undefined") return obj[path]; - // 匹配 useTranslations("namespace") 或 getTranslations("namespace") - const namespacePattern = /(?:useTranslations|getTranslations)\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g; - - // 匹配没有参数的 useTranslations() 或 getTranslations() - 使用根级别 - const rootNamespacePattern = /(?:useTranslations|getTranslations)\s*\(\s*\)/g; - - // 首先找到所有的 namespace 声明 - const namespaces: { name: string; isRoot: boolean }[] = []; - let match; - - while ((match = namespacePattern.exec(content)) !== null) { - namespaces.push({ name: match[1], isRoot: false }); + let current: unknown = obj; + for (const part of path.split(".")) { + if (typeof current !== "object" || current === null) return undefined; + current = (current as Record)[part]; } - - // 检查是否有根级别的翻译 - let hasRootTranslations = false; - while ((match = rootNamespacePattern.exec(content)) !== null) { - hasRootTranslations = true; - } - - if (namespaces.length === 0 && !hasRootTranslations) { - return usages; - } - - // 匹配 t("key") 或 t('key') 或 t(`key`) 调用 - // 支持简单键、嵌套键、带插值的键 - const tCallPattern = /\bt\s*\(\s*["'`]([^"'`]+)["'`]\s*(?:,|\))/g; - - const allKeys = new Set(); - - while ((match = tCallPattern.exec(content)) !== null) { - allKeys.add(match[1]); - } - - // 匹配模板字面量动态键 t(`prefix.${variable}`) - const dynamicTCallPattern = /\bt\s*\(\s*`([^`]+)\$\{[^}]+\}([^`]*)`\s*(?:,|\))/g; - while ((match = dynamicTCallPattern.exec(content)) !== null) { - // 标记为动态键,前缀部分 - const prefix = match[1]; - if (prefix) { - allKeys.add(`[DYNAMIC PREFIX: ${prefix}]`); - } - } - - if (allKeys.size === 0) { - return usages; - } - - // 为每个 namespace 创建使用记录 - for (const ns of namespaces) { - usages.push({ - file: filePath, - namespace: ns.name, - keys: allKeys, - }); - } - - // 如果有根级别翻译,添加一个特殊的 namespace - if (hasRootTranslations) { - usages.push({ - file: filePath, - namespace: "__ROOT__", - keys: allKeys, - }); - } - - return usages; + return current; } -/** - * 获取翻译对象中所有的键路径 - */ -function getAllKeysFromTranslation(obj: Record, prefix = ""): Set { - const keys = new Set(); +function keyExists(key: string, namespace: string, translations: Record): boolean { + let targetObj: unknown; - for (const key of Object.keys(obj)) { - const fullKey = prefix ? `${prefix}.${key}` : key; - keys.add(fullKey); - - if (typeof obj[key] === "object" && obj[key] !== null) { - const nestedKeys = getAllKeysFromTranslation( - obj[key] as Record, - fullKey - ); - nestedKeys.forEach(k => keys.add(k)); - } - } - - return keys; -} - -/** - * 检查键是否存在于翻译对象中 - */ -function keyExistsInTranslation(key: string, namespace: string, translations: Record): boolean { - // 处理根级别翻译 if (namespace === "__ROOT__") { - const parts = key.split("."); - let current: unknown = translations; - - for (const part of parts) { - if (typeof current !== "object" || current === null) { - return false; - } - current = (current as Record)[part]; - } - - return typeof current === "string"; + targetObj = translations; + } else { + targetObj = navigateToObject(translations, namespace); } - // 处理带命名空间的翻译 - const nsTranslations = translations[namespace]; - if (!nsTranslations || typeof nsTranslations !== "object") { - return false; - } + if (typeof targetObj !== "object" || targetObj === null) return false; - const parts = key.split("."); - let current: unknown = nsTranslations; + const target = targetObj as Record; - for (const part of parts) { - if (typeof current !== "object" || current === null) { - return false; - } + if (typeof target[key] === "string") return true; + + let current: unknown = target; + for (const part of key.split(".")) { + if (typeof current !== "object" || current === null) return false; current = (current as Record)[part]; } return typeof current === "string"; } -/** - * 主函数 - */ function main() { const targetLocale = process.argv[2]; - const localesToCheck = targetLocale - ? [targetLocale] - : ALL_LOCALES; + const localesToCheck = targetLocale ? [targetLocale] : ALL_LOCALES; - console.log("🔍 扫描代码中的翻译使用情况...\n"); + console.log("Scanning source files...\n"); - // 获取所有源代码文件 - const sourceFiles = [ - ...getAllFiles(SRC_DIR, [".tsx", ".ts"]), - ]; + const sourceFiles = getAllFiles(SRC_DIR, [".tsx", ".ts"]); + const allUsages: TranslationUsage[] = []; - // 提取所有翻译使用 - const allUsages: FileTranslationUsage[] = []; - for (const file of sourceFiles) { - allUsages.push(...extractTranslationsFromFile(file)); + for (const filePath of sourceFiles) { + const content = fs.readFileSync(filePath, "utf-8"); + const bindings = extractTranslationBindings(content); + const usages = extractTranslationCalls(content, filePath, bindings); + allUsages.push(...usages); } - console.log(`📁 扫描了 ${sourceFiles.length} 个文件`); - console.log(`📝 发现 ${allUsages.length} 个翻译使用声明\n`); + const uniqueUsages = new Map(); + for (const usage of allUsages) { + const key = `${usage.file}:${usage.line}:${usage.namespace}:${usage.key}`; + uniqueUsages.set(key, usage); + } + + const dedupedUsages = Array.from(uniqueUsages.values()); + + console.log(`Found ${sourceFiles.length} files, ${dedupedUsages.length} translation usages\n`); - // 检查每种语言 for (const locale of localesToCheck) { console.log(`\n${"=".repeat(60)}`); - console.log(`🌐 检查语言: ${locale}`); + console.log(`Locale: ${locale}`); console.log("=".repeat(60)); const translationPath = path.join(MESSAGES_DIR, `${locale}.json`); if (!fs.existsSync(translationPath)) { - console.log(`❌ 翻译文件不存在: ${translationPath}`); + console.log(`File not found: ${translationPath}`); continue; } let translations: Record; try { - const content = fs.readFileSync(translationPath, "utf-8"); - translations = JSON.parse(content); + translations = JSON.parse(fs.readFileSync(translationPath, "utf-8")); } catch (e) { - console.log(`❌ 翻译文件解析失败: ${translationPath}`); + console.log(`Failed to parse: ${translationPath}`); console.log(e); continue; } - const missingKeys: { file: string; namespace: string; key: string }[] = []; + const missing: TranslationUsage[] = []; + const dynamic: TranslationUsage[] = []; - for (const usage of allUsages) { - for (const key of usage.keys) { - // 跳过动态键标记 - if (key.startsWith("[DYNAMIC PREFIX:")) { - continue; - } - - if (!keyExistsInTranslation(key, usage.namespace, translations)) { - missingKeys.push({ - file: usage.file, - namespace: usage.namespace, - key: key, - }); - } + for (const usage of dedupedUsages) { + if (usage.isDynamic) { + dynamic.push(usage); + } else if (!keyExists(usage.key, usage.namespace, translations)) { + missing.push(usage); } } - if (missingKeys.length === 0) { - console.log(`✅ 没有缺失的翻译键`); + if (missing.length === 0 && dynamic.length === 0) { + console.log("All translations exist!"); } else { - console.log(`\n❌ 发现 ${missingKeys.length} 个缺失的翻译键:\n`); - - // 按文件分组显示 - const groupedByFile = new Map(); - for (const item of missingKeys) { - const file = item.file; - if (!groupedByFile.has(file)) { - groupedByFile.set(file, []); + if (missing.length > 0) { + console.log(`\nMissing ${missing.length} translations:\n`); + + const byFile = new Map(); + for (const usage of missing) { + if (!byFile.has(usage.file)) byFile.set(usage.file, []); + byFile.get(usage.file)!.push(usage); + } + + for (const [file, usages] of byFile) { + console.log(file); + for (const usage of usages) { + const ns = usage.namespace === "__ROOT__" ? "(root)" : usage.namespace; + console.log(` L${usage.line} [${ns}] ${usage.key}`); + } + console.log(); } - groupedByFile.get(file)!.push(item); } - for (const [file, items] of groupedByFile) { - console.log(`📄 ${file}`); - for (const item of items) { - const nsDisplay = item.namespace === "__ROOT__" ? "(root)" : item.namespace; - console.log(` [${nsDisplay}] ${item.key}`); + if (dynamic.length > 0) { + console.log(`\n${dynamic.length} dynamic keys (manual review):\n`); + + const byFile = new Map(); + for (const usage of dynamic) { + if (!byFile.has(usage.file)) byFile.set(usage.file, []); + byFile.get(usage.file)!.push(usage); + } + + for (const [file, usages] of byFile) { + console.log(file); + for (const usage of usages) { + const ns = usage.namespace === "__ROOT__" ? "(root)" : usage.namespace; + console.log(` L${usage.line} [${ns}] ${usage.key}`); + } } - console.log(); } } } - console.log("\n✨ 检查完成!"); + console.log("\nDone!"); } main(); diff --git a/scripts/find-unused-translations.ts b/scripts/find-unused-translations.ts index ed6dbeb..93fa270 100644 --- a/scripts/find-unused-translations.ts +++ b/scripts/find-unused-translations.ts @@ -1,12 +1,6 @@ /** - * 查找多余(未使用)的翻译键 - * - * 扫描翻译文件中的所有键,与代码中实际使用的键对比,找出多余的键 - * + * 查找多余的翻译键 * 用法: npx tsx scripts/find-unused-translations.ts [locale] - * 示例: - * npx tsx scripts/find-unused-translations.ts # 检查所有语言 - * npx tsx scripts/find-unused-translations.ts en-US # 只检查 en-US */ import * as fs from "fs"; @@ -14,32 +8,75 @@ import * as path from "path"; const SRC_DIR = "./src"; const MESSAGES_DIR = "./messages"; +const ALL_LOCALES = ["en-US", "zh-CN", "ja-JP", "ko-KR", "de-DE", "fr-FR", "it-IT", "ug-CN"]; -// 所有支持的语言 -const ALL_LOCALES = [ - "en-US", - "zh-CN", - "ja-JP", - "ko-KR", - "de-DE", - "fr-FR", - "it-IT", - "ug-CN", -]; +function parseStringLiteral(s: string): string | null { + s = s.trim(); + if (s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1); + if (s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1); + if (s.startsWith("`") && s.endsWith("`")) { + return s.includes("${") || s.includes("+") ? null : s.slice(1, -1); + } + return null; +} + +function extractTranslationBindings(content: string): Map { + const bindings = new Map(); + + const patterns = [ + /(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?(?:useTranslations|getTranslations)\s*\(\s*([^)]*)\s*\)/g, + /(?:const|let|var)\s*\{\s*(\w+)\s*\}\s*=\s*await\s+getTranslations\s*\(\s*([^)]*)\s*\)/g, + ]; + + for (const pattern of patterns) { + let match; + while ((match = pattern.exec(content)) !== null) { + const varName = match[1]; + const arg = match[2].trim(); + if (arg === "") { + bindings.set(varName, "__ROOT__"); + } else { + const ns = parseStringLiteral(arg); + if (ns !== null) bindings.set(varName, ns); + } + } + } + + return bindings; +} + +function extractUsedKeys(content: string, bindings: Map): Map> { + const usedKeys = new Map>(); + + for (const [varName, namespace] of bindings) { + const callPattern = new RegExp( + `\\b${varName}\\s*\\(\\s*("[^"]*"|'[^']*'|\`[^\`]*\`)(?:\\s*,|\\s*\\))`, + "g" + ); + + let match; + while ((match = callPattern.exec(content)) !== null) { + const arg = match[1]; + const key = parseStringLiteral(arg); + + if (key !== null) { + if (!usedKeys.has(namespace)) { + usedKeys.set(namespace, new Set()); + } + usedKeys.get(namespace)!.add(key); + } + } + } + + return usedKeys; +} -/** - * 递归获取目录下所有文件 - */ function getAllFiles(dir: string, extensions: string[]): string[] { const files: string[] = []; - if (!fs.existsSync(dir)) return files; - const entries = fs.readdirSync(dir, { withFileTypes: true }); - - for (const entry of entries) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { files.push(...getAllFiles(fullPath, extensions)); } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) { @@ -50,142 +87,19 @@ function getAllFiles(dir: string, extensions: string[]): string[] { return files; } -/** - * 代码中实际使用的翻译键 - */ -interface UsedTranslations { - // namespace -> Set of keys - byNamespace: Map>; - // 根级别的完整路径 (当使用 useTranslations() 无参数时) - rootKeys: Set; -} - -/** - * 从代码文件中提取使用的翻译键 - */ -function extractUsedTranslationsFromFiles(files: string[]): UsedTranslations { - const result: UsedTranslations = { - byNamespace: new Map(), - rootKeys: new Set(), - }; - - for (const filePath of files) { - const content = fs.readFileSync(filePath, "utf-8"); - - // 匹配 useTranslations("namespace") 或 getTranslations("namespace") - const namespacePattern = /(?:useTranslations|getTranslations)\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g; - - // 检查是否有根级别的翻译 (无参数调用) - const hasRootTranslations = /(?:useTranslations|getTranslations)\s*\(\s*\)/.test(content); - - // 收集该文件中的所有 namespace - const namespaces: string[] = []; - let match; - - while ((match = namespacePattern.exec(content)) !== null) { - namespaces.push(match[1]); - } - - // 匹配 t("key") 或 t('key') 或 t(`key`) 调用 - const tCallPattern = /\bt\s*\(\s*["'`]([^"'`]+)["'`]\s*(?:,|\))/g; - - const keysInFile = new Set(); - - while ((match = tCallPattern.exec(content)) !== null) { - keysInFile.add(match[1]); - } - - // 匹配带命名空间的 t 调用 t("namespace.key") - 跨 namespace 访问 - const crossNamespacePattern = /\bt\s*\(\s*["'`]([^.]+\.[^"'`]+)["'`]\s*(?:,|\))/g; - - // 将键添加到对应的 namespace - for (const ns of namespaces) { - if (!result.byNamespace.has(ns)) { - result.byNamespace.set(ns, new Set()); - } - - for (const key of keysInFile) { - // 检查是否是跨 namespace 访问 (包含点) - const dotIndex = key.indexOf("."); - if (dotIndex > 0) { - const possibleNs = key.substring(0, dotIndex); - const remainingKey = key.substring(dotIndex + 1); - - // 如果第一部分是已知的 namespace,则是跨 namespace 访问 - if (result.byNamespace.has(possibleNs) || possibleNs !== ns) { - // 添加到对应的 namespace - if (!result.byNamespace.has(possibleNs)) { - result.byNamespace.set(possibleNs, new Set()); - } - result.byNamespace.get(possibleNs)!.add(remainingKey); - continue; - } - } - - result.byNamespace.get(ns)!.add(key); - } - } - - // 如果有根级别翻译,添加到 rootKeys - if (hasRootTranslations) { - for (const key of keysInFile) { - result.rootKeys.add(key); - } - } - } - - return result; -} - -/** - * 获取翻译对象中所有的键路径(带完整路径) - */ -function getAllTranslationKeys( +function flattenTranslations( obj: Record, - locale: string -): { namespace: string; key: string; fullPath: string }[] { - const result: { namespace: string; key: string; fullPath: string }[] = []; - - // 获取顶层 namespace - for (const ns of Object.keys(obj)) { - const nsValue = obj[ns]; - - if (typeof nsValue === "object" && nsValue !== null) { - // 递归获取该 namespace 下的所有键 - const keys = getNestedKeys(nsValue as Record); - - for (const key of keys) { - result.push({ - namespace: ns, - key: key, - fullPath: `${ns}.${key}`, - }); - } - } else if (typeof nsValue === "string") { - // 顶层直接是字符串值 - result.push({ - namespace: ns, - key: "", - fullPath: ns, - }); - } - } - - return result; -} - -/** - * 递归获取嵌套对象的键路径 - */ -function getNestedKeys(obj: Record, prefix = ""): string[] { + prefix = "" +): string[] { const keys: string[] = []; for (const key of Object.keys(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; + const value = obj[key]; - if (typeof obj[key] === "object" && obj[key] !== null) { - keys.push(...getNestedKeys(obj[key] as Record, fullKey)); - } else if (typeof obj[key] === "string") { + if (typeof value === "object" && value !== null) { + keys.push(...flattenTranslations(value as Record, fullKey)); + } else if (typeof value === "string") { keys.push(fullKey); } } @@ -193,139 +107,114 @@ function getNestedKeys(obj: Record, prefix = ""): string[] { return keys; } -/** - * 检查键是否被代码使用 - */ function isKeyUsed( - namespace: string, - key: string, - used: UsedTranslations + fullKey: string, + usedKeys: Map> ): boolean { - // 检查该 namespace 下是否使用了该键 - const nsKeys = used.byNamespace.get(namespace); - if (nsKeys) { - // 检查精确匹配 - if (nsKeys.has(key)) { - return true; - } + const parts = fullKey.split("."); + + for (let i = 1; i < parts.length; i++) { + const namespace = parts.slice(0, i).join("."); + const keyInNamespace = parts.slice(i).join("."); - // 检查前缀匹配 (父级键被使用时,子键也算被使用) - for (const usedKey of nsKeys) { - if (key.startsWith(usedKey + ".") || usedKey.startsWith(key + ".")) { - return true; + const nsKeys = usedKeys.get(namespace); + if (nsKeys) { + if (nsKeys.has(keyInNamespace)) return true; + + for (const usedKey of nsKeys) { + if (keyInNamespace.startsWith(usedKey + ".")) return true; } } } - // 检查根级别使用 - for (const rootKey of used.rootKeys) { - if (rootKey === `${namespace}.${key}` || rootKey.startsWith(`${namespace}.${key}.`)) { - return true; - } - } - - // 检查跨 namespace 使用 (t("namespace.key") 形式) - for (const [ns, keys] of used.byNamespace) { - for (const k of keys) { - // 检查是否是跨 namespace 访问 - if (k.includes(".")) { - const fullKey = `${ns}.${k}`; - if (fullKey === `${namespace}.${key}`) { - return true; - } - } - } - } + const rootKeys = usedKeys.get("__ROOT__"); + if (rootKeys && rootKeys.has(fullKey)) return true; return false; } -/** - * 主函数 - */ function main() { const targetLocale = process.argv[2]; - const localesToCheck = targetLocale - ? [targetLocale] - : ALL_LOCALES; + const localesToCheck = targetLocale ? [targetLocale] : ALL_LOCALES; - console.log("🔍 扫描代码中使用的翻译键...\n"); + console.log("Scanning source files...\n"); - // 获取所有源代码文件 const sourceFiles = getAllFiles(SRC_DIR, [".tsx", ".ts"]); + const allUsedKeys = new Map>(); - console.log(`📁 扫描了 ${sourceFiles.length} 个文件`); - - // 提取代码中使用的翻译 - const usedTranslations = extractUsedTranslationsFromFiles(sourceFiles); - - console.log(`📝 发现 ${usedTranslations.byNamespace.size} 个命名空间被使用`); - let totalKeys = 0; - for (const keys of usedTranslations.byNamespace.values()) { - totalKeys += keys.size; + for (const filePath of sourceFiles) { + const content = fs.readFileSync(filePath, "utf-8"); + const bindings = extractTranslationBindings(content); + const usedKeys = extractUsedKeys(content, bindings); + + for (const [ns, keys] of usedKeys) { + if (!allUsedKeys.has(ns)) { + allUsedKeys.set(ns, new Set()); + } + for (const key of keys) { + allUsedKeys.get(ns)!.add(key); + } + } } - console.log(`📝 发现 ${totalKeys} 个翻译键被使用\n`); - // 检查每种语言 + console.log(`Scanned ${sourceFiles.length} files`); + console.log(`Found ${allUsedKeys.size} namespaces used\n`); + for (const locale of localesToCheck) { console.log(`\n${"=".repeat(60)}`); - console.log(`🌐 检查语言: ${locale}`); + console.log(`Locale: ${locale}`); console.log("=".repeat(60)); const translationPath = path.join(MESSAGES_DIR, `${locale}.json`); if (!fs.existsSync(translationPath)) { - console.log(`❌ 翻译文件不存在: ${translationPath}`); + console.log(`File not found: ${translationPath}`); continue; } let translations: Record; try { - const content = fs.readFileSync(translationPath, "utf-8"); - translations = JSON.parse(content); + translations = JSON.parse(fs.readFileSync(translationPath, "utf-8")); } catch (e) { - console.log(`❌ 翻译文件解析失败: ${translationPath}`); - console.log(e); + console.log(`Failed to parse: ${translationPath}`); continue; } - // 获取翻译文件中的所有键 - const allTranslationKeys = getAllTranslationKeys(translations, locale); + const allKeys = flattenTranslations(translations); + console.log(`Total ${allKeys.length} translation keys`); - console.log(`📊 翻译文件中共有 ${allTranslationKeys.length} 个翻译键`); - - // 找出未使用的键 - const unusedKeys = allTranslationKeys.filter( - item => !isKeyUsed(item.namespace, item.key, usedTranslations) - ); + const unusedKeys = allKeys.filter(key => !isKeyUsed(key, allUsedKeys)); if (unusedKeys.length === 0) { - console.log(`✅ 没有多余的翻译键`); + console.log("No unused translations!"); } else { - console.log(`\n⚠️ 发现 ${unusedKeys.length} 个可能多余的翻译键:\n`); + console.log(`\n${unusedKeys.length} potentially unused translations:\n`); - // 按 namespace 分组显示 - const groupedByNs = new Map(); - for (const item of unusedKeys) { - if (!groupedByNs.has(item.namespace)) { - groupedByNs.set(item.namespace, []); + const groupedByNs = new Map(); + for (const key of unusedKeys) { + const firstDot = key.indexOf("."); + const ns = firstDot > 0 ? key.substring(0, firstDot) : key; + const subKey = firstDot > 0 ? key.substring(firstDot + 1) : ""; + + if (!groupedByNs.has(ns)) { + groupedByNs.set(ns, []); } - groupedByNs.get(item.namespace)!.push(item); + groupedByNs.get(ns)!.push(subKey || "(root)"); } - for (const [ns, items] of groupedByNs) { - console.log(`📦 ${ns}`); - for (const item of items) { - console.log(` ${item.key || "(root value)"}`); + for (const [ns, keys] of groupedByNs) { + console.log(`${ns}`); + for (const key of keys) { + console.log(` ${key}`); } console.log(); } - console.log("💡 提示: 这些键可能被动态使用(如 t(`prefix.${var}`)),请人工确认后再删除。"); + console.log("Note: These may be used dynamically (e.g., t(`prefix.${var}`)). Review before deleting."); } } - console.log("\n✨ 检查完成!"); + console.log("\nDone!"); } main();