From b2ccf997b5a7b8d437e29a58d64170f499c779d5 Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Mon, 28 Sep 2026 12:29:20 +0330 Subject: [PATCH] feat(i18n): enforce translation synchronization across all languages --- package.json | 1 + scripts/check-translations.js | 135 +++++++++++++++++ src/components/layout/ServerStatusDialog.tsx | 150 +++++++++++-------- src/features/dashboard/DashboardHome.tsx | 5 + src/locales/__tests__/locales.test.ts | 12 ++ src/locales/ar.json | 3 +- src/locales/en.json | 3 +- src/locales/fa.json | 3 +- 8 files changed, 248 insertions(+), 64 deletions(-) create mode 100644 scripts/check-translations.js create mode 100644 src/locales/__tests__/locales.test.ts diff --git a/package.json b/package.json index b818a50..72ac6ae 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "vite", "build": "tsc --noEmit && vite build", "typecheck": "tsc --noEmit", + "check:i18n": "node scripts/check-translations.js", "test": "vitest run", "lint": "eslint .", "preview": "vite preview" diff --git a/scripts/check-translations.js b/scripts/check-translations.js new file mode 100644 index 0000000..742bae8 --- /dev/null +++ b/scripts/check-translations.js @@ -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, 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) + } +} diff --git a/src/components/layout/ServerStatusDialog.tsx b/src/components/layout/ServerStatusDialog.tsx index 8a4d7cc..0f30b5c 100644 --- a/src/components/layout/ServerStatusDialog.tsx +++ b/src/components/layout/ServerStatusDialog.tsx @@ -5,6 +5,7 @@ import { DialogHeader, DialogTitle, DialogDescription, + DialogFooter, DialogClose, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' @@ -12,6 +13,7 @@ import { Badge } from '@/components/ui/badge' import { Ic } from '@/icons' import { useServerHealthStore } from '@/stores/server-health' import { useAuthStore } from '@/stores/auth' +import { cn } from '@/lib/utils' interface ServerStatusDialogProps { open: boolean @@ -33,53 +35,25 @@ export default function ServerStatusDialog({ open, onOpenChange }: ServerStatusD -
-
-
- -
-
- - وضعیت سیستم و سرور - - - خلاصه ارتباط سرور، پیکربندی API و اطلاعات حساب - -
+
+
+
- -
- - - - - +
+ + وضعیت سیستم و سرور + + + خلاصه ارتباط سرور، پیکربندی API و اطلاعات حساب +
@@ -100,22 +74,35 @@ export default function ServerStatusDialog({ open, onOpenChange }: ServerStatusD /> وضعیت سرور جنگو (Backend) - + + {health.status === 'connected' + ? 'آنلاین و متصل' : health.status === 'checking' - ? 'border-amber-500/30 bg-amber-500/10 text-amber-400' - : 'border-rose-500/30 bg-rose-500/10 text-rose-400' - } - > - {health.status === 'connected' - ? 'آنلاین و متصل' - : health.status === 'checking' - ? 'در حال بررسی...' - : 'قطع ارتباط'} - + ? 'در حال بررسی...' + : 'قطع ارتباط'} + + +
{health.message} @@ -186,6 +173,47 @@ export default function ServerStatusDialog({ open, onOpenChange }: ServerStatusD
+ + +
+ + + {health.status === 'connected' + ? 'ارتباط با سرور پایدار است' + : health.status === 'checking' + ? 'در حال آزمایش اتصال...' + : 'سرور در دسترس نیست'} + +
+ +
+ + + + +
+
) diff --git a/src/features/dashboard/DashboardHome.tsx b/src/features/dashboard/DashboardHome.tsx index 1362eef..8a40740 100644 --- a/src/features/dashboard/DashboardHome.tsx +++ b/src/features/dashboard/DashboardHome.tsx @@ -1,9 +1,11 @@ import { Ic } from '@/icons' import { Badge } from '@/components/ui/badge' import { useAuthStore } from '@/stores/auth' +import { useTranslation } from '@/i18n' export default function DashboardHome() { const user = useAuthStore((s) => s.user) + const { t } = useTranslation() return (
@@ -21,6 +23,9 @@ export default function DashboardHome() {

پنل مدیریت عقیله آماده اضافه کردن ویجت‌ها، جداول و ماژول‌های جدید شماست.

+

+ {t('common.testSyncNotice')} +

diff --git a/src/locales/__tests__/locales.test.ts b/src/locales/__tests__/locales.test.ts new file mode 100644 index 0000000..741dc20 --- /dev/null +++ b/src/locales/__tests__/locales.test.ts @@ -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([]) + } + }) +}) diff --git a/src/locales/ar.json b/src/locales/ar.json index baec031..95d05c2 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -64,7 +64,8 @@ "next": "التالي", "showing": "عرض", "to": "إلى", - "of": "من" + "of": "من", + "testSyncNotice": "هذه رسالة تجريبية لاختبار مزامنة اللغات." }, "billing": { "title": "إدارة الفواتير والمدفوعات", diff --git a/src/locales/en.json b/src/locales/en.json index 9d0e8ca..53685d2 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -64,7 +64,8 @@ "next": "Next", "showing": "Showing", "to": "to", - "of": "of" + "of": "of", + "testSyncNotice": "This is a test message for language synchronization verification." }, "billing": { "title": "Invoices & Billing Management", diff --git a/src/locales/fa.json b/src/locales/fa.json index 187120a..0cf4d53 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -64,7 +64,8 @@ "next": "بعدی", "showing": "نمایش", "to": "تا", - "of": "از" + "of": "از", + "testSyncNotice": "این یک پیام آزمایشی برای تست هماهنگی زبان‌هاست" }, "billing": { "title": "مدیریت صورت‌حساب‌ها و فاکتورها",