find-missing-translations.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /**
  2. * Script to find missing translations in locale files
  3. *
  4. * Usage:
  5. * node scripts/find-missing-translations.js [options]
  6. *
  7. * Options:
  8. * --locale=<locale> Only check a specific locale (e.g. --locale=fr)
  9. * --file=<file> Only check a specific file (e.g. --file=chat.json)
  10. * --help Show this help message
  11. */
  12. const fs = require("fs")
  13. const path = require("path")
  14. // Process command line arguments
  15. const args = process.argv.slice(2).reduce((acc, arg) => {
  16. if (arg === "--help") {
  17. acc.help = true
  18. } else if (arg.startsWith("--locale=")) {
  19. acc.locale = arg.split("=")[1]
  20. } else if (arg.startsWith("--file=")) {
  21. acc.file = arg.split("=")[1]
  22. }
  23. return acc
  24. }, {})
  25. // Show help if requested
  26. if (args.help) {
  27. console.log(`
  28. Find Missing Translations
  29. A utility script to identify missing translations across locale files.
  30. Compares non-English locale files to the English ones to find any missing keys.
  31. Usage:
  32. node scripts/find-missing-translations.js [options]
  33. Options:
  34. --locale=<locale> Only check a specific locale (e.g. --locale=fr)
  35. --file=<file> Only check a specific file (e.g. --file=chat.json)
  36. --help Show this help message
  37. Output:
  38. - Generates a report of missing translations
  39. `)
  40. process.exit(0)
  41. }
  42. // Path to the locales directory
  43. const LOCALES_DIR = path.join(__dirname, "../webview-ui/src/i18n/locales")
  44. // Recursively find all keys in an object
  45. function findKeys(obj, parentKey = "") {
  46. let keys = []
  47. for (const [key, value] of Object.entries(obj)) {
  48. const currentKey = parentKey ? `${parentKey}.${key}` : key
  49. if (typeof value === "object" && value !== null) {
  50. // If value is an object, recurse
  51. keys = [...keys, ...findKeys(value, currentKey)]
  52. } else {
  53. // If value is a primitive, add the key
  54. keys.push(currentKey)
  55. }
  56. }
  57. return keys
  58. }
  59. // Get value at a dotted path in an object
  60. function getValueAtPath(obj, path) {
  61. const parts = path.split(".")
  62. let current = obj
  63. for (const part of parts) {
  64. if (current === undefined || current === null) {
  65. return undefined
  66. }
  67. current = current[part]
  68. }
  69. return current
  70. }
  71. // Main function to find missing translations
  72. function findMissingTranslations() {
  73. try {
  74. // Get all locale directories (or filter to the specified locale)
  75. const allLocales = fs.readdirSync(LOCALES_DIR).filter((item) => {
  76. const stats = fs.statSync(path.join(LOCALES_DIR, item))
  77. return stats.isDirectory() && item !== "en" // Exclude English as it's our source
  78. })
  79. // Filter to the specified locale if provided
  80. const locales = args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales
  81. if (args.locale && locales.length === 0) {
  82. console.error(`Error: Locale '${args.locale}' not found in ${LOCALES_DIR}`)
  83. process.exit(1)
  84. }
  85. console.log(`Checking ${locales.length} non-English locale(s): ${locales.join(", ")}`)
  86. // Get all English JSON files
  87. const englishDir = path.join(LOCALES_DIR, "en")
  88. let englishFiles = fs.readdirSync(englishDir).filter((file) => file.endsWith(".json") && !file.startsWith("."))
  89. // Filter to the specified file if provided
  90. if (args.file) {
  91. if (!englishFiles.includes(args.file)) {
  92. console.error(`Error: File '${args.file}' not found in ${englishDir}`)
  93. process.exit(1)
  94. }
  95. englishFiles = englishFiles.filter((file) => file === args.file)
  96. }
  97. // Load file contents
  98. const englishFileContents = englishFiles.map((file) => ({
  99. name: file,
  100. content: JSON.parse(fs.readFileSync(path.join(englishDir, file), "utf8")),
  101. }))
  102. console.log(
  103. `Checking ${englishFileContents.length} translation file(s): ${englishFileContents.map((f) => f.name).join(", ")}`,
  104. )
  105. // Results object to store missing translations
  106. const missingTranslations = {}
  107. // For each locale, check for missing translations
  108. for (const locale of locales) {
  109. missingTranslations[locale] = {}
  110. for (const { name, content: englishContent } of englishFileContents) {
  111. const localeFilePath = path.join(LOCALES_DIR, locale, name)
  112. // Check if the file exists in the locale
  113. if (!fs.existsSync(localeFilePath)) {
  114. missingTranslations[locale][name] = { file: "File is missing entirely" }
  115. continue
  116. }
  117. // Load the locale file
  118. const localeContent = JSON.parse(fs.readFileSync(localeFilePath, "utf8"))
  119. // Find all keys in the English file
  120. const englishKeys = findKeys(englishContent)
  121. // Check for missing keys in the locale file
  122. const missingKeys = []
  123. for (const key of englishKeys) {
  124. const englishValue = getValueAtPath(englishContent, key)
  125. const localeValue = getValueAtPath(localeContent, key)
  126. if (localeValue === undefined) {
  127. missingKeys.push({
  128. key,
  129. englishValue,
  130. })
  131. }
  132. }
  133. if (missingKeys.length > 0) {
  134. missingTranslations[locale][name] = missingKeys
  135. }
  136. }
  137. }
  138. // Output results
  139. let hasMissingTranslations = false
  140. console.log("\nMissing Translations Report:\n")
  141. for (const [locale, files] of Object.entries(missingTranslations)) {
  142. if (Object.keys(files).length === 0) {
  143. console.log(`✅ ${locale}: No missing translations`)
  144. continue
  145. }
  146. hasMissingTranslations = true
  147. console.log(`📝 ${locale}:`)
  148. for (const [fileName, missingItems] of Object.entries(files)) {
  149. if (missingItems.file) {
  150. console.log(` - ${fileName}: ${missingItems.file}`)
  151. continue
  152. }
  153. console.log(` - ${fileName}: ${missingItems.length} missing translations`)
  154. for (const { key, englishValue } of missingItems) {
  155. console.log(` ${key}: "${englishValue}"`)
  156. }
  157. }
  158. console.log("")
  159. }
  160. if (!hasMissingTranslations) {
  161. console.log("\n✅ All translations are complete!")
  162. } else {
  163. console.log("✏️ To add missing translations:")
  164. console.log("1. Add the missing keys to the corresponding locale files")
  165. console.log("2. Translate the English values to the appropriate language")
  166. console.log("3. Run this script again to verify all translations are complete")
  167. // Exit with error code to fail CI checks
  168. process.exit(1)
  169. }
  170. } catch (error) {
  171. console.error("Error:", error.message)
  172. console.error(error.stack)
  173. process.exit(1)
  174. }
  175. }
  176. // Run the main function
  177. findMissingTranslations()