import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import data from './languages' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const targetLanguage = 'en-US' const languages = data.languages.filter(language => language.supported).map(language => language.value) function parseArgs(argv) { const args = { files: [], languages: [], autoRemove: false, help: false, errors: [], } const collectValues = (startIndex) => { const values = [] let cursor = startIndex + 1 while (cursor < argv.length && !argv[cursor].startsWith('--')) { const value = argv[cursor].trim() if (value) values.push(value) cursor++ } return { values, nextIndex: cursor - 1 } } const validateList = (values, flag) => { if (!values.length) { args.errors.push(`${flag} requires at least one value. Example: ${flag} app billing`) return false } const invalid = values.find(value => value.includes(',')) if (invalid) { args.errors.push(`${flag} expects space-separated values. Example: ${flag} app billing`) return false } return true } for (let index = 2; index < argv.length; index++) { const arg = argv[index] if (arg === '--auto-remove') { args.autoRemove = true continue } if (arg === '--help' || arg === '-h') { args.help = true break } if (arg.startsWith('--file=')) { args.errors.push('--file expects space-separated values. Example: --file app billing') continue } if (arg === '--file') { const { values, nextIndex } = collectValues(index) if (validateList(values, '--file')) args.files.push(...values) index = nextIndex continue } if (arg.startsWith('--lang=')) { args.errors.push('--lang expects space-separated values. Example: --lang zh-Hans ja-JP') continue } if (arg === '--lang') { const { values, nextIndex } = collectValues(index) if (validateList(values, '--lang')) args.languages.push(...values) index = nextIndex continue } } return args } function printHelp() { console.log(`Usage: pnpm run check-i18n [options] Options: --file Check only specific files; provide space-separated names and repeat --file if needed --lang Check only specific locales; provide space-separated locales and repeat --lang if needed --auto-remove Remove extra keys automatically -h, --help Show help Examples: pnpm run check-i18n --file app billing --lang zh-Hans ja-JP pnpm run check-i18n --auto-remove `) } async function getKeysFromLanguage(language) { return new Promise((resolve, reject) => { const folderPath = path.resolve(__dirname, '../i18n', language) const allKeys = [] fs.readdir(folderPath, (err, files) => { if (err) { console.error('Error reading folder:', err) reject(err) return } // Filter only .json files const translationFiles = files.filter(file => /\.json$/.test(file)) translationFiles.forEach((file) => { const filePath = path.join(folderPath, file) const fileName = file.replace(/\.json$/, '') // Remove file extension const camelCaseFileName = fileName.replace(/[-_](.)/g, (_, c) => c.toUpperCase()) // Convert to camel case try { const content = fs.readFileSync(filePath, 'utf8') const translationObj = JSON.parse(content) if (!translationObj || typeof translationObj !== 'object') { console.error(`Error parsing file: ${filePath}`) reject(new Error(`Error parsing file: ${filePath}`)) return } // Flat structure: just get all keys directly const fileKeys = Object.keys(translationObj).map(key => `${camelCaseFileName}.${key}`) allKeys.push(...fileKeys) } catch (error) { console.error(`Error processing file ${filePath}:`, error.message) reject(error) } }) resolve(allKeys) }) }) } async function removeExtraKeysFromFile(language, fileName, extraKeys) { const filePath = path.resolve(__dirname, '../i18n', language, `${fileName}.json`) if (!fs.existsSync(filePath)) { console.log(`āš ļø File not found: ${filePath}`) return false } try { // Filter keys that belong to this file const camelCaseFileName = fileName.replace(/[-_](.)/g, (_, c) => c.toUpperCase()) const fileSpecificKeys = extraKeys .filter(key => key.startsWith(`${camelCaseFileName}.`)) .map(key => key.substring(camelCaseFileName.length + 1)) // Remove file prefix if (fileSpecificKeys.length === 0) return false console.log(`šŸ”„ Processing file: ${filePath}`) // Read and parse JSON const content = fs.readFileSync(filePath, 'utf8') const translationObj = JSON.parse(content) let modified = false // Remove each extra key (flat structure - direct property deletion) for (const keyToRemove of fileSpecificKeys) { if (keyToRemove in translationObj) { delete translationObj[keyToRemove] console.log(`šŸ—‘ļø Removed key: ${keyToRemove}`) modified = true } else { console.log(`āš ļø Could not find key: ${keyToRemove}`) } } if (modified) { // Write back to file const newContent = `${JSON.stringify(translationObj, null, 2)}\n` fs.writeFileSync(filePath, newContent) console.log(`šŸ’¾ Updated file: ${filePath}`) return true } return false } catch (error) { console.error(`Error processing file ${filePath}:`, error.message) return false } } // Add command line argument support const args = parseArgs(process.argv) const targetFiles = Array.from(new Set(args.files)) const targetLangs = Array.from(new Set(args.languages)) const autoRemove = args.autoRemove async function main() { const compareKeysCount = async () => { let hasDiff = false const allTargetKeys = await getKeysFromLanguage(targetLanguage) // Filter target keys by file if specified const camelTargetFiles = targetFiles.map(file => file.replace(/[-_](.)/g, (_, c) => c.toUpperCase())) const targetKeys = targetFiles.length ? allTargetKeys.filter(key => camelTargetFiles.some(file => key.startsWith(`${file}.`))) : allTargetKeys // Filter languages by target language if specified const languagesToProcess = targetLangs.length ? targetLangs : languages const allLanguagesKeys = await Promise.all(languagesToProcess.map(language => getKeysFromLanguage(language))) // Filter language keys by file if specified const languagesKeys = targetFiles.length ? allLanguagesKeys.map(keys => keys.filter(key => camelTargetFiles.some(file => key.startsWith(`${file}.`)))) : allLanguagesKeys const keysCount = languagesKeys.map(keys => keys.length) const targetKeysCount = targetKeys.length const comparison = languagesToProcess.reduce((result, language, index) => { const languageKeysCount = keysCount[index] const difference = targetKeysCount - languageKeysCount result[language] = difference return result }, {}) console.log(comparison) // Print missing keys and extra keys for (let index = 0; index < languagesToProcess.length; index++) { const language = languagesToProcess[index] const languageKeys = languagesKeys[index] const missingKeys = targetKeys.filter(key => !languageKeys.includes(key)) const extraKeys = languageKeys.filter(key => !targetKeys.includes(key)) console.log(`Missing keys in ${language}:`, missingKeys) if (missingKeys.length > 0) hasDiff = true // Show extra keys only when there are extra keys (negative difference) if (extraKeys.length > 0) { console.log(`Extra keys in ${language} (not in ${targetLanguage}):`, extraKeys) // Auto-remove extra keys if flag is set if (autoRemove) { console.log(`\nšŸ¤– Auto-removing extra keys from ${language}...`) // Get all translation files const i18nFolder = path.resolve(__dirname, '../i18n', language) const files = fs.readdirSync(i18nFolder) .filter(file => /\.json$/.test(file)) .map(file => file.replace(/\.json$/, '')) .filter(f => targetFiles.length === 0 || targetFiles.includes(f)) let totalRemoved = 0 for (const fileName of files) { const removed = await removeExtraKeysFromFile(language, fileName, extraKeys) if (removed) totalRemoved++ } console.log(`āœ… Auto-removal completed for ${language}. Modified ${totalRemoved} files.`) } else { hasDiff = true } } } return hasDiff } console.log('šŸš€ Starting check-i18n script...') if (targetFiles.length) console.log(`šŸ“ Checking files: ${targetFiles.join(', ')}`) if (targetLangs.length) console.log(`šŸŒ Checking languages: ${targetLangs.join(', ')}`) if (autoRemove) console.log('šŸ¤– Auto-remove mode: ENABLED') const hasDiff = await compareKeysCount() if (hasDiff) { console.error('\nāŒ i18n keys are not aligned. Fix issues above.') process.exitCode = 1 } else { console.log('\nāœ… All i18n files are in sync') } } async function bootstrap() { if (args.help) { printHelp() return } if (args.errors.length) { args.errors.forEach(message => console.error(`āŒ ${message}`)) printHelp() process.exit(1) return } const unknownLangs = targetLangs.filter(lang => !languages.includes(lang)) if (unknownLangs.length) { console.error(`āŒ Unsupported languages: ${unknownLangs.join(', ')}`) process.exit(1) return } await main() } bootstrap().catch((error) => { console.error('āŒ Unexpected error:', error.message) process.exit(1) })