mirror of
https://github.com/langgenius/dify.git
synced 2026-04-05 10:25:48 +08:00
chore: i18n namespace refactor in package.json and add missing translations (#30324)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -17,8 +17,7 @@ web/i18n
|
||||
└── ...
|
||||
|
||||
web/i18n-config
|
||||
├── auto-gen-i18n.js
|
||||
├── check-i18n.js
|
||||
├── language.ts
|
||||
├── i18next-config.ts
|
||||
└── ...
|
||||
```
|
||||
@@ -159,10 +158,10 @@ We have a list of languages that we support in the `languages.ts` file. But some
|
||||
|
||||
## Utility scripts
|
||||
|
||||
- Auto-fill translations: `pnpm run auto-gen-i18n --file app common --lang zh-Hans ja-JP [--dry-run]`
|
||||
- Auto-fill translations: `pnpm run i18n:gen --file app common --lang zh-Hans ja-JP [--dry-run]`
|
||||
- Use space-separated values; repeat `--file` / `--lang` as needed. Defaults to all en-US files and all supported locales except en-US.
|
||||
- Protects placeholders (`{{var}}`, `${var}`, `<tag>`) before translation and restores them after.
|
||||
- Check missing/extra keys: `pnpm run check-i18n --file app billing --lang zh-Hans [--auto-remove]`
|
||||
- Check missing/extra keys: `pnpm run i18n:check --file app billing --lang zh-Hans [--auto-remove]`
|
||||
- Use space-separated values; repeat `--file` / `--lang` as needed. Returns non-zero on missing/extra keys; `--auto-remove` deletes extra keys automatically.
|
||||
|
||||
Workflows: `.github/workflows/translate-i18n-base-on-english.yml` auto-runs the translation generator on `web/i18n/en-US/*.json` changes to main. `check-i18n` is a manual script (not run in CI).
|
||||
Workflows: `.github/workflows/translate-i18n-base-on-english.yml` auto-runs the translation generator on `web/i18n/en-US/*.json` changes to main. `i18n:check` is a manual script (not run in CI).
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { translate } from 'bing-translate-api'
|
||||
import data from './languages'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
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
|
||||
}, {})
|
||||
|
||||
const supportedLanguages = Object.keys(languageKeyMap)
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
files: [],
|
||||
languages: [],
|
||||
isDryRun: 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 === '--dry-run') {
|
||||
args.isDryRun = 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 auto-gen-i18n [options]
|
||||
|
||||
Options:
|
||||
--file <name...> Process only specific files; provide space-separated names and repeat --file if needed
|
||||
--lang <locale> Process only specific locales; provide space-separated locales and repeat --lang if needed (default: all supported except en-US)
|
||||
--dry-run Preview changes without writing files
|
||||
-h, --help Show help
|
||||
|
||||
Examples:
|
||||
pnpm run auto-gen-i18n --file app common --lang zh-Hans ja-JP
|
||||
pnpm run auto-gen-i18n --dry-run
|
||||
`)
|
||||
}
|
||||
|
||||
function protectPlaceholders(text) {
|
||||
const placeholders = []
|
||||
let safeText = text
|
||||
const patterns = [
|
||||
/\{\{[^{}]+\}\}/g, // mustache
|
||||
/\$\{[^{}]+\}/g, // template expressions
|
||||
/<[^>]+>/g, // html-like tags
|
||||
]
|
||||
|
||||
patterns.forEach((pattern) => {
|
||||
safeText = safeText.replace(pattern, (match) => {
|
||||
const token = `__PH_${placeholders.length}__`
|
||||
placeholders.push({ token, value: match })
|
||||
return token
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
safeText,
|
||||
restore(translated) {
|
||||
return placeholders.reduce((result, { token, value }) => result.replace(new RegExp(token, 'g'), value), translated)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function translateText(source, toLanguage) {
|
||||
if (typeof source !== 'string')
|
||||
return { value: source, skipped: false }
|
||||
|
||||
const trimmed = source.trim()
|
||||
if (!trimmed)
|
||||
return { value: source, skipped: false }
|
||||
|
||||
const { safeText, restore } = protectPlaceholders(source)
|
||||
|
||||
try {
|
||||
const { translation } = await translate(safeText, null, languageKeyMap[toLanguage])
|
||||
return { value: restore(translation), skipped: false }
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`❌ Error translating to ${toLanguage}:`, error.message)
|
||||
return { value: source, skipped: true, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
async function translateMissingKeys(sourceObj, targetObject, toLanguage) {
|
||||
const skippedKeys = []
|
||||
const translatedKeys = []
|
||||
|
||||
for (const key of Object.keys(sourceObj)) {
|
||||
const sourceValue = sourceObj[key]
|
||||
const targetValue = targetObject[key]
|
||||
|
||||
// Skip if target already has this key
|
||||
if (targetValue !== undefined)
|
||||
continue
|
||||
|
||||
const translationResult = await translateText(sourceValue, toLanguage)
|
||||
targetObject[key] = translationResult.value ?? ''
|
||||
if (translationResult.skipped)
|
||||
skippedKeys.push(`${key}: ${sourceValue}`)
|
||||
else
|
||||
translatedKeys.push(key)
|
||||
}
|
||||
|
||||
return { skipped: skippedKeys, translated: translatedKeys }
|
||||
}
|
||||
async function autoGenTrans(fileName, toGenLanguage, isDryRun = false) {
|
||||
const fullKeyFilePath = path.resolve(__dirname, i18nFolder, targetLanguage, `${fileName}.json`)
|
||||
const toGenLanguageFilePath = path.resolve(__dirname, i18nFolder, toGenLanguage, `${fileName}.json`)
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(fullKeyFilePath, 'utf8')
|
||||
const fullKeyContent = JSON.parse(content)
|
||||
|
||||
if (!fullKeyContent || typeof fullKeyContent !== 'object')
|
||||
throw new Error(`Failed to extract translation object from ${fullKeyFilePath}`)
|
||||
|
||||
// if toGenLanguageFilePath does not exist, create it with empty object
|
||||
let toGenOutPut = {}
|
||||
if (fs.existsSync(toGenLanguageFilePath)) {
|
||||
const existingContent = fs.readFileSync(toGenLanguageFilePath, 'utf8')
|
||||
toGenOutPut = JSON.parse(existingContent)
|
||||
}
|
||||
|
||||
console.log(`\n🌍 Processing ${fileName} for ${toGenLanguage}...`)
|
||||
const result = await translateMissingKeys(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 res = `${JSON.stringify(toGenOutPut, null, 2)}\n`
|
||||
|
||||
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 args = parseArgs(process.argv)
|
||||
const isDryRun = args.isDryRun
|
||||
const targetFiles = args.files
|
||||
const targetLangs = args.languages
|
||||
|
||||
// Rate limiting helper
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (args.help) {
|
||||
printHelp()
|
||||
return
|
||||
}
|
||||
|
||||
if (args.errors.length) {
|
||||
args.errors.forEach(message => console.error(`❌ ${message}`))
|
||||
printHelp()
|
||||
process.exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🚀 Starting auto-gen-i18n script...')
|
||||
console.log(`📋 Mode: ${isDryRun ? 'DRY RUN (no files will be modified)' : 'LIVE MODE'}`)
|
||||
|
||||
const filesInEn = fs
|
||||
.readdirSync(path.resolve(__dirname, i18nFolder, targetLanguage))
|
||||
.filter(file => /\.json$/.test(file)) // Only process .json files
|
||||
.map(file => file.replace(/\.json$/, ''))
|
||||
|
||||
// Filter by target files if specified
|
||||
const filesToProcess = targetFiles.length > 0 ? filesInEn.filter(f => targetFiles.includes(f)) : filesInEn
|
||||
const languagesToProcess = Array.from(new Set((targetLangs.length > 0 ? targetLangs : supportedLanguages)
|
||||
.filter(lang => lang !== targetLanguage)))
|
||||
|
||||
const unknownLangs = languagesToProcess.filter(lang => !languageKeyMap[lang])
|
||||
if (unknownLangs.length) {
|
||||
console.error(`❌ Unsupported languages: ${unknownLangs.join(', ')}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!filesToProcess.length) {
|
||||
console.log('ℹ️ No files to process based on provided arguments')
|
||||
return
|
||||
}
|
||||
|
||||
if (!languagesToProcess.length) {
|
||||
console.log('ℹ️ No languages to process (did you only specify en-US?)')
|
||||
return
|
||||
}
|
||||
|
||||
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.')
|
||||
|
||||
if (totalErrors > 0)
|
||||
process.exitCode = 1
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('❌ Unexpected error:', error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,334 +0,0 @@
|
||||
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 <name...> Check only specific files; provide space-separated names and repeat --file if needed
|
||||
--lang <locale> 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)
|
||||
})
|
||||
Reference in New Issue
Block a user