find-missing-i18n-key.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  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. // Directory to traverse
  31. const TARGET_DIR = path.join(__dirname, "../webview-ui/src/components")
  32. const LOCALES_DIR = path.join(__dirname, "../webview-ui/src/i18n/locales")
  33. // Regular expressions to match i18n keys
  34. const i18nPatterns = [
  35. /{t\("([^"]+)"\)}/g, // Match {t("key")} format
  36. /i18nKey="([^"]+)"/g, // Match i18nKey="key" format
  37. /t\("([a-zA-Z][a-zA-Z0-9_]*[:.][a-zA-Z0-9_.]+)"\)/g, // Match t("key") format, where key contains a colon or dot
  38. ]
  39. // Get all language directories
  40. function getLocaleDirs() {
  41. const allLocales = fs.readdirSync(LOCALES_DIR).filter((file) => {
  42. const stats = fs.statSync(path.join(LOCALES_DIR, file))
  43. return stats.isDirectory() // Do not exclude any language directories
  44. })
  45. // Filter to a specific language if specified
  46. return args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales
  47. }
  48. // Get the value from JSON by path
  49. function getValueByPath(obj, path) {
  50. const parts = path.split(".")
  51. let current = obj
  52. for (const part of parts) {
  53. if (current === undefined || current === null) {
  54. return undefined
  55. }
  56. current = current[part]
  57. }
  58. return current
  59. }
  60. // Check if the key exists in all language files, return a list of missing language files
  61. function checkKeyInLocales(key, localeDirs) {
  62. const [file, ...pathParts] = key.split(":")
  63. const jsonPath = pathParts.join(".")
  64. const missingLocales = []
  65. localeDirs.forEach((locale) => {
  66. const filePath = path.join(LOCALES_DIR, locale, `${file}.json`)
  67. if (!fs.existsSync(filePath)) {
  68. missingLocales.push(`${locale}/${file}.json`)
  69. return
  70. }
  71. const json = JSON.parse(fs.readFileSync(filePath, "utf8"))
  72. if (getValueByPath(json, jsonPath) === undefined) {
  73. missingLocales.push(`${locale}/${file}.json`)
  74. }
  75. })
  76. return missingLocales
  77. }
  78. // Recursively traverse the directory
  79. function findMissingI18nKeys() {
  80. const localeDirs = getLocaleDirs()
  81. const results = []
  82. function walk(dir) {
  83. const files = fs.readdirSync(dir)
  84. for (const file of files) {
  85. const filePath = path.join(dir, file)
  86. const stat = fs.statSync(filePath)
  87. // Exclude test files
  88. if (filePath.includes(".test.")) continue
  89. if (stat.isDirectory()) {
  90. walk(filePath) // Recursively traverse subdirectories
  91. } else if (stat.isFile() && [".ts", ".tsx", ".js", ".jsx"].includes(path.extname(filePath))) {
  92. const content = fs.readFileSync(filePath, "utf8")
  93. // Match all i18n keys
  94. for (const pattern of i18nPatterns) {
  95. let match
  96. while ((match = pattern.exec(content)) !== null) {
  97. const key = match[1]
  98. const missingLocales = checkKeyInLocales(key, localeDirs)
  99. if (missingLocales.length > 0) {
  100. results.push({
  101. key,
  102. missingLocales,
  103. file: path.relative(TARGET_DIR, filePath),
  104. })
  105. }
  106. }
  107. }
  108. }
  109. }
  110. }
  111. walk(TARGET_DIR)
  112. return results
  113. }
  114. // Execute and output the results
  115. function main() {
  116. try {
  117. const localeDirs = getLocaleDirs()
  118. if (args.locale && localeDirs.length === 0) {
  119. console.error(`Error: Language '${args.locale}' not found in ${LOCALES_DIR}`)
  120. process.exit(1)
  121. }
  122. console.log(`Checking ${localeDirs.length} non-English languages: ${localeDirs.join(", ")}`)
  123. const missingKeys = findMissingI18nKeys()
  124. if (missingKeys.length === 0) {
  125. console.log("\n✅ All i18n keys are present!")
  126. return
  127. }
  128. console.log("\nMissing i18n keys:\n")
  129. missingKeys.forEach(({ key, missingLocales, file }) => {
  130. console.log(`File: ${file}`)
  131. console.log(`Key: ${key}`)
  132. console.log("Missing in:")
  133. missingLocales.forEach((file) => console.log(` - ${file}`))
  134. console.log("-------------------")
  135. })
  136. // Exit code 1 indicates missing keys
  137. process.exit(1)
  138. } catch (error) {
  139. console.error("Error:", error.message)
  140. console.error(error.stack)
  141. process.exit(1)
  142. }
  143. }
  144. main()