8 changed files with 248 additions and 64 deletions
-
1package.json
-
135scripts/check-translations.js
-
88src/components/layout/ServerStatusDialog.tsx
-
5src/features/dashboard/DashboardHome.tsx
-
12src/locales/__tests__/locales.test.ts
-
3src/locales/ar.json
-
3src/locales/en.json
-
3src/locales/fa.json
@ -0,0 +1,135 @@ |
|||
#!/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) |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
import { describe, it, expect } from 'vitest' |
|||
import { validateTranslations } from '../../../scripts/check-translations.js' |
|||
|
|||
describe('Locale Translations Synchronization', () => { |
|||
it('should ensure fa.json, en.json, and ar.json have identical keys and no empty values', () => { |
|||
const result = validateTranslations() |
|||
expect(result.success).toBe(true) |
|||
if (!result.success) { |
|||
expect(result.errors).toEqual([]) |
|||
} |
|||
}) |
|||
}) |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue