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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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 (including multiline values)
  112. for (const keyToRemove of fileSpecificKeys) {
  113. const keyParts = keyToRemove.split('.')
  114. let targetLineIndex = -1
  115. const linesToRemoveForKey = []
  116. // Build regex pattern for the exact key path
  117. if (keyParts.length === 1) {
  118. // Simple key at root level like "pickDate: 'value'"
  119. for (let i = 0; i < lines.length; i++) {
  120. const line = lines[i]
  121. const simpleKeyPattern = new RegExp(`^\\s*${keyParts[0]}\\s*:`)
  122. if (simpleKeyPattern.test(line)) {
  123. targetLineIndex = i
  124. break
  125. }
  126. }
  127. }
  128. else {
  129. // Nested key - need to find the exact path
  130. const currentPath = []
  131. let braceDepth = 0
  132. for (let i = 0; i < lines.length; i++) {
  133. const line = lines[i]
  134. const trimmedLine = line.trim()
  135. // Track current object path
  136. const keyMatch = trimmedLine.match(/^(\w+)\s*:\s*{/)
  137. if (keyMatch) {
  138. currentPath.push(keyMatch[1])
  139. braceDepth++
  140. }
  141. else if (trimmedLine === '},' || trimmedLine === '}') {
  142. if (braceDepth > 0) {
  143. braceDepth--
  144. currentPath.pop()
  145. }
  146. }
  147. // Check if this line matches our target key
  148. const leafKeyMatch = trimmedLine.match(/^(\w+)\s*:/)
  149. if (leafKeyMatch) {
  150. const fullPath = [...currentPath, leafKeyMatch[1]]
  151. const fullPathString = fullPath.join('.')
  152. if (fullPathString === keyToRemove) {
  153. targetLineIndex = i
  154. break
  155. }
  156. }
  157. }
  158. }
  159. if (targetLineIndex !== -1) {
  160. linesToRemoveForKey.push(targetLineIndex)
  161. // Check if this is a multiline key-value pair
  162. const keyLine = lines[targetLineIndex]
  163. const trimmedKeyLine = keyLine.trim()
  164. // If key line ends with ":" (not ":", "{ " or complete value), it's likely multiline
  165. if (trimmedKeyLine.endsWith(':') && !trimmedKeyLine.includes('{') && !trimmedKeyLine.match(/:\s*['"`]/)) {
  166. // Find the value lines that belong to this key
  167. let currentLine = targetLineIndex + 1
  168. let foundValue = false
  169. while (currentLine < lines.length) {
  170. const line = lines[currentLine]
  171. const trimmed = line.trim()
  172. // Skip empty lines
  173. if (trimmed === '') {
  174. currentLine++
  175. continue
  176. }
  177. // Check if this line starts a new key (indicates end of current value)
  178. if (trimmed.match(/^\w+\s*:/))
  179. break
  180. // Check if this line is part of the value
  181. if (trimmed.startsWith('\'') || trimmed.startsWith('"') || trimmed.startsWith('`') || foundValue) {
  182. linesToRemoveForKey.push(currentLine)
  183. foundValue = true
  184. // Check if this line ends the value (ends with quote and comma/no comma)
  185. if ((trimmed.endsWith('\',') || trimmed.endsWith('",') || trimmed.endsWith('`,')
  186. || trimmed.endsWith('\'') || trimmed.endsWith('"') || trimmed.endsWith('`'))
  187. && !trimmed.startsWith('//'))
  188. break
  189. }
  190. else {
  191. break
  192. }
  193. currentLine++
  194. }
  195. }
  196. linesToRemove.push(...linesToRemoveForKey)
  197. console.log(`🗑️ Found key to remove: ${keyToRemove} at line ${targetLineIndex + 1}${linesToRemoveForKey.length > 1 ? ` (multiline, ${linesToRemoveForKey.length} lines)` : ''}`)
  198. modified = true
  199. }
  200. else {
  201. console.log(`⚠️ Could not find key: ${keyToRemove}`)
  202. }
  203. }
  204. if (modified) {
  205. // Remove duplicates and sort in reverse order to maintain correct indices
  206. const uniqueLinesToRemove = [...new Set(linesToRemove)].sort((a, b) => b - a)
  207. for (const lineIndex of uniqueLinesToRemove) {
  208. const line = lines[lineIndex]
  209. console.log(`🗑️ Removing line ${lineIndex + 1}: ${line.trim()}`)
  210. lines.splice(lineIndex, 1)
  211. // Also remove trailing comma from previous line if it exists and the next line is a closing brace
  212. if (lineIndex > 0 && lineIndex < lines.length) {
  213. const prevLine = lines[lineIndex - 1]
  214. const nextLine = lines[lineIndex] ? lines[lineIndex].trim() : ''
  215. if (prevLine.trim().endsWith(',') && (nextLine.startsWith('}') || nextLine === ''))
  216. lines[lineIndex - 1] = prevLine.replace(/,\s*$/, '')
  217. }
  218. }
  219. // Write back to file
  220. const newContent = lines.join('\n')
  221. fs.writeFileSync(filePath, newContent)
  222. console.log(`💾 Updated file: ${filePath}`)
  223. return true
  224. }
  225. return false
  226. }
  227. catch (error) {
  228. console.error(`Error processing file ${filePath}:`, error.message)
  229. return false
  230. }
  231. }
  232. // Add command line argument support
  233. const targetFile = process.argv.find(arg => arg.startsWith('--file='))?.split('=')[1]
  234. const targetLang = process.argv.find(arg => arg.startsWith('--lang='))?.split('=')[1]
  235. const autoRemove = process.argv.includes('--auto-remove')
  236. async function main() {
  237. const compareKeysCount = async () => {
  238. const allTargetKeys = await getKeysFromLanguage(targetLanguage)
  239. // Filter target keys by file if specified
  240. const targetKeys = targetFile
  241. ? allTargetKeys.filter(key => key.startsWith(`${targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase())}.`))
  242. : allTargetKeys
  243. // Filter languages by target language if specified
  244. const languagesToProcess = targetLang ? [targetLang] : languages
  245. const allLanguagesKeys = await Promise.all(languagesToProcess.map(language => getKeysFromLanguage(language)))
  246. // Filter language keys by file if specified
  247. const languagesKeys = targetFile
  248. ? allLanguagesKeys.map(keys => keys.filter(key => key.startsWith(`${targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase())}.`)))
  249. : allLanguagesKeys
  250. const keysCount = languagesKeys.map(keys => keys.length)
  251. const targetKeysCount = targetKeys.length
  252. const comparison = languagesToProcess.reduce((result, language, index) => {
  253. const languageKeysCount = keysCount[index]
  254. const difference = targetKeysCount - languageKeysCount
  255. result[language] = difference
  256. return result
  257. }, {})
  258. console.log(comparison)
  259. // Print missing keys and extra keys
  260. for (let index = 0; index < languagesToProcess.length; index++) {
  261. const language = languagesToProcess[index]
  262. const languageKeys = languagesKeys[index]
  263. const missingKeys = targetKeys.filter(key => !languageKeys.includes(key))
  264. const extraKeys = languageKeys.filter(key => !targetKeys.includes(key))
  265. console.log(`Missing keys in ${language}:`, missingKeys)
  266. // Show extra keys only when there are extra keys (negative difference)
  267. if (extraKeys.length > 0) {
  268. console.log(`Extra keys in ${language} (not in ${targetLanguage}):`, extraKeys)
  269. // Auto-remove extra keys if flag is set
  270. if (autoRemove) {
  271. console.log(`\n🤖 Auto-removing extra keys from ${language}...`)
  272. // Get all translation files
  273. const i18nFolder = path.resolve(__dirname, '../i18n', language)
  274. const files = fs.readdirSync(i18nFolder)
  275. .filter(file => /\.ts$/.test(file))
  276. .map(file => file.replace(/\.ts$/, ''))
  277. .filter(f => !targetFile || f === targetFile) // Filter by target file if specified
  278. let totalRemoved = 0
  279. for (const fileName of files) {
  280. const removed = await removeExtraKeysFromFile(language, fileName, extraKeys)
  281. if (removed) totalRemoved++
  282. }
  283. console.log(`✅ Auto-removal completed for ${language}. Modified ${totalRemoved} files.`)
  284. }
  285. }
  286. }
  287. }
  288. console.log('🚀 Starting check-i18n script...')
  289. if (targetFile)
  290. console.log(`📁 Checking file: ${targetFile}`)
  291. if (targetLang)
  292. console.log(`🌍 Checking language: ${targetLang}`)
  293. if (autoRemove)
  294. console.log('🤖 Auto-remove mode: ENABLED')
  295. compareKeysCount()
  296. }
  297. main()