[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

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

fix: simplify translation sync logic and maintain original order (#627)

authored by

TAKAHASHI Shuuji and committed by
GitHub
ac82a5ce cefc172b

+109 -136
+109 -136
scripts/compare-translations.ts
··· 18 18 19 19 type NestedObject = { [key: string]: unknown } 20 20 21 - const flattenObject = (obj: NestedObject, prefix = ''): Record<string, unknown> => { 22 - return Object.keys(obj).reduce<Record<string, unknown>>((acc, key) => { 23 - const propertyPath = prefix ? `${prefix}.${key}` : key 24 - const value = obj[key] 25 - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { 26 - Object.assign(acc, flattenObject(value as NestedObject, propertyPath)) 27 - } else { 28 - acc[propertyPath] = value 29 - } 30 - return acc 31 - }, {}) 32 - } 33 - 34 21 const loadJson = (filePath: string): NestedObject => { 35 22 if (!existsSync(filePath)) { 36 23 console.error(`${COLORS.red}Error: File not found at ${filePath}${COLORS.reset}`) ··· 39 26 return JSON.parse(readFileSync(filePath, 'utf-8')) as NestedObject 40 27 } 41 28 42 - const addMissingKeys = ( 43 - obj: NestedObject, 44 - keysToAdd: string[], 45 - referenceFlat: Record<string, unknown>, 46 - ): NestedObject => { 47 - const result: NestedObject = { ...obj } 48 - 49 - for (const keyPath of keysToAdd) { 50 - const parts = keyPath.split('.') 51 - let current = result 52 - 53 - for (let i = 0; i < parts.length - 1; i++) { 54 - const part = parts[i]! 55 - if (!(part in current) || typeof current[part] !== 'object') { 56 - current[part] = {} 57 - } 58 - current = current[part] as NestedObject 59 - } 29 + type SyncStats = { 30 + missing: string[] 31 + extra: string[] 32 + referenceKeys: string[] 33 + } 60 34 61 - const lastPart = parts[parts.length - 1]! 62 - if (!(lastPart in current)) { 63 - const enValue = referenceFlat[keyPath] 64 - current[lastPart] = `EN TEXT TO REPLACE: ${enValue}` 65 - } 66 - } 35 + // Check if value is a non-null object and not array 36 + const isNested = (val: unknown): val is NestedObject => 37 + val !== null && typeof val === 'object' && !Array.isArray(val) 67 38 68 - return result 69 - } 70 - 71 - const removeKeysFromObject = (obj: NestedObject, keysToRemove: string[]): NestedObject => { 39 + const syncLocaleData = ( 40 + reference: NestedObject, 41 + target: NestedObject, 42 + stats: SyncStats, 43 + fix: boolean, 44 + prefix = '', 45 + ): NestedObject => { 72 46 const result: NestedObject = {} 73 47 74 - for (const key of Object.keys(obj)) { 75 - const value = obj[key] 48 + for (const key of Object.keys(reference)) { 49 + const propertyPath = prefix ? `${prefix}.${key}` : key 50 + const refValue = reference[key] 76 51 77 - // Check if this key or any nested path starting with this key should be removed 78 - const shouldRemoveKey = keysToRemove.some(k => k === key || k.startsWith(`${key}.`)) 79 - const hasNestedRemovals = keysToRemove.some(k => k.startsWith(`${key}.`)) 52 + if (isNested(refValue)) { 53 + const nextTarget = isNested(target[key]) ? target[key] : {} 54 + result[key] = syncLocaleData(refValue, nextTarget, stats, fix, propertyPath) 55 + } else { 56 + stats.referenceKeys.push(propertyPath) 80 57 81 - if (keysToRemove.includes(key)) { 82 - // Skip this key entirely 83 - continue 58 + if (key in target) { 59 + result[key] = target[key] 60 + } else { 61 + stats.missing.push(propertyPath) 62 + if (fix) { 63 + result[key] = `EN TEXT TO REPLACE: ${refValue}` 64 + } 65 + } 84 66 } 67 + } 85 68 86 - if (typeof value === 'object' && value !== null && !Array.isArray(value) && hasNestedRemovals) { 87 - // Recursively process nested objects 88 - const nestedKeysToRemove = keysToRemove 89 - .filter(k => k.startsWith(`${key}.`)) 90 - .map(k => k.slice(key.length + 1)) 91 - const cleaned = removeKeysFromObject(value as NestedObject, nestedKeysToRemove) 92 - // Only add if there are remaining keys 93 - if (Object.keys(cleaned).length > 0) { 94 - result[key] = cleaned 95 - } 96 - } else if (!shouldRemoveKey || hasNestedRemovals) { 97 - result[key] = value 69 + for (const key of Object.keys(target)) { 70 + const propertyPath = prefix ? `${prefix}.${key}` : key 71 + if (!(key in reference)) { 72 + stats.extra.push(propertyPath) 98 73 } 99 74 } 100 75 ··· 118 93 119 94 const processLocale = ( 120 95 localeFile: string, 121 - referenceKeys: string[], 122 - referenceFlat: Record<string, unknown>, 96 + referenceContent: NestedObject, 123 97 fix = false, 124 - ): { missing: string[]; removed: string[]; added: string[] } => { 98 + ): SyncStats => { 125 99 const filePath = join(LOCALES_DIRECTORY, localeFile) 126 - let content = loadJson(filePath) 127 - const flattenedKeys = Object.keys(flattenObject(content)) 128 - 129 - const missingKeys = referenceKeys.filter(key => !flattenedKeys.includes(key)) 130 - const extraneousKeys = flattenedKeys.filter(key => !referenceKeys.includes(key)) 131 - 132 - let modified = false 100 + const targetContent = loadJson(filePath) 133 101 134 - if (extraneousKeys.length > 0) { 135 - content = removeKeysFromObject(content, extraneousKeys) 136 - modified = true 102 + const stats: SyncStats = { 103 + missing: [], 104 + extra: [], 105 + referenceKeys: [], 137 106 } 138 107 139 - if (fix && missingKeys.length > 0) { 140 - content = addMissingKeys(content, missingKeys, referenceFlat) 141 - modified = true 142 - } 108 + const newContent = syncLocaleData(referenceContent, targetContent, stats, fix) 143 109 144 - if (modified) { 145 - writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n', 'utf-8') 110 + // Write if there are removals (always) or we are in fix mode 111 + if (stats.extra.length > 0 || fix) { 112 + writeFileSync(filePath, JSON.stringify(newContent, null, 2) + '\n', 'utf-8') 146 113 } 147 114 148 - return { missing: missingKeys, removed: extraneousKeys, added: fix ? missingKeys : [] } 115 + return stats 149 116 } 150 117 151 - const runSingleLocale = ( 152 - locale: string, 153 - referenceKeys: string[], 154 - referenceFlat: Record<string, unknown>, 155 - fix = false, 156 - ): void => { 118 + const runSingleLocale = (locale: string, referenceContent: NestedObject, fix = false): void => { 157 119 const localeFile = locale.endsWith('.json') ? locale : `${locale}.json` 158 120 const filePath = join(LOCALES_DIRECTORY, localeFile) 159 121 ··· 162 124 process.exit(1) 163 125 } 164 126 165 - let content = loadJson(filePath) 166 - const flattenedKeys = Object.keys(flattenObject(content)) 167 - const missingKeys = referenceKeys.filter(key => !flattenedKeys.includes(key)) 127 + const { missing, extra, referenceKeys } = processLocale(localeFile, referenceContent, fix) 168 128 169 129 console.log( 170 130 `${COLORS.cyan}=== Missing keys for ${localeFile}${fix ? ' (with --fix)' : ''} ===${COLORS.reset}`, 171 131 ) 172 132 console.log(`Reference: ${REFERENCE_FILE_NAME} (${referenceKeys.length} keys)`) 173 - console.log(`Target: ${localeFile} (${flattenedKeys.length} keys)`) 174 133 175 - if (missingKeys.length === 0) { 176 - console.log(`\n${COLORS.green}No missing keys!${COLORS.reset}\n`) 177 - } else if (fix) { 178 - content = addMissingKeys(content, missingKeys, referenceFlat) 179 - writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n', 'utf-8') 180 - console.log( 181 - `\n${COLORS.green}Added ${missingKeys.length} missing key(s) with EN placeholder:${COLORS.reset}`, 182 - ) 183 - missingKeys.forEach(key => console.log(` - ${key}`)) 184 - console.log('') 134 + if (missing.length > 0) { 135 + if (fix) { 136 + console.log( 137 + `\n${COLORS.green}Added ${missing.length} missing key(s) with EN placeholder:${COLORS.reset}`, 138 + ) 139 + missing.forEach(key => console.log(` - ${key}`)) 140 + } else { 141 + console.log(`\n${COLORS.yellow}Missing ${missing.length} key(s):${COLORS.reset}`) 142 + missing.forEach(key => console.log(` - ${key}`)) 143 + } 185 144 } else { 186 - console.log(`\n${COLORS.yellow}Missing ${missingKeys.length} key(s):${COLORS.reset}`) 187 - missingKeys.forEach(key => console.log(` - ${key}`)) 188 - console.log('') 145 + console.log(`\n${COLORS.green}No missing keys!${COLORS.reset}`) 189 146 } 147 + 148 + if (extra.length > 0) { 149 + console.log(`\n${COLORS.magenta}Removed ${extra.length} extra key(s):${COLORS.reset}`) 150 + extra.forEach(key => console.log(` - ${key}`)) 151 + } 152 + console.log('') 190 153 } 191 154 192 - const runAllLocales = ( 193 - referenceKeys: string[], 194 - referenceFlat: Record<string, unknown>, 195 - fix = false, 196 - ): void => { 155 + const runAllLocales = (referenceContent: NestedObject, fix = false): void => { 197 156 const localeFiles = readdirSync(LOCALES_DIRECTORY).filter( 198 157 file => file.endsWith('.json') && file !== REFERENCE_FILE_NAME, 199 158 ) 200 159 201 - console.log(`${COLORS.cyan}=== Translation Audit${fix ? ' (with --fix)' : ''} ===${COLORS.reset}`) 202 - console.log(`Reference: ${REFERENCE_FILE_NAME} (${referenceKeys.length} keys)`) 203 - console.log(`Checking ${localeFiles.length} locale(s)...`) 160 + const results: (SyncStats & { file: string })[] = [] 204 161 205 162 let totalMissing = 0 206 163 let totalRemoved = 0 207 164 let totalAdded = 0 208 165 209 166 for (const localeFile of localeFiles) { 210 - const { missing, removed, added } = processLocale(localeFile, referenceKeys, referenceFlat, fix) 167 + const stats = processLocale(localeFile, referenceContent, fix) 168 + results.push({ 169 + file: localeFile, 170 + ...stats, 171 + }) 211 172 212 - if (missing.length > 0 || removed.length > 0) { 213 - console.log(`\n${COLORS.cyan}--- ${localeFile} ---${COLORS.reset}`) 173 + if (fix) { 174 + if (stats.missing.length > 0) totalAdded += stats.missing.length 175 + } else { 176 + if (stats.missing.length > 0) totalMissing += stats.missing.length 177 + } 178 + if (stats.extra.length > 0) totalRemoved += stats.extra.length 179 + } 180 + 181 + const referenceKeysCount = results.length > 0 ? results[0]!.referenceKeys.length : 0 182 + 183 + console.log(`${COLORS.cyan}=== Translation Audit${fix ? ' (with --fix)' : ''} ===${COLORS.reset}`) 184 + console.log(`Reference: ${REFERENCE_FILE_NAME} (${referenceKeysCount} keys)`) 185 + console.log(`Checking ${localeFiles.length} locale(s)...`) 214 186 215 - if (added.length > 0) { 216 - logSection('ADDED MISSING KEYS (with EN placeholder)', added, COLORS.green, '', '') 217 - totalAdded += added.length 218 - } else if (missing.length > 0) { 219 - logSection( 220 - 'MISSING KEYS (in en.json but not in this locale)', 221 - missing, 222 - COLORS.yellow, 223 - '', 224 - '', 225 - ) 226 - totalMissing += missing.length 187 + for (const res of results) { 188 + if (res.missing.length > 0 || res.extra.length > 0) { 189 + console.log(`\n${COLORS.cyan}--- ${res.file} ---${COLORS.reset}`) 190 + 191 + if (res.missing.length > 0) { 192 + if (fix) { 193 + logSection('ADDED MISSING KEYS (with EN placeholder)', res.missing, COLORS.green, '', '') 194 + } else { 195 + logSection( 196 + 'MISSING KEYS (in en.json but not in this locale)', 197 + res.missing, 198 + COLORS.yellow, 199 + '', 200 + '', 201 + ) 202 + } 227 203 } 228 204 229 - if (removed.length > 0) { 205 + if (res.extra.length > 0) { 230 206 logSection( 231 - 'REMOVED EXTRANEOUS KEYS (were in this locale but not in en.json)', 232 - removed, 207 + 'REMOVED EXTRA KEYS (were in this locale but not in en.json)', 208 + res.extra, 233 209 COLORS.magenta, 234 210 '', 235 211 '', 236 212 ) 237 - totalRemoved += removed.length 238 213 } 239 214 } 240 215 } ··· 249 224 console.log(`${COLORS.yellow} Missing keys across all locales: ${totalMissing}${COLORS.reset}`) 250 225 } 251 226 if (totalRemoved > 0) { 252 - console.log(`${COLORS.magenta} Removed extraneous keys: ${totalRemoved}${COLORS.reset}`) 227 + console.log(`${COLORS.magenta} Removed extra keys: ${totalRemoved}${COLORS.reset}`) 253 228 } 254 229 if (totalMissing === 0 && totalRemoved === 0 && totalAdded === 0) { 255 230 console.log(`${COLORS.green} All locales are in sync!${COLORS.reset}`) ··· 260 235 const run = (): void => { 261 236 const referenceFilePath = join(LOCALES_DIRECTORY, REFERENCE_FILE_NAME) 262 237 const referenceContent = loadJson(referenceFilePath) 263 - const referenceFlat = flattenObject(referenceContent) 264 - const referenceKeys = Object.keys(referenceFlat) 265 238 266 239 const args = process.argv.slice(2) 267 240 const fix = args.includes('--fix') ··· 269 242 270 243 if (targetLocale) { 271 244 // Single locale mode 272 - runSingleLocale(targetLocale, referenceKeys, referenceFlat, fix) 245 + runSingleLocale(targetLocale, referenceContent, fix) 273 246 } else { 274 247 // All locales mode: check all and remove extraneous keys 275 - runAllLocales(referenceKeys, referenceFlat, fix) 248 + runAllLocales(referenceContent, fix) 276 249 } 277 250 } 278 251