find-missing-translations.js 8.0 KB

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