|
@@ -0,0 +1,218 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * Script to find missing translations in locale files
|
|
|
|
|
+ *
|
|
|
|
|
+ * Usage:
|
|
|
|
|
+ * node scripts/find-missing-translations.js [options]
|
|
|
|
|
+ *
|
|
|
|
|
+ * Options:
|
|
|
|
|
+ * --locale=<locale> Only check a specific locale (e.g. --locale=fr)
|
|
|
|
|
+ * --file=<file> Only check a specific file (e.g. --file=chat.json)
|
|
|
|
|
+ * --help Show this help message
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+const fs = require("fs")
|
|
|
|
|
+const path = require("path")
|
|
|
|
|
+
|
|
|
|
|
+// Process command line arguments
|
|
|
|
|
+const args = process.argv.slice(2).reduce((acc, arg) => {
|
|
|
|
|
+ if (arg === "--help") {
|
|
|
|
|
+ acc.help = true
|
|
|
|
|
+ } else if (arg.startsWith("--locale=")) {
|
|
|
|
|
+ acc.locale = arg.split("=")[1]
|
|
|
|
|
+ } else if (arg.startsWith("--file=")) {
|
|
|
|
|
+ acc.file = arg.split("=")[1]
|
|
|
|
|
+ }
|
|
|
|
|
+ return acc
|
|
|
|
|
+}, {})
|
|
|
|
|
+
|
|
|
|
|
+// Show help if requested
|
|
|
|
|
+if (args.help) {
|
|
|
|
|
+ console.log(`
|
|
|
|
|
+Find Missing Translations
|
|
|
|
|
+
|
|
|
|
|
+A utility script to identify missing translations across locale files.
|
|
|
|
|
+Compares non-English locale files to the English ones to find any missing keys.
|
|
|
|
|
+
|
|
|
|
|
+Usage:
|
|
|
|
|
+ node scripts/find-missing-translations.js [options]
|
|
|
|
|
+
|
|
|
|
|
+Options:
|
|
|
|
|
+ --locale=<locale> Only check a specific locale (e.g. --locale=fr)
|
|
|
|
|
+ --file=<file> Only check a specific file (e.g. --file=chat.json)
|
|
|
|
|
+ --help Show this help message
|
|
|
|
|
+
|
|
|
|
|
+Output:
|
|
|
|
|
+ - Generates a report of missing translations
|
|
|
|
|
+ `)
|
|
|
|
|
+ process.exit(0)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Path to the locales directory
|
|
|
|
|
+const LOCALES_DIR = path.join(__dirname, "../webview-ui/src/i18n/locales")
|
|
|
|
|
+
|
|
|
|
|
+// Recursively find all keys in an object
|
|
|
|
|
+function findKeys(obj, parentKey = "") {
|
|
|
|
|
+ let keys = []
|
|
|
|
|
+
|
|
|
|
|
+ for (const [key, value] of Object.entries(obj)) {
|
|
|
|
|
+ const currentKey = parentKey ? `${parentKey}.${key}` : key
|
|
|
|
|
+
|
|
|
|
|
+ if (typeof value === "object" && value !== null) {
|
|
|
|
|
+ // If value is an object, recurse
|
|
|
|
|
+ keys = [...keys, ...findKeys(value, currentKey)]
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // If value is a primitive, add the key
|
|
|
|
|
+ keys.push(currentKey)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return keys
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Get value at a dotted path in an object
|
|
|
|
|
+function getValueAtPath(obj, path) {
|
|
|
|
|
+ const parts = path.split(".")
|
|
|
|
|
+ let current = obj
|
|
|
|
|
+
|
|
|
|
|
+ for (const part of parts) {
|
|
|
|
|
+ if (current === undefined || current === null) {
|
|
|
|
|
+ return undefined
|
|
|
|
|
+ }
|
|
|
|
|
+ current = current[part]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return current
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Main function to find missing translations
|
|
|
|
|
+function findMissingTranslations() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Get all locale directories (or filter to the specified locale)
|
|
|
|
|
+ const allLocales = fs.readdirSync(LOCALES_DIR).filter((item) => {
|
|
|
|
|
+ const stats = fs.statSync(path.join(LOCALES_DIR, item))
|
|
|
|
|
+ return stats.isDirectory() && item !== "en" // Exclude English as it's our source
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // Filter to the specified locale if provided
|
|
|
|
|
+ const locales = args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales
|
|
|
|
|
+
|
|
|
|
|
+ if (args.locale && locales.length === 0) {
|
|
|
|
|
+ console.error(`Error: Locale '${args.locale}' not found in ${LOCALES_DIR}`)
|
|
|
|
|
+ process.exit(1)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`Checking ${locales.length} non-English locale(s): ${locales.join(", ")}`)
|
|
|
|
|
+
|
|
|
|
|
+ // Get all English JSON files
|
|
|
|
|
+ const englishDir = path.join(LOCALES_DIR, "en")
|
|
|
|
|
+ let englishFiles = fs.readdirSync(englishDir).filter((file) => file.endsWith(".json") && !file.startsWith("."))
|
|
|
|
|
+
|
|
|
|
|
+ // Filter to the specified file if provided
|
|
|
|
|
+ if (args.file) {
|
|
|
|
|
+ if (!englishFiles.includes(args.file)) {
|
|
|
|
|
+ console.error(`Error: File '${args.file}' not found in ${englishDir}`)
|
|
|
|
|
+ process.exit(1)
|
|
|
|
|
+ }
|
|
|
|
|
+ englishFiles = englishFiles.filter((file) => file === args.file)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Load file contents
|
|
|
|
|
+ const englishFileContents = englishFiles.map((file) => ({
|
|
|
|
|
+ name: file,
|
|
|
|
|
+ content: JSON.parse(fs.readFileSync(path.join(englishDir, file), "utf8")),
|
|
|
|
|
+ }))
|
|
|
|
|
+
|
|
|
|
|
+ console.log(
|
|
|
|
|
+ `Checking ${englishFileContents.length} translation file(s): ${englishFileContents.map((f) => f.name).join(", ")}`,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ // Results object to store missing translations
|
|
|
|
|
+ const missingTranslations = {}
|
|
|
|
|
+
|
|
|
|
|
+ // For each locale, check for missing translations
|
|
|
|
|
+ for (const locale of locales) {
|
|
|
|
|
+ missingTranslations[locale] = {}
|
|
|
|
|
+
|
|
|
|
|
+ for (const { name, content: englishContent } of englishFileContents) {
|
|
|
|
|
+ const localeFilePath = path.join(LOCALES_DIR, locale, name)
|
|
|
|
|
+
|
|
|
|
|
+ // Check if the file exists in the locale
|
|
|
|
|
+ if (!fs.existsSync(localeFilePath)) {
|
|
|
|
|
+ missingTranslations[locale][name] = { file: "File is missing entirely" }
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Load the locale file
|
|
|
|
|
+ const localeContent = JSON.parse(fs.readFileSync(localeFilePath, "utf8"))
|
|
|
|
|
+
|
|
|
|
|
+ // Find all keys in the English file
|
|
|
|
|
+ const englishKeys = findKeys(englishContent)
|
|
|
|
|
+
|
|
|
|
|
+ // Check for missing keys in the locale file
|
|
|
|
|
+ const missingKeys = []
|
|
|
|
|
+
|
|
|
|
|
+ for (const key of englishKeys) {
|
|
|
|
|
+ const englishValue = getValueAtPath(englishContent, key)
|
|
|
|
|
+ const localeValue = getValueAtPath(localeContent, key)
|
|
|
|
|
+
|
|
|
|
|
+ if (localeValue === undefined) {
|
|
|
|
|
+ missingKeys.push({
|
|
|
|
|
+ key,
|
|
|
|
|
+ englishValue,
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (missingKeys.length > 0) {
|
|
|
|
|
+ missingTranslations[locale][name] = missingKeys
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Output results
|
|
|
|
|
+ let hasMissingTranslations = false
|
|
|
|
|
+
|
|
|
|
|
+ console.log("\nMissing Translations Report:\n")
|
|
|
|
|
+
|
|
|
|
|
+ for (const [locale, files] of Object.entries(missingTranslations)) {
|
|
|
|
|
+ if (Object.keys(files).length === 0) {
|
|
|
|
|
+ console.log(`✅ ${locale}: No missing translations`)
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ hasMissingTranslations = true
|
|
|
|
|
+ console.log(`📝 ${locale}:`)
|
|
|
|
|
+
|
|
|
|
|
+ for (const [fileName, missingItems] of Object.entries(files)) {
|
|
|
|
|
+ if (missingItems.file) {
|
|
|
|
|
+ console.log(` - ${fileName}: ${missingItems.file}`)
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log(` - ${fileName}: ${missingItems.length} missing translations`)
|
|
|
|
|
+
|
|
|
|
|
+ for (const { key, englishValue } of missingItems) {
|
|
|
|
|
+ console.log(` ${key}: "${englishValue}"`)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log("")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!hasMissingTranslations) {
|
|
|
|
|
+ console.log("\n✅ All translations are complete!")
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log("✏️ To add missing translations:")
|
|
|
|
|
+ console.log("1. Add the missing keys to the corresponding locale files")
|
|
|
|
|
+ console.log("2. Translate the English values to the appropriate language")
|
|
|
|
|
+ console.log("3. Run this script again to verify all translations are complete")
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("Error:", error.message)
|
|
|
|
|
+ console.error(error.stack)
|
|
|
|
|
+ process.exit(1)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Run the main function
|
|
|
|
|
+findMissingTranslations()
|