[READ-ONLY] One Calendar is a privacy-first calendar web app built with Next.js. It has modern security features, including e2ee, password-protected sharing, and self-destructing share links 📅 calendar.xyehr.cn
nextjs
0

Configure Feed

Select the types of activity you want to include in your feed.

One-Calendar / lib / cleanup-i18n.mjs
5.8 kB 198 lines
1import fs from 'node:fs/promises' 2import path from 'node:path' 3import { fileURLToPath } from 'node:url' 4import readline from 'node:readline' 5 6const __filename = fileURLToPath(import.meta.url) 7const __dirname = path.dirname(__filename) 8const projectRoot = path.resolve(__dirname, '..') 9const localesDir = path.join(projectRoot, 'locales') 10const sourceLocale = 'en.json' 11 12const rl = readline.createInterface({ 13 input: process.stdin, 14 output: process.stdout, 15}) 16 17const question = (query) => new Promise((resolve) => rl.question(query, resolve)) 18 19/** 20 * Parses user input like "1, 3-5, 8" into an array of 0-based indices. 21 */ 22function parseIndices(input, max) { 23 if (input.toLowerCase() === 'all') { 24 return Array.from({ length: max }, (_, i) => i) 25 } 26 27 const result = new Set() 28 const parts = input.split(/[\s,]+/) 29 30 for (const part of parts) { 31 if (part.includes('-')) { 32 const split = part.split('-').map((n) => parseInt(n.trim(), 10)) 33 if (split.length === 2) { 34 const [start, end] = split 35 if (!isNaN(start) && !isNaN(end)) { 36 for (let i = Math.min(start, end); i <= Math.max(start, end); i++) { 37 if (i >= 1 && i <= max) result.add(i - 1) 38 } 39 } 40 } 41 } else { 42 const n = parseInt(part.trim(), 10) 43 if (!isNaN(n) && n >= 1 && n <= max) { 44 result.add(n - 1) 45 } 46 } 47 } 48 49 return Array.from(result).sort((a, b) => a - b) 50} 51 52async function getFiles(dir) { 53 const dirents = await fs.readdir(dir, { withFileTypes: true }) 54 const files = await Promise.all( 55 dirents.map((dirent) => { 56 const res = path.resolve(dir, dirent.name) 57 return dirent.isDirectory() ? getFiles(res) : res 58 }), 59 ) 60 return files.flat() 61} 62 63/** 64 * Escapes special characters for use in a regular expression. 65 */ 66function escapeRegExp(string) { 67 return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 68} 69 70async function run() { 71 console.log('🔍 Scanning for unused i18n keys...') 72 73 const enPath = path.join(localesDir, sourceLocale) 74 const enContent = await fs.readFile(enPath, 'utf8') 75 const enJson = JSON.parse(enContent) 76 const keys = Object.keys(enJson) 77 78 const searchDirs = ['app', 'components', 'lib', 'hooks'] 79 let allCodeFiles = [] 80 for (const dir of searchDirs) { 81 const fullPath = path.join(projectRoot, dir) 82 try { 83 const files = await getFiles(fullPath) 84 allCodeFiles = allCodeFiles.concat( 85 files.filter((f) => /\.(ts|tsx|js|jsx|mjs)$/.test(f)), 86 ) 87 } catch (e) { 88 // Directory might not exist 89 } 90 } 91 92 console.log(`📄 Found ${allCodeFiles.length} code files.`) 93 console.log(`🔑 Found ${keys.length} keys in ${sourceLocale}.`) 94 95 const fileContents = await Promise.all( 96 allCodeFiles.map(async (f) => { 97 const content = await fs.readFile(f, 'utf8') 98 return { path: f, content } 99 }), 100 ) 101 102 const unusedKeys = [] 103 104 for (const key of keys) { 105 let used = false 106 const escapedKey = escapeRegExp(key) 107 108 // Patterns to look for 109 // Using string concatenation for the regex to avoid backtick escaping issues in template literals 110 const patterns = [ 111 new RegExp('t(\\.|\\?\\.)' + escapedKey + '\\b'), 112 new RegExp("t\\[['\"`]" + escapedKey + "['\"`]\\]"), 113 new RegExp('\\{\\s*[^}]*\\b' + escapedKey + '\\b[^}]*\\}\\s*=\\s*t\\b'), 114 new RegExp("['\"`]" + escapedKey + "['\"`]") 115 ] 116 117 for (const { content } of fileContents) { 118 if (patterns.some(pattern => pattern.test(content))) { 119 used = true 120 break 121 } 122 } 123 124 if (!used) { 125 unusedKeys.push(key) 126 } 127 } 128 129 if (unusedKeys.length === 0) { 130 console.log('✅ No unused keys found!') 131 process.exit(0) 132 } 133 134 console.log(`\nFound ${unusedKeys.length} potentially unused keys:`) 135 unusedKeys.forEach((key, index) => { 136 console.log(`${index + 1}. ${key}`) 137 }) 138 139 console.log('\nOptions:') 140 console.log('- Enter indices to delete (e.g., "1, 3-5, 8")') 141 console.log('- Type "all" to delete everything listed') 142 console.log('- Press Enter to cancel') 143 144 const input = await question('\nYour selection: ') 145 const selectedIndices = parseIndices(input, unusedKeys.length) 146 147 if (selectedIndices.length > 0) { 148 const keysToDelete = selectedIndices.map((i) => unusedKeys[i]) 149 150 console.log(`\nSelected ${keysToDelete.length} keys for deletion.`) 151 const confirm = await question('Are you sure you want to proceed? (y/N): ') 152 153 if (confirm.toLowerCase() === 'y') { 154 const localeFiles = (await fs.readdir(localesDir)).filter((f) => f.endsWith('.json')) 155 156 for (const file of localeFiles) { 157 const filePath = path.join(localesDir, file) 158 const content = await fs.readFile(filePath, 'utf8') 159 const json = JSON.parse(content) 160 161 let deletedCount = 0 162 for (const key of keysToDelete) { 163 if (Object.prototype.hasOwnProperty.call(json, key)) { 164 delete json[key] 165 deletedCount++ 166 } 167 } 168 169 if (deletedCount > 0) { 170 await fs.writeFile(filePath, JSON.stringify(json, null, 2) + '\n', 'utf8') 171 console.log(`Updated ${file} (removed ${deletedCount} keys)`) 172 } 173 } 174 175 // Check for i18n.lock and warn 176 try { 177 await fs.access(path.join(projectRoot, 'i18n.lock')) 178 console.log('\n⚠️ Detected i18n.lock file. You may need to run your translation tool (e.g., lingo) to resync.') 179 } catch (e) { 180 // No lock file, ignore 181 } 182 183 console.log('\n✨ Cleanup complete!') 184 console.log('Run `pnpm run generate:locales` to update lib/locales.ts') 185 } else { 186 console.log('\nAborted.') 187 } 188 } else { 189 console.log('\nNo keys selected. Aborted.') 190 } 191 192 rl.close() 193} 194 195run().catch((err) => { 196 console.error('Error:', err) 197 process.exit(1) 198})