|
| 1 | +const fs = require("fs") |
| 2 | +const path = require("path") |
| 3 | + |
| 4 | +// Parse command-line arguments |
| 5 | +const args = process.argv.slice(2).reduce((acc, arg) => { |
| 6 | + if (arg === "--help") { |
| 7 | + acc.help = true |
| 8 | + } else if (arg.startsWith("--locale=")) { |
| 9 | + acc.locale = arg.split("=")[1] |
| 10 | + } else if (arg.startsWith("--file=")) { |
| 11 | + acc.file = arg.split("=")[1] |
| 12 | + } |
| 13 | + return acc |
| 14 | +}, {}) |
| 15 | + |
| 16 | +// Display help information |
| 17 | +if (args.help) { |
| 18 | + console.log(` |
| 19 | +Find missing i18n translations |
| 20 | +
|
| 21 | +A useful script to identify whether the i18n keys used in component files exist in all language files. |
| 22 | +
|
| 23 | +Usage: |
| 24 | + node scripts/find-missing-i18n-key.js [options] |
| 25 | +
|
| 26 | +Options: |
| 27 | + --locale=<locale> Only check a specific language (e.g., --locale=de) |
| 28 | + --file=<file> Only check a specific file (e.g., --file=chat.json) |
| 29 | + --help Display help information |
| 30 | +
|
| 31 | +Output: |
| 32 | + - Generate a report of missing translations |
| 33 | + `) |
| 34 | + process.exit(0) |
| 35 | +} |
| 36 | + |
| 37 | +// Directory to traverse |
| 38 | +const TARGET_DIR = path.join(__dirname, "../webview-ui/src/components") |
| 39 | +const LOCALES_DIR = path.join(__dirname, "../webview-ui/src/i18n/locales") |
| 40 | + |
| 41 | +// Regular expressions to match i18n keys |
| 42 | +const i18nPatterns = [ |
| 43 | + /{t\("([^"]+)"\)}/g, // Match {t("key")} format |
| 44 | + /i18nKey="([^"]+)"/g, // Match i18nKey="key" format |
| 45 | + /t\("([a-zA-Z][a-zA-Z0-9_]*[:.][a-zA-Z0-9_.]+)"\)/g, // Match t("key") format, where key contains a colon or dot |
| 46 | +] |
| 47 | + |
| 48 | +// Get all language directories |
| 49 | +function getLocaleDirs() { |
| 50 | + const allLocales = fs.readdirSync(LOCALES_DIR).filter((file) => { |
| 51 | + const stats = fs.statSync(path.join(LOCALES_DIR, file)) |
| 52 | + return stats.isDirectory() // Do not exclude any language directories |
| 53 | + }) |
| 54 | + |
| 55 | + // Filter to a specific language if specified |
| 56 | + return args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales |
| 57 | +} |
| 58 | + |
| 59 | +// Get the value from JSON by path |
| 60 | +function getValueByPath(obj, path) { |
| 61 | + const parts = path.split(".") |
| 62 | + let current = obj |
| 63 | + |
| 64 | + for (const part of parts) { |
| 65 | + if (current === undefined || current === null) { |
| 66 | + return undefined |
| 67 | + } |
| 68 | + current = current[part] |
| 69 | + } |
| 70 | + |
| 71 | + return current |
| 72 | +} |
| 73 | + |
| 74 | +// Check if the key exists in all language files, return a list of missing language files |
| 75 | +function checkKeyInLocales(key, localeDirs) { |
| 76 | + const [file, ...pathParts] = key.split(":") |
| 77 | + const jsonPath = pathParts.join(".") |
| 78 | + |
| 79 | + const missingLocales = [] |
| 80 | + |
| 81 | + localeDirs.forEach((locale) => { |
| 82 | + const filePath = path.join(LOCALES_DIR, locale, `${file}.json`) |
| 83 | + if (!fs.existsSync(filePath)) { |
| 84 | + missingLocales.push(`${locale}/${file}.json`) |
| 85 | + return |
| 86 | + } |
| 87 | + |
| 88 | + const json = JSON.parse(fs.readFileSync(filePath, "utf8")) |
| 89 | + if (getValueByPath(json, jsonPath) === undefined) { |
| 90 | + missingLocales.push(`${locale}/${file}.json`) |
| 91 | + } |
| 92 | + }) |
| 93 | + |
| 94 | + return missingLocales |
| 95 | +} |
| 96 | + |
| 97 | +// Recursively traverse the directory |
| 98 | +function findMissingI18nKeys() { |
| 99 | + const localeDirs = getLocaleDirs() |
| 100 | + const results = [] |
| 101 | + |
| 102 | + function walk(dir) { |
| 103 | + const files = fs.readdirSync(dir) |
| 104 | + |
| 105 | + for (const file of files) { |
| 106 | + const filePath = path.join(dir, file) |
| 107 | + const stat = fs.statSync(filePath) |
| 108 | + |
| 109 | + // Exclude test files |
| 110 | + if (filePath.includes(".test.")) continue |
| 111 | + |
| 112 | + if (stat.isDirectory()) { |
| 113 | + walk(filePath) // Recursively traverse subdirectories |
| 114 | + } else if (stat.isFile() && [".ts", ".tsx", ".js", ".jsx"].includes(path.extname(filePath))) { |
| 115 | + const content = fs.readFileSync(filePath, "utf8") |
| 116 | + |
| 117 | + // Match all i18n keys |
| 118 | + for (const pattern of i18nPatterns) { |
| 119 | + let match |
| 120 | + while ((match = pattern.exec(content)) !== null) { |
| 121 | + const key = match[1] |
| 122 | + const missingLocales = checkKeyInLocales(key, localeDirs) |
| 123 | + if (missingLocales.length > 0) { |
| 124 | + results.push({ |
| 125 | + key, |
| 126 | + missingLocales, |
| 127 | + file: path.relative(TARGET_DIR, filePath), |
| 128 | + }) |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + walk(TARGET_DIR) |
| 137 | + return results |
| 138 | +} |
| 139 | + |
| 140 | +// Execute and output the results |
| 141 | +function main() { |
| 142 | + try { |
| 143 | + const localeDirs = getLocaleDirs() |
| 144 | + if (args.locale && localeDirs.length === 0) { |
| 145 | + console.error(`Error: Language '${args.locale}' not found in ${LOCALES_DIR}`) |
| 146 | + process.exit(1) |
| 147 | + } |
| 148 | + |
| 149 | + console.log(`Checking ${localeDirs.length} non-English languages: ${localeDirs.join(", ")}`) |
| 150 | + |
| 151 | + const missingKeys = findMissingI18nKeys() |
| 152 | + |
| 153 | + if (missingKeys.length === 0) { |
| 154 | + console.log("\n✅ All i18n keys are present!") |
| 155 | + return |
| 156 | + } |
| 157 | + |
| 158 | + console.log("\nMissing i18n keys:\n") |
| 159 | + missingKeys.forEach(({ key, missingLocales, file }) => { |
| 160 | + console.log(`File: ${file}`) |
| 161 | + console.log(`Key: ${key}`) |
| 162 | + console.log("Missing in:") |
| 163 | + missingLocales.forEach((file) => console.log(` - ${file}`)) |
| 164 | + console.log("-------------------") |
| 165 | + }) |
| 166 | + |
| 167 | + // Exit code 1 indicates missing keys |
| 168 | + process.exit(1) |
| 169 | + } catch (error) { |
| 170 | + console.error("Error:", error.message) |
| 171 | + console.error(error.stack) |
| 172 | + process.exit(1) |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +main() |
0 commit comments