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

chore: update `compare-translations.ts` logic (#1063)

Co-authored-by: Daniel Roe <daniel@roe.dev>

authored by

Joaquín Sánchez
Daniel Roe
and committed by
GitHub
d731543f f4e7f6f1

+189 -32
+37 -15
lunaria/prepare-json-files.ts
··· 31 31 })), 32 32 ] 33 33 34 - export async function prepareJsonFiles() { 34 + export async function prepareJsonFiles(): Promise<void> { 35 35 await fs.rm(destFolder, { recursive: true, force: true }) 36 36 await fs.mkdir(destFolder) 37 37 await Promise.all(currentLocales.map(l => mergeLocale(l))) 38 38 } 39 39 40 - async function loadJsonFile(name: string) { 41 - return JSON.parse(await fs.readFile(path.resolve(`${localesFolder}/${name}`), 'utf8')) 42 - } 40 + type NestedObject = Record<string, unknown> 43 41 44 - function getFileName(file: string | { path: string }): string { 45 - return typeof file === 'string' ? file : file.path 46 - } 47 - 48 - async function mergeLocale(locale: LocaleObject) { 42 + export async function mergeLocaleObject( 43 + locale: LocaleObject, 44 + options: { copy?: boolean } = {}, 45 + ): Promise<NestedObject | undefined> { 46 + const { copy = false } = options 49 47 const files = locale.files ?? [] 50 48 if (locale.file || files.length === 1) { 51 - const json = locale.file ?? (files[0] ? getFileName(files[0]) : undefined) 52 - if (!json) return 53 - await fs.cp(path.resolve(`${localesFolder}/${json}`), path.resolve(`${destFolder}/${json}`)) 54 - return 49 + const json = 50 + (locale.file ? getFileName(locale.file) : undefined) ?? 51 + (files[0] ? getFileName(files[0]) : undefined) 52 + if (!json) return undefined 53 + if (copy) { 54 + await fs.cp(path.resolve(`${localesFolder}/${json}`), path.resolve(`${destFolder}/${json}`)) 55 + return undefined 56 + } 57 + 58 + return await loadJsonFile<NestedObject>(json) 55 59 } 56 60 57 61 const firstFile = files[0] 58 - if (!firstFile) return 59 - const source = await loadJsonFile(getFileName(firstFile)) 62 + if (!firstFile) return undefined 63 + const source = await loadJsonFile<NestedObject>(getFileName(firstFile)) 60 64 let currentSource: unknown 61 65 for (let i = 1; i < files.length; i++) { 62 66 const file = files[i] ··· 65 69 deepCopy(currentSource, source) 66 70 } 67 71 72 + return source 73 + } 74 + 75 + async function loadJsonFile<T = unknown>(name: string): Promise<T> { 76 + return JSON.parse(await fs.readFile(path.resolve(`${localesFolder}/${name}`), 'utf8')) 77 + } 78 + 79 + function getFileName(file: string | { path: string }): string { 80 + return typeof file === 'string' ? file : file.path 81 + } 82 + 83 + async function mergeLocale(locale: LocaleObject): Promise<void> { 84 + const source = await mergeLocaleObject(locale, { copy: true }) 85 + if (!source) { 86 + return 87 + } 88 + 68 89 await fs.writeFile( 69 90 path.resolve(`${destFolder}/${locale.code}.json`), 70 91 JSON.stringify(source, null, 2), 92 + 'utf-8', 71 93 ) 72 94 }
+152 -17
scripts/compare-translations.ts
··· 1 1 /* eslint-disable no-console */ 2 - import process from 'node:process' 2 + import type { LocaleObject } from '@nuxtjs/i18n' 3 + import * as process from 'node:process' 3 4 import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' 4 - import { join } from 'node:path' 5 + import { basename, join } from 'node:path' 5 6 import { fileURLToPath } from 'node:url' 7 + import { countryLocaleVariants, currentLocales } from '../config/i18n.ts' 8 + import { mergeLocaleObject } from '../lunaria/prepare-json-files.ts' 6 9 import { COLORS } from './utils.ts' 7 10 8 11 const LOCALES_DIRECTORY = fileURLToPath(new URL('../i18n/locales', import.meta.url)) 9 12 const REFERENCE_FILE_NAME = 'en.json' 10 13 11 14 type NestedObject = { [key: string]: unknown } 15 + interface LocaleInfo { 16 + filePath: string 17 + locale: string 18 + lang: string 19 + country?: string 20 + forCountry?: boolean 21 + mergeLocale?: boolean 22 + } 12 23 13 - const loadJson = (filePath: string): NestedObject => { 24 + const countries = new Map<string, Map<string, LocaleInfo>>() 25 + const availableLocales = new Map<string, LocaleObject>() 26 + 27 + function extractLocalInfo(filePath: string): LocaleInfo { 28 + const locale = basename(filePath, '.json') 29 + const [lang, country] = locale.split('-') 30 + return { filePath, locale, lang, country } 31 + } 32 + 33 + function createVariantInfo( 34 + code: string, 35 + options: { forCountry: boolean; mergeLocale: boolean }, 36 + ): LocaleInfo { 37 + const [lang, country] = code.split('-') 38 + return { filePath: '', locale: code, lang, country, ...options } 39 + } 40 + 41 + const populateLocaleCountries = (): void => { 42 + for (const lang of Object.keys(countryLocaleVariants)) { 43 + const variants = countryLocaleVariants[lang] 44 + for (const variant of variants) { 45 + if (!countries.has(lang)) { 46 + countries.set(lang, new Map()) 47 + } 48 + if (variant.country) { 49 + countries 50 + .get(lang)! 51 + .set(lang, createVariantInfo(lang, { forCountry: true, mergeLocale: false })) 52 + countries 53 + .get(lang)! 54 + .set( 55 + variant.code, 56 + createVariantInfo(variant.code, { forCountry: true, mergeLocale: true }), 57 + ) 58 + } else { 59 + countries 60 + .get(lang)! 61 + .set( 62 + variant.code, 63 + createVariantInfo(variant.code, { forCountry: false, mergeLocale: true }), 64 + ) 65 + } 66 + } 67 + } 68 + 69 + for (const localeData of currentLocales) { 70 + availableLocales.set(localeData.code, localeData) 71 + } 72 + } 73 + 74 + /** 75 + * We use ISO 639-1 for the language and ISO 3166-1 for the country (e.g. es-ES), we're preventing here: 76 + * using the language as the JSON file name when there is no country variant. 77 + * 78 + * For example, `az.json` is wrong, should be `az-AZ.json` since it is not included at `countryLocaleVariants`. 79 + */ 80 + const checkCountryVariant = (localeInfo: LocaleInfo): void => { 81 + const { locale, lang, country } = localeInfo 82 + const countryVariant = countries.get(lang) 83 + if (countryVariant) { 84 + if (country) { 85 + const found = countryVariant.get(locale) 86 + if (!found) { 87 + console.error( 88 + `${COLORS.red}Error: Invalid locale file "${locale}", it should be included at "countryLocaleVariants" in config/i18n.ts"${COLORS.reset}`, 89 + ) 90 + process.exit(1) 91 + } 92 + localeInfo.forCountry = found.forCountry 93 + localeInfo.mergeLocale = found.mergeLocale 94 + } else { 95 + localeInfo.forCountry = false 96 + localeInfo.mergeLocale = false 97 + } 98 + } else { 99 + if (!country) { 100 + console.error( 101 + `${COLORS.red}Error: Invalid locale file "${locale}", it should be included at "countryLocaleVariants" in config/i18n.ts, or change the name to include country name "${lang}-<country-name>"${COLORS.reset}`, 102 + ) 103 + process.exit(1) 104 + } 105 + } 106 + } 107 + 108 + const checkJsonName = (filePath: string): LocaleInfo => { 109 + const info = extractLocalInfo(filePath) 110 + checkCountryVariant(info) 111 + return info 112 + } 113 + 114 + const loadJson = async ({ filePath, mergeLocale, locale }: LocaleInfo): Promise<NestedObject> => { 14 115 if (!existsSync(filePath)) { 15 116 console.error(`${COLORS.red}Error: File not found at ${filePath}${COLORS.reset}`) 16 117 process.exit(1) 17 118 } 18 - return JSON.parse(readFileSync(filePath, 'utf-8')) as NestedObject 119 + 120 + if (!mergeLocale) { 121 + return JSON.parse(readFileSync(filePath, 'utf-8')) as NestedObject 122 + } 123 + 124 + const localeObject = availableLocales.get(locale) 125 + if (!localeObject) { 126 + console.error( 127 + `${COLORS.red}Error: Locale "${locale}" not found in currentLocales${COLORS.reset}`, 128 + ) 129 + process.exit(1) 130 + } 131 + const merged = await mergeLocaleObject(localeObject) 132 + if (!merged) { 133 + console.error(`${COLORS.red}Error: Failed to merge locale "${locale}"${COLORS.reset}`) 134 + process.exit(1) 135 + } 136 + return merged 19 137 } 20 138 21 139 type SyncStats = { ··· 43 161 44 162 if (isNested(refValue)) { 45 163 const nextTarget = isNested(target[key]) ? target[key] : {} 46 - result[key] = syncLocaleData(refValue, nextTarget, stats, fix, propertyPath) 164 + const data = syncLocaleData(refValue, nextTarget, stats, fix, propertyPath) 165 + // When fixing, empty objects won't occur since missing keys get placeholders. 166 + // Without --fix, keep empty objects to preserve structural parity with the reference. 167 + if (fix && Object.keys(data).length === 0) { 168 + delete result[key] 169 + } else { 170 + result[key] = data 171 + } 47 172 } else { 48 173 stats.referenceKeys.push(propertyPath) 49 174 ··· 83 208 keys.forEach(key => console.log(` - ${key}`)) 84 209 } 85 210 86 - const processLocale = ( 211 + const processLocale = async ( 87 212 localeFile: string, 88 213 referenceContent: NestedObject, 89 214 fix = false, 90 - ): SyncStats => { 215 + ): Promise<SyncStats> => { 91 216 const filePath = join(LOCALES_DIRECTORY, localeFile) 92 - const targetContent = loadJson(filePath) 217 + const localeInfo = checkJsonName(filePath) 218 + const targetContent = await loadJson(localeInfo) 93 219 94 220 const stats: SyncStats = { 95 221 missing: [], ··· 107 233 return stats 108 234 } 109 235 110 - const runSingleLocale = (locale: string, referenceContent: NestedObject, fix = false): void => { 236 + const runSingleLocale = async ( 237 + locale: string, 238 + referenceContent: NestedObject, 239 + fix = false, 240 + ): Promise<void> => { 111 241 const localeFile = locale.endsWith('.json') ? locale : `${locale}.json` 112 242 const filePath = join(LOCALES_DIRECTORY, localeFile) 113 243 ··· 116 246 process.exit(1) 117 247 } 118 248 119 - const { missing, extra, referenceKeys } = processLocale(localeFile, referenceContent, fix) 249 + const { missing, extra, referenceKeys } = await processLocale(localeFile, referenceContent, fix) 120 250 121 251 console.log( 122 252 `${COLORS.cyan}=== Missing keys for ${localeFile}${fix ? ' (with --fix)' : ''} ===${COLORS.reset}`, ··· 144 274 console.log('') 145 275 } 146 276 147 - const runAllLocales = (referenceContent: NestedObject, fix = false): void => { 277 + const runAllLocales = async (referenceContent: NestedObject, fix = false): Promise<void> => { 148 278 const localeFiles = readdirSync(LOCALES_DIRECTORY).filter( 149 279 file => file.endsWith('.json') && file !== REFERENCE_FILE_NAME, 150 280 ) ··· 156 286 let totalAdded = 0 157 287 158 288 for (const localeFile of localeFiles) { 159 - const stats = processLocale(localeFile, referenceContent, fix) 289 + const stats = await processLocale(localeFile, referenceContent, fix) 160 290 results.push({ 161 291 file: localeFile, 162 292 ...stats, ··· 224 354 console.log('') 225 355 } 226 356 227 - const run = (): void => { 357 + const run = async (): Promise<void> => { 358 + populateLocaleCountries() 228 359 const referenceFilePath = join(LOCALES_DIRECTORY, REFERENCE_FILE_NAME) 229 - const referenceContent = loadJson(referenceFilePath) 360 + const referenceContent = await loadJson({ 361 + filePath: referenceFilePath, 362 + locale: 'en', 363 + lang: 'en', 364 + }) 230 365 231 366 const args = process.argv.slice(2) 232 367 const fix = args.includes('--fix') ··· 234 369 235 370 if (targetLocale) { 236 371 // Single locale mode 237 - runSingleLocale(targetLocale, referenceContent, fix) 372 + await runSingleLocale(targetLocale, referenceContent, fix) 238 373 } else { 239 374 // All locales mode: check all and remove extraneous keys 240 - runAllLocales(referenceContent, fix) 375 + await runAllLocales(referenceContent, fix) 241 376 } 242 377 } 243 378 244 - run() 379 + await run()