#!/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, 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) } }