| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- const fs = require('node:fs')
- const path = require('node:path')
- const vm = require('node:vm')
- const transpile = require('typescript').transpile
- const magicast = require('magicast')
- const { parseModule, generateCode, loadFile } = magicast
- const bingTranslate = require('bing-translate-api')
- const { translate } = bingTranslate
- const data = require('./languages.json')
-
- const targetLanguage = 'en-US'
- const i18nFolder = '../i18n' // Path to i18n folder relative to this script
- // https://github.com/plainheart/bing-translate-api/blob/master/src/met/lang.json
- const languageKeyMap = data.languages.reduce((map, language) => {
- if (language.supported) {
- if (language.value === 'zh-Hans' || language.value === 'zh-Hant')
- map[language.value] = language.value
- else
- map[language.value] = language.value.split('-')[0]
- }
-
- return map
- }, {})
-
- async function translateMissingKeyDeeply(sourceObj, targetObject, toLanguage) {
- const skippedKeys = []
- const translatedKeys = []
-
- await Promise.all(Object.keys(sourceObj).map(async (key) => {
- if (targetObject[key] === undefined) {
- if (typeof sourceObj[key] === 'object') {
- targetObject[key] = {}
- const result = await translateMissingKeyDeeply(sourceObj[key], targetObject[key], toLanguage)
- skippedKeys.push(...result.skipped)
- translatedKeys.push(...result.translated)
- }
- else {
- try {
- const source = sourceObj[key]
- if (!source) {
- targetObject[key] = ''
- return
- }
-
- // Skip template literal placeholders
- if (source === 'TEMPLATE_LITERAL_PLACEHOLDER') {
- console.log(`⏭️ Skipping template literal key: "${key}"`)
- skippedKeys.push(`${key}: ${source}`)
- return
- }
-
- // Only skip obvious code patterns, not normal text with parentheses
- const codePatterns = [
- /\{\{.*\}\}/, // Template variables like {{key}}
- /\$\{.*\}/, // Template literals ${...}
- /<[^>]+>/, // HTML/XML tags
- /function\s*\(/, // Function definitions
- /=\s*\(/, // Assignment with function calls
- ]
-
- const isCodeLike = codePatterns.some(pattern => pattern.test(source))
- if (isCodeLike) {
- console.log(`⏭️ Skipping code-like content: "${source.substring(0, 50)}..."`)
- skippedKeys.push(`${key}: ${source}`)
- return
- }
-
- console.log(`🔄 Translating: "${source}" to ${toLanguage}`)
- const { translation } = await translate(sourceObj[key], null, languageKeyMap[toLanguage])
- targetObject[key] = translation
- translatedKeys.push(`${key}: ${translation}`)
- console.log(`✅ Translated: "${translation}"`)
- }
- catch (error) {
- console.error(`❌ Error translating "${sourceObj[key]}" to ${toLanguage}. Key: ${key}`, error.message)
- skippedKeys.push(`${key}: ${sourceObj[key]} (Error: ${error.message})`)
-
- // Add retry mechanism for network errors
- if (error.message.includes('network') || error.message.includes('timeout')) {
- console.log(`🔄 Retrying translation for key: ${key}`)
- try {
- await new Promise(resolve => setTimeout(resolve, 1000)) // Wait 1 second
- const { translation } = await translate(sourceObj[key], null, languageKeyMap[toLanguage])
- targetObject[key] = translation
- translatedKeys.push(`${key}: ${translation}`)
- console.log(`✅ Retry successful: "${translation}"`)
- }
- catch (retryError) {
- console.error(`❌ Retry failed for key ${key}:`, retryError.message)
- }
- }
- }
- }
- }
- else if (typeof sourceObj[key] === 'object') {
- targetObject[key] = targetObject[key] || {}
- const result = await translateMissingKeyDeeply(sourceObj[key], targetObject[key], toLanguage)
- skippedKeys.push(...result.skipped)
- translatedKeys.push(...result.translated)
- }
- }))
-
- return { skipped: skippedKeys, translated: translatedKeys }
- }
- async function autoGenTrans(fileName, toGenLanguage, isDryRun = false) {
- const fullKeyFilePath = path.resolve(__dirname, i18nFolder, targetLanguage, `${fileName}.ts`)
- const toGenLanguageFilePath = path.resolve(__dirname, i18nFolder, toGenLanguage, `${fileName}.ts`)
-
- try {
- const content = fs.readFileSync(fullKeyFilePath, 'utf8')
-
- // Temporarily replace template literals with regular strings for AST parsing
- // This allows us to process other keys while skipping problematic ones
- let processedContent = content
- const templateLiteralPattern = /(resolutionTooltip):\s*`([^`]*)`/g
- processedContent = processedContent.replace(templateLiteralPattern, (match, key, value) => {
- console.log(`⏭️ Temporarily replacing template literal for key: ${key}`)
- return `${key}: "TEMPLATE_LITERAL_PLACEHOLDER"`
- })
-
- // Create a safer module environment for vm
- const moduleExports = {}
- const context = {
- exports: moduleExports,
- module: { exports: moduleExports },
- require,
- console,
- __filename: fullKeyFilePath,
- __dirname: path.dirname(fullKeyFilePath),
- }
-
- // Use vm.runInNewContext instead of eval for better security
- vm.runInNewContext(transpile(processedContent), context)
-
- const fullKeyContent = moduleExports.default || moduleExports
-
- if (!fullKeyContent || typeof fullKeyContent !== 'object')
- throw new Error(`Failed to extract translation object from ${fullKeyFilePath}`)
-
- // if toGenLanguageFilePath is not exist, create it
- if (!fs.existsSync(toGenLanguageFilePath)) {
- fs.writeFileSync(toGenLanguageFilePath, `const translation = {
- }
-
- export default translation
- `)
- }
- // To keep object format and format it for magicast to work: const translation = { ... } => export default {...}
- const readContent = await loadFile(toGenLanguageFilePath)
- const { code: toGenContent } = generateCode(readContent)
-
- // Also handle template literals in target file content
- let processedToGenContent = toGenContent
- processedToGenContent = processedToGenContent.replace(templateLiteralPattern, (match, key, value) => {
- console.log(`⏭️ Temporarily replacing template literal in target file for key: ${key}`)
- return `${key}: "TEMPLATE_LITERAL_PLACEHOLDER"`
- })
- const mod = await parseModule(`export default ${processedToGenContent.replace('export default translation', '').replace('const translation = ', '')}`)
- const toGenOutPut = mod.exports.default
-
- console.log(`\n🌍 Processing ${fileName} for ${toGenLanguage}...`)
- const result = await translateMissingKeyDeeply(fullKeyContent, toGenOutPut, toGenLanguage)
-
- // Generate summary report
- console.log(`\n📊 Translation Summary for ${fileName} -> ${toGenLanguage}:`)
- console.log(` ✅ Translated: ${result.translated.length} keys`)
- console.log(` ⏭️ Skipped: ${result.skipped.length} keys`)
-
- if (result.skipped.length > 0) {
- console.log(`\n⚠️ Skipped keys in ${fileName} (${toGenLanguage}):`)
- result.skipped.slice(0, 5).forEach(item => console.log(` - ${item}`))
- if (result.skipped.length > 5)
- console.log(` ... and ${result.skipped.length - 5} more`)
- }
-
- const { code } = generateCode(mod)
- let res = `const translation =${code.replace('export default', '')}
-
- export default translation
- `.replace(/,\n\n/g, ',\n').replace('};', '}')
-
- // Restore original template literals by reading from the original target file if it exists
- if (fs.existsSync(toGenLanguageFilePath)) {
- const originalContent = fs.readFileSync(toGenLanguageFilePath, 'utf8')
- // Extract original template literal content for resolutionTooltip
- const originalMatch = originalContent.match(/(resolutionTooltip):\s*`([^`]*)`/s)
- if (originalMatch) {
- const [fullMatch, key, value] = originalMatch
- res = res.replace(
- `${key}: "TEMPLATE_LITERAL_PLACEHOLDER"`,
- `${key}: \`${value}\``,
- )
- console.log(`🔄 Restored original template literal for key: ${key}`)
- }
- }
-
- if (!isDryRun) {
- fs.writeFileSync(toGenLanguageFilePath, res)
- console.log(`💾 Saved translations to ${toGenLanguageFilePath}`)
- }
- else {
- console.log(`🔍 [DRY RUN] Would save translations to ${toGenLanguageFilePath}`)
- }
-
- return result
- }
- catch (error) {
- console.error(`Error processing file ${fullKeyFilePath}:`, error.message)
- throw error
- }
- }
-
- // Add command line argument support
- const isDryRun = process.argv.includes('--dry-run')
- const targetFiles = process.argv
- .filter(arg => arg.startsWith('--file='))
- .map(arg => arg.split('=')[1])
- const targetLang = process.argv.find(arg => arg.startsWith('--lang='))?.split('=')[1]
-
- // Rate limiting helper
- function delay(ms) {
- return new Promise(resolve => setTimeout(resolve, ms))
- }
-
- async function main() {
- console.log('🚀 Starting auto-gen-i18n script...')
- console.log(`📋 Mode: ${isDryRun ? 'DRY RUN (no files will be modified)' : 'LIVE MODE'}`)
-
- const files = fs
- .readdirSync(path.resolve(__dirname, i18nFolder, targetLanguage))
- .filter(file => /\.ts$/.test(file)) // Only process .ts files
- .map(file => file.replace(/\.ts$/, ''))
- // Removed app-debug exclusion, now only skip specific problematic keys
-
- // Filter by target files if specified
- const filesToProcess = targetFiles.length > 0 ? files.filter(f => targetFiles.includes(f)) : files
- const languagesToProcess = targetLang ? [targetLang] : Object.keys(languageKeyMap)
-
- console.log(`📁 Files to process: ${filesToProcess.join(', ')}`)
- console.log(`🌍 Languages to process: ${languagesToProcess.join(', ')}`)
-
- let totalTranslated = 0
- let totalSkipped = 0
- let totalErrors = 0
-
- // Process files sequentially to avoid API rate limits
- for (const file of filesToProcess) {
- console.log(`\n📄 Processing file: ${file}`)
-
- // Process languages with rate limiting
- for (const language of languagesToProcess) {
- try {
- const result = await autoGenTrans(file, language, isDryRun)
- totalTranslated += result.translated.length
- totalSkipped += result.skipped.length
-
- // Rate limiting: wait 500ms between language processing
- await delay(500)
- }
- catch (e) {
- console.error(`❌ Error translating ${file} to ${language}:`, e.message)
- totalErrors++
- }
- }
- }
-
- // Final summary
- console.log('\n🎉 Auto-translation completed!')
- console.log('📊 Final Summary:')
- console.log(` ✅ Total keys translated: ${totalTranslated}`)
- console.log(` ⏭️ Total keys skipped: ${totalSkipped}`)
- console.log(` ❌ Total errors: ${totalErrors}`)
-
- if (isDryRun)
- console.log('\n💡 This was a dry run. To actually translate, run without --dry-run flag.')
- }
-
- main()
|