import { readFileSync, writeFileSync } from 'node:fs' import { basename, resolve } from 'node:path' import { globSync } from 'tinyglobby' import consola from 'consola' import { isArray, isObject, isString } from 'lodash-unified' import { localeRoot, normalizePath } from '@element-plus/build-utils' type TranslatePair = { [key: string]: string | string[] | TranslatePair } const langRoot = resolve(localeRoot, 'lang') const enFile = normalizePath(resolve(langRoot, 'en.ts')) const localePath = normalizePath(resolve(`${langRoot}/!(en).ts`)) const commentInfo = '// to be translated' const untranslatedRexExp = new RegExp( `\\w+:(\\s+|\\n\\s+).*,?\\s*${commentInfo}$`, 'gm' ) function parseLocaleFile(content: string): TranslatePair { try { const objectContent = content .replace(/^export\s+default\s+/, '') .replace(/;\s*$/, '') .replaceAll(untranslatedRexExp, '') return new Function(`return ${objectContent}`)() } catch (error) { consola.error('Failed to parse locale file:', error) return {} } } function mergeLocaleObjects( source: TranslatePair, target: TranslatePair, path = '' ): TranslatePair { const result: TranslatePair = {} Object.entries(source).forEach(([key, value]) => { const currentPath = path ? `${path}.${key}` : key if (isArray(value)) { if (isArray(target[key])) { result[key] = mergeLocaleArrays( value as Array, target[key] as Array, currentPath ) } else { result[key] = value.map((item, index) => { consola.log(`📝 Added new array: ${currentPath}[${index}]`) return `${item} ${commentInfo}` }) } } else if (isObject(value)) { if (isObject(target[key]) && !isArray(target[key])) { result[key] = mergeLocaleObjects( value as TranslatePair, target[key] as TranslatePair, currentPath ) } else { result[key] = addTranslationComments(value) } } else if (key in target) { result[key] = target[key] } else if (isString(value)) { result[key] = `${value} ${commentInfo}` consola.log(`📝 Added new field: ${currentPath}`) } }) Object.entries(target).forEach(([key, value]) => { if (!(key in source)) { result[key] = value consola.warn( `Found extra field in target: ${path ? `${path}.${key}` : key}` ) } }) return result } function mergeLocaleArrays( sourceArray: Array, targetArray: Array, path: string ): Array { const result: Array = [] sourceArray.forEach((sourceItem, index) => { if (index < targetArray.length) { result[index] = targetArray[index] } else { result[index] = `${sourceItem} ${commentInfo}` consola.log(`📝 Added new array item: ${path}[${index}]`) } }) if (targetArray.length > sourceArray.length) { for (let i = sourceArray.length; i < targetArray.length; i++) { result[i] = targetArray[i] consola.warn(`Found extra array item in target: ${path}[${i}]`) } } return result } function addTranslationComments(obj: TranslatePair): TranslatePair { return Object.entries(obj).reduce((all, [key, value]) => { if (isArray(value)) { all[key] = value.map((item) => `${item} ${commentInfo}`) } else if (isObject(value)) { all[key] = addTranslationComments(value) } else if (isString(value)) { all[key] = `${value} ${commentInfo}` } return all }, {} as TranslatePair) } function objectToTypescript(obj: TranslatePair, indent = 0): string { const spaces = ' '.repeat(indent) const items: string[] = [] Object.entries(obj).forEach(([key, value]) => { if (isArray(value)) { let needComment = false const arrayItems = value.map((item) => { if (item.includes(commentInfo)) { const [actualValue] = item.split(commentInfo) const escapedValue = actualValue.trim().replace(/'/g, "\\'") needComment = true return `'${escapedValue}'` } else { const escapedValue = item.replace(/'/g, "\\'") return `'${escapedValue}'` } }) items.push( `${spaces} ${key}: [${arrayItems.join(', ')}],${ needComment ? ` ${commentInfo}` : '' }` ) } else if (isObject(value)) { items.push( `${spaces} ${key}: {\n${objectToTypescript( value as TranslatePair, indent + 1 )}\n${spaces} },` ) } else if (isString(value)) { if (value.includes(commentInfo)) { const [actualValue] = value.split(commentInfo) const escapedValue = actualValue.trim().replace(/'/g, "\\'") items.push(`${spaces} ${key}: '${escapedValue}', ${commentInfo}`) } else { const escapedValue = value.replace(/'/g, "\\'") items.push(`${spaces} ${key}: '${escapedValue}',`) } } }) return items.join('\n') } function generateTypescriptFile(obj: TranslatePair): string { return `export default {\n${objectToTypescript(obj)}\n}\n` } function countKeys(obj: TranslatePair): number { return Object.values(obj).reduce((all, value) => { if (isObject(value) && !isArray(value)) { all += countKeys(value) } else { all++ } return all }, 0) } function main() { consola.start('Starting to synchronize content from English files...') const enContent = readFileSync(enFile, 'utf-8') const enObject = parseLocaleFile(enContent) const localeFiles = globSync(localePath) let totalUpdated = 0 let totalAdded = 0 consola.info(`Found ${localeFiles.length} locale files to process`) localeFiles.forEach((filePath) => { const fileName = basename(filePath) consola.start(`Processing ${fileName}...`) try { const content = readFileSync(filePath, 'utf-8') const targetObject = parseLocaleFile(content) const originalKeys = countKeys(targetObject.el as TranslatePair) targetObject.el = mergeLocaleObjects( enObject.el as TranslatePair, targetObject.el as TranslatePair ) const newKeys = countKeys(targetObject.el as TranslatePair) const addedKeys = newKeys - originalKeys if (addedKeys > 0) { const newContent = generateTypescriptFile(targetObject) writeFileSync(filePath, newContent, 'utf-8') consola.success( `Updated ${fileName} - ${addedKeys} fields waiting to be translated` ) totalUpdated++ totalAdded += addedKeys } else { consola.success(`${fileName} is up to date`) } } catch (error) { consola.error(`Failed to process ${fileName}:`, error) } }) consola.log(`🎉 Synchronization completed!`) consola.log( `📊 Updated ${totalUpdated} files - ${totalAdded} fields waiting to be translated` ) } main()