You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

check-i18n.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. const fs = require('node:fs')
  2. const path = require('node:path')
  3. const vm = require('node:vm')
  4. const transpile = require('typescript').transpile
  5. const targetLanguage = 'en-US'
  6. const data = require('./languages.json')
  7. const languages = data.languages.filter(language => language.supported).map(language => language.value)
  8. async function getKeysFromLanguage(language) {
  9. return new Promise((resolve, reject) => {
  10. const folderPath = path.resolve(__dirname, '../i18n', language)
  11. const allKeys = []
  12. fs.readdir(folderPath, (err, files) => {
  13. if (err) {
  14. console.error('Error reading folder:', err)
  15. reject(err)
  16. return
  17. }
  18. // Filter only .ts and .js files
  19. const translationFiles = files.filter(file => /\.(ts|js)$/.test(file))
  20. translationFiles.forEach((file) => {
  21. const filePath = path.join(folderPath, file)
  22. const fileName = file.replace(/\.[^/.]+$/, '') // Remove file extension
  23. const camelCaseFileName = fileName.replace(/[-_](.)/g, (_, c) =>
  24. c.toUpperCase(),
  25. ) // Convert to camel case
  26. try {
  27. const content = fs.readFileSync(filePath, 'utf8')
  28. // Create a safer module environment for vm
  29. const moduleExports = {}
  30. const context = {
  31. exports: moduleExports,
  32. module: { exports: moduleExports },
  33. require,
  34. console,
  35. __filename: filePath,
  36. __dirname: folderPath,
  37. }
  38. // Use vm.runInNewContext instead of eval for better security
  39. vm.runInNewContext(transpile(content), context)
  40. // Extract the translation object
  41. const translationObj = moduleExports.default || moduleExports
  42. if(!translationObj || typeof translationObj !== 'object') {
  43. console.error(`Error parsing file: ${filePath}`)
  44. reject(new Error(`Error parsing file: ${filePath}`))
  45. return
  46. }
  47. const nestedKeys = []
  48. const iterateKeys = (obj, prefix = '') => {
  49. for (const key in obj) {
  50. const nestedKey = prefix ? `${prefix}.${key}` : key
  51. if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
  52. // This is an object (but not array), recurse into it but don't add it as a key
  53. iterateKeys(obj[key], nestedKey)
  54. }
  55. else {
  56. // This is a leaf node (string, number, boolean, array, etc.), add it as a key
  57. nestedKeys.push(nestedKey)
  58. }
  59. }
  60. }
  61. iterateKeys(translationObj)
  62. // Fixed: accumulate keys instead of overwriting
  63. const fileKeys = nestedKeys.map(key => `${camelCaseFileName}.${key}`)
  64. allKeys.push(...fileKeys)
  65. }
  66. catch (error) {
  67. console.error(`Error processing file ${filePath}:`, error.message)
  68. reject(error)
  69. }
  70. })
  71. resolve(allKeys)
  72. })
  73. })
  74. }
  75. function removeKeysFromObject(obj, keysToRemove, prefix = '') {
  76. let modified = false
  77. for (const key in obj) {
  78. const fullKey = prefix ? `${prefix}.${key}` : key
  79. if (keysToRemove.includes(fullKey)) {
  80. delete obj[key]
  81. modified = true
  82. console.log(`🗑️ Removed key: ${fullKey}`)
  83. }
  84. else if (typeof obj[key] === 'object' && obj[key] !== null) {
  85. const subModified = removeKeysFromObject(obj[key], keysToRemove, fullKey)
  86. modified = modified || subModified
  87. }
  88. }
  89. return modified
  90. }
  91. async function removeExtraKeysFromFile(language, fileName, extraKeys) {
  92. const filePath = path.resolve(__dirname, '../i18n', language, `${fileName}.ts`)
  93. if (!fs.existsSync(filePath)) {
  94. console.log(`⚠️ File not found: ${filePath}`)
  95. return false
  96. }
  97. try {
  98. // Filter keys that belong to this file
  99. const camelCaseFileName = fileName.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
  100. const fileSpecificKeys = extraKeys
  101. .filter(key => key.startsWith(`${camelCaseFileName}.`))
  102. .map(key => key.substring(camelCaseFileName.length + 1)) // Remove file prefix
  103. if (fileSpecificKeys.length === 0)
  104. return false
  105. console.log(`🔄 Processing file: ${filePath}`)
  106. // Read the original file content
  107. const content = fs.readFileSync(filePath, 'utf8')
  108. const lines = content.split('\n')
  109. let modified = false
  110. const linesToRemove = []
  111. // Find lines to remove for each key
  112. for (const keyToRemove of fileSpecificKeys) {
  113. const keyParts = keyToRemove.split('.')
  114. let targetLineIndex = -1
  115. // Build regex pattern for the exact key path
  116. if (keyParts.length === 1) {
  117. // Simple key at root level like "pickDate: 'value'"
  118. for (let i = 0; i < lines.length; i++) {
  119. const line = lines[i]
  120. const simpleKeyPattern = new RegExp(`^\\s*${keyParts[0]}\\s*:`)
  121. if (simpleKeyPattern.test(line)) {
  122. targetLineIndex = i
  123. break
  124. }
  125. }
  126. }
  127. else {
  128. // Nested key - need to find the exact path
  129. const currentPath = []
  130. let braceDepth = 0
  131. for (let i = 0; i < lines.length; i++) {
  132. const line = lines[i]
  133. const trimmedLine = line.trim()
  134. // Track current object path
  135. const keyMatch = trimmedLine.match(/^(\w+)\s*:\s*{/)
  136. if (keyMatch) {
  137. currentPath.push(keyMatch[1])
  138. braceDepth++
  139. }
  140. else if (trimmedLine === '},' || trimmedLine === '}') {
  141. if (braceDepth > 0) {
  142. braceDepth--
  143. currentPath.pop()
  144. }
  145. }
  146. // Check if this line matches our target key
  147. const leafKeyMatch = trimmedLine.match(/^(\w+)\s*:/)
  148. if (leafKeyMatch) {
  149. const fullPath = [...currentPath, leafKeyMatch[1]]
  150. const fullPathString = fullPath.join('.')
  151. if (fullPathString === keyToRemove) {
  152. targetLineIndex = i
  153. break
  154. }
  155. }
  156. }
  157. }
  158. if (targetLineIndex !== -1) {
  159. linesToRemove.push(targetLineIndex)
  160. console.log(`🗑️ Found key to remove: ${keyToRemove} at line ${targetLineIndex + 1}`)
  161. modified = true
  162. }
  163. else {
  164. console.log(`⚠️ Could not find key: ${keyToRemove}`)
  165. }
  166. }
  167. if (modified) {
  168. // Remove lines in reverse order to maintain correct indices
  169. linesToRemove.sort((a, b) => b - a)
  170. for (const lineIndex of linesToRemove) {
  171. const line = lines[lineIndex]
  172. console.log(`🗑️ Removing line ${lineIndex + 1}: ${line.trim()}`)
  173. lines.splice(lineIndex, 1)
  174. // Also remove trailing comma from previous line if it exists and the next line is a closing brace
  175. if (lineIndex > 0 && lineIndex < lines.length) {
  176. const prevLine = lines[lineIndex - 1]
  177. const nextLine = lines[lineIndex] ? lines[lineIndex].trim() : ''
  178. if (prevLine.trim().endsWith(',') && (nextLine.startsWith('}') || nextLine === ''))
  179. lines[lineIndex - 1] = prevLine.replace(/,\s*$/, '')
  180. }
  181. }
  182. // Write back to file
  183. const newContent = lines.join('\n')
  184. fs.writeFileSync(filePath, newContent)
  185. console.log(`💾 Updated file: ${filePath}`)
  186. return true
  187. }
  188. return false
  189. }
  190. catch (error) {
  191. console.error(`Error processing file ${filePath}:`, error.message)
  192. return false
  193. }
  194. }
  195. // Add command line argument support
  196. const targetFile = process.argv.find(arg => arg.startsWith('--file='))?.split('=')[1]
  197. const targetLang = process.argv.find(arg => arg.startsWith('--lang='))?.split('=')[1]
  198. const autoRemove = process.argv.includes('--auto-remove')
  199. async function main() {
  200. const compareKeysCount = async () => {
  201. const allTargetKeys = await getKeysFromLanguage(targetLanguage)
  202. // Filter target keys by file if specified
  203. const targetKeys = targetFile
  204. ? allTargetKeys.filter(key => key.startsWith(targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase())))
  205. : allTargetKeys
  206. // Filter languages by target language if specified
  207. const languagesToProcess = targetLang ? [targetLang] : languages
  208. const allLanguagesKeys = await Promise.all(languagesToProcess.map(language => getKeysFromLanguage(language)))
  209. // Filter language keys by file if specified
  210. const languagesKeys = targetFile
  211. ? allLanguagesKeys.map(keys => keys.filter(key => key.startsWith(targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase()))))
  212. : allLanguagesKeys
  213. const keysCount = languagesKeys.map(keys => keys.length)
  214. const targetKeysCount = targetKeys.length
  215. const comparison = languagesToProcess.reduce((result, language, index) => {
  216. const languageKeysCount = keysCount[index]
  217. const difference = targetKeysCount - languageKeysCount
  218. result[language] = difference
  219. return result
  220. }, {})
  221. console.log(comparison)
  222. // Print missing keys and extra keys
  223. for (let index = 0; index < languagesToProcess.length; index++) {
  224. const language = languagesToProcess[index]
  225. const languageKeys = languagesKeys[index]
  226. const missingKeys = targetKeys.filter(key => !languageKeys.includes(key))
  227. const extraKeys = languageKeys.filter(key => !targetKeys.includes(key))
  228. console.log(`Missing keys in ${language}:`, missingKeys)
  229. // Show extra keys only when there are extra keys (negative difference)
  230. if (extraKeys.length > 0) {
  231. console.log(`Extra keys in ${language} (not in ${targetLanguage}):`, extraKeys)
  232. // Auto-remove extra keys if flag is set
  233. if (autoRemove) {
  234. console.log(`\n🤖 Auto-removing extra keys from ${language}...`)
  235. // Get all translation files
  236. const i18nFolder = path.resolve(__dirname, '../i18n', language)
  237. const files = fs.readdirSync(i18nFolder)
  238. .filter(file => /\.ts$/.test(file))
  239. .map(file => file.replace(/\.ts$/, ''))
  240. .filter(f => !targetFile || f === targetFile) // Filter by target file if specified
  241. let totalRemoved = 0
  242. for (const fileName of files) {
  243. const removed = await removeExtraKeysFromFile(language, fileName, extraKeys)
  244. if (removed) totalRemoved++
  245. }
  246. console.log(`✅ Auto-removal completed for ${language}. Modified ${totalRemoved} files.`)
  247. }
  248. }
  249. }
  250. }
  251. console.log('🚀 Starting check-i18n script...')
  252. if (targetFile)
  253. console.log(`📁 Checking file: ${targetFile}`)
  254. if (targetLang)
  255. console.log(`🌍 Checking language: ${targetLang}`)
  256. if (autoRemove)
  257. console.log('🤖 Auto-remove mode: ENABLED')
  258. compareKeysCount()
  259. }
  260. main()