You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

135 lines
4.1 KiB

#!/usr/bin/env node
/**
* Translation Synchronization Checker for aqila_panel.
* Validates that fa.json, en.json, and ar.json:
* 1. Exist and have valid JSON syntax.
* 2. Contain identical keys (nested dot-notation paths).
* 3. Have no null, undefined, or empty/whitespace values.
*/
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const LOCALES_DIR = path.resolve(__dirname, '../src/locales')
const TARGET_LANGS = ['fa', 'en', 'ar']
function loadJson(filePath) {
try {
const raw = fs.readFileSync(filePath, 'utf-8')
return JSON.parse(raw)
} catch (err) {
console.error(`❌ Failed to read or parse JSON file: ${filePath}`)
console.error(err.message)
process.exit(1)
}
}
/**
* Recursively flattens an object to dot-notation paths and validates values.
* Returns { keys: Map<string, string>, emptyKeys: string[] }
*/
function extractKeysAndValidate(obj, prefix = '') {
const keys = new Map()
const emptyKeys = []
for (const [key, value] of Object.entries(obj)) {
const fullPath = prefix ? `${prefix}.${key}` : key
if (value === null || value === undefined) {
emptyKeys.push(fullPath)
keys.set(fullPath, '')
} else if (typeof value === 'object' && !Array.isArray(value)) {
const child = extractKeysAndValidate(value, fullPath)
for (const [childKey, childVal] of child.keys.entries()) {
keys.set(childKey, childVal)
}
emptyKeys.push(...child.emptyKeys)
} else if (typeof value === 'string') {
if (value.trim() === '') {
emptyKeys.push(fullPath)
}
keys.set(fullPath, value)
} else {
keys.set(fullPath, String(value))
}
}
return { keys, emptyKeys }
}
export function validateTranslations() {
console.log('🔍 Checking translation synchronization for aqila_panel locales...')
console.log(`📁 Directory: ${LOCALES_DIR}\n`)
const data = {}
const extracted = {}
for (const lang of TARGET_LANGS) {
const filePath = path.join(LOCALES_DIR, `${lang}.json`)
if (!fs.existsSync(filePath)) {
console.error(`❌ Missing locale file: ${filePath}`)
return { success: false, errors: [`Missing locale file: ${filePath}`] }
}
data[lang] = loadJson(filePath)
extracted[lang] = extractKeysAndValidate(data[lang])
}
const allKeyPaths = new Set()
for (const lang of TARGET_LANGS) {
for (const key of extracted[lang].keys.keys()) {
allKeyPaths.add(key)
}
}
const errors = []
// Check 1: Empty values
for (const lang of TARGET_LANGS) {
if (extracted[lang].emptyKeys.length > 0) {
errors.push(
`[${lang}.json] Contains ${extracted[lang].emptyKeys.length} empty value(s):\n - ` +
extracted[lang].emptyKeys.join('\n - ')
)
}
}
// Check 2: Missing keys across languages
for (const key of allKeyPaths) {
const missingIn = TARGET_LANGS.filter((lang) => !extracted[lang].keys.has(key))
if (missingIn.length > 0) {
errors.push(
`Key "${key}" is missing in: [${missingIn.map((l) => `${l}.json`).join(', ')}]`
)
}
}
// Check 3: Key counts
const counts = TARGET_LANGS.map((lang) => `${lang}.json: ${extracted[lang].keys.size} keys`).join(' | ')
if (errors.length > 0) {
console.error('❌ Translation synchronization check FAILED!\n')
console.error(`📊 Key counts: ${counts}\n`)
console.error('🚨 Details:')
errors.forEach((err, idx) => console.error(`${idx + 1}. ${err}`))
console.error('\n⚠️ Please ensure all keys exist and have non-empty values in fa.json, en.json, and ar.json.\n')
return { success: false, errors }
}
console.log('✅ Translation synchronization check PASSED!')
console.log(`📊 Total keys verified: ${allKeyPaths.size} keys`)
console.log(` ${counts}\n`)
return { success: true, count: allKeyPaths.size }
}
// Run if called directly from CLI
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
const result = validateTranslations()
if (!result.success) {
process.exit(1)
}
}