find-missing-i18n-key.js 5.5 KB

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