Update: 2026-07-25 18:05:37

This commit is contained in:
Hamza-Ayed
2026-07-25 18:05:37 +03:00
parent 61d6380861
commit 27369b8ac1
+171 -20
View File
@@ -402,7 +402,7 @@
'Completed trips (passenger)': 'الرحلات المكتملة (الراكب)',
'Captain phone': 'هاتف الكابتن',
'Captain completed / cancelled': 'الكابتن مكتملة / ملغاة',
'Pickup': 'نقطة الت Pickup',
'Pickup': 'نقطة الاستلام',
'Drop-off': 'نقطة النزول',
'Passenger fare': 'أجرة الراكب',
'Captain earning': 'أرباح الكابتن',
@@ -464,6 +464,157 @@
'Search rides by phone number': 'بحث في الرحلات برقم الهاتف',
'Search by captain ID, phone or email': 'بحث بمعرّف الكابتن أو البريد الإلكتروني أو الهاتف',
'Search by passenger ID, phone or email': 'بحث بمعرّف الراكب أو البريد الإلكتروني أو الهاتف',
// API error messages
' — the endpoint returned HTML, check the API base URL': ' — الـ API أرجع HTML، تحقق من عنوان الخادم',
'Session rejected by the server': 'الخادم رفض الجلسة',
'Request failed': 'فشل الطلب',
// Auth
'your WhatsApp': 'واتسابك',
'Verification code sent.': 'تم إرسال رمز التحقق.',
'No token issued.': 'لم يُصدر توكن.',
'Admin': 'المشرف',
'Welcome back,': 'مرحباً بعودتك،',
// Load everything
'No data — check diagnostics': 'لا توجد بيانات — تحقق من التشخيص',
'Every endpoint failed.': 'فشل كل الـ APIs.',
'Open "Session & Security" → Run diagnostics.': 'افتح "الجلسة والأمان" ← شغّل التشخيص.',
// Rides table
'Unknown': 'غير معروف',
'Unassigned': 'لم يُعيَّن',
'No rides found for': 'لم تُعثر على رحلات لـ',
'Search results for': 'نتائج البحث عن',
'No captain matches': 'لا يوجد كابتن يطابق',
'No passenger matches': 'لا يوجد راكب يطابق',
'Unnamed': 'بدون اسم',
// Captain/Passenger profile
'Captain #': 'كابتن #',
'Passenger #': 'راكب #',
'Loading profile…': 'جارٍ تحميل الملف الشخصي…',
'No profile returned for this record.': 'لم تُرجَع ملفات لهذا السجل.',
// Approvals
'Set admin': 'تعيين المشرف',
'to': 'إلى',
'approved': 'تم اعتماده',
'rejected': 'تم رفضه',
// Blacklist
'Enter the phone number of the account to remove.': 'أدخل رقم الهاتف المراد حذفه.',
'The retyped phone does not match — nothing was deleted.': 'رقم الهاتف المُعاد إدخاله غير متطابق — لم يُحذف شيء.',
'Removing a captain requires their account ID.': 'حذف الكابتن يتطلب معرّف حسابه.',
'Permanently delete': 'حذف نهائياً',
'and blacklist that number?': 'وإضافة الرقم للقائمة السوداء؟',
'The account is deleted from the database. This cannot be undone.': 'الحساب يُحذف من قاعدة البيانات. لا يمكن التراجع.',
'Deleting…': 'جارٍ الحذف…',
'Captain removed and blacklisted.': 'تم حذف الكابتن وإضافته للقائمة السوداء.',
'Passenger removed and blacklisted.': 'تم حذف الراكب وإضافته للقائمة السوداء.',
'Remove and blacklist': 'حذف و加入 القائمة السوداء',
'Delete and blacklist': 'حذف و加入 القائمة السوداء',
// Lift block
'Enter the blocked phone number.': 'أدخل رقم الهاتف المحظور.',
'They will be able to register again.': 'سيتمكن من التسجيل مرة أخرى.',
'Working…': 'جارٍ التنفيذ…',
'Block lifted.': 'تم رفع الحظر.',
'Lift block': 'رفع الحظر',
// Campaign
'Parameters changed since the preview': 'تغيّرت المعايير منذ المعاينة',
'preview again before launching': 'عاين مرة أخرى قبل الإطلاق',
'Launch this campaign in': 'إطلاق الحملة في',
'It creates a discount code valid for 7 days and pushes a notification to every passenger there.': 'ينشئ كود خصم صالحاً لمدة ٧ أيام ويرسل إشعاراً لكل راكب هناك.',
'This cannot be undone.': 'لا يمكن التراجع.',
'The AI found no opportunity — launching would still send.': 'لم يجد الذكاء الاصطناعي فرصة — الإطلاق سيرسل على أي حال.',
'Launch is now enabled.': 'الإطلاق متاح الآن.',
'Campaign dispatched.': 'تم إرسال الحملة.',
// App versions
'Version unchanged.': 'الإصدار لم يتغيّر.',
'Use digits separated by dots, e.g. 1.4.2': 'استخدم أرقاماً مفصولة بنقاط، مثال: 1.4.2',
'Users on an older build may be prompted or forced to update.': 'قد يُطلب من المستخدمين التحديث أو يُجبرون عليه.',
// Driver docs
'Loading documents…': 'جارٍ تحميل الوثائق…',
'Activate captain': 'تفعيل الكابتن',
'They will be able to accept rides immediately.': 'سيتمكّن من قبول الرحلات فوراً.',
'WARNING: no documents are on file for this captain.': 'تحذير: لا توجد وثائق مسجّلة لهذا الكابتن.',
'document(s) reviewed': 'وثيقة/وثائق تمت مراجعتها',
'Captain activated.': 'تم تفعيل الكابتن.',
// Staff
'Activate account': 'تفعيل الحساب',
'They will be able to sign in immediately.': 'سيتمكن من تسجيل الدخول فوراً.',
'Name and password are required.': 'الاسم وكلمة المرور مطلوبان.',
'Use a password of at least 8 characters.': 'استخدم كلمة مرور من ٨ أحرف على الأقل.',
'Only a super admin can create administrator accounts.': 'المدير العام فقط يمكنه إنشاء حسابات مشرفين.',
'ADMINISTRATOR': 'مدير',
'customer service': 'خدمة عملاء',
'Create a account for': 'إنشاء حساب لـ',
'Administrators can see and change platform data.': 'المشرفون يمكنهم رؤية وتعديل بيانات المنصة.',
'Customer service staff can view operational data.': 'موظفو خدمة العملاء يمكنهم رؤية البيانات التشغيلية.',
'Creating…': 'جارٍ الإنشاء…',
'Staff account created.': 'تم إنشاء حساب الموظف.',
'Could not create account:': 'تعذّر إنشاء الحساب:',
// Routes
'approve': 'اعتماد',
'reject': 'رفض',
'The route goes live and passengers can ride it.': 'المسار يصبح نشطاً ويمكن للركاب استخدامه.',
'The organisation will have to resubmit the route.': 'سيتعيّن على المؤسسة إعادة تقديم المسار.',
'Approve route': 'اعتماد المسار',
'Reject route': 'رفض المسار',
'from': 'من',
'Route': 'المسار',
'Route approved.': 'تم اعتماد المسار.',
'Route rejected.': 'تم رفض المسار.',
// Broadcast
'Broadcasting is restricted to super admins.': 'الإرسال مقتصر على المدير العام فقط.',
'every captain': 'كل كابتن',
'every passenger': 'كل راكب',
'Enter both a title and a message.': 'أدخل عنواناً ونصاً للرسالة.',
'Send this notification to': 'إرسال هذا الإشعار إلى',
'on the platform?': 'على المنصة؟',
'It is delivered immediately and cannot be recalled.': 'يُسلَّم فوراً ولا يمكن استرجاعه.',
'Sent to': 'أُرسل إلى',
'at': 'في',
'Notification delivered to': 'تم تسليم الإشعار إلى',
'Send failed:': 'فشل الإرسال:',
'Notification title': 'عنوان الإشعار',
'Message text': 'نص الرسالة',
// Tariff
'Tariff': 'التعرفة',
'row #': 'صف #',
'Only a super admin can change pricing.': 'المدير العام فقط يمكنه تغيير الأسعار.',
'Nothing changed on this tariff row.': 'لم يتغيّر شيء في هذا السطر.',
'Apply these pricing changes to': 'تطبيق تغييرات التسعير على',
'This takes effect immediately for passengers.': 'يسري فوراً للركاب.',
'Tariff updated and recorded in the audit log.': 'تم تحديث التعرفة وتسجيلها في سجل العمليات.',
'Update failed:': 'فشل التحديث:',
// Diagnostics
'Running…': 'جارٍ التشخيص…',
'Console build:': 'إصدار المنصة:',
'MISSING — not signed in': 'غير موجود — لم يُسجَّل الدخول',
'NETWORK FAILURE —': 'فشل الشبكة —',
'(blocked by CORS, DNS, mixed content, or the host is unreachable)': '(محظور بواسطة CORS أو DNS أو محتوى مختلط أو الخادم غير متاح)',
'Run diagnostics': 'تشغيل التشخيص',
'(empty response body)': '(استجابة فارغة)',
'Custom…': 'مخصص…',
// WhatsApp
'Sending messages is restricted to super admins.': 'إرسال الرسائل مقتصر على المدير العام فقط.',
'Enter both a recipient and a message.': 'أدخل المستلم والرسالة.',
'Send this WhatsApp message to': 'إرسال رسالة واتساب إلى',
'Message sent.': 'تم إرسال الرسالة.',
// Crypto tool
'This tool is restricted to super admins.': 'هذه الأداة مقتصرة على المدير العام فقط.',
'Enter both your admin phone and the value.': 'أدخل هاتف المشرف والقيمة المراد فك/تشفيرها.',
// API base
'API base set to': 'تم تغيير عنوان API إلى',
'Reloading data…': 'جارٍ إعادة تحميل البيانات…',
'Diagnostics report copied.': 'تم نسخ تقرير التشخيص.',
'Copy failed — select the text manually.': 'فشل النسخ — حدد النص يدوياً.',
// Charts
'No ride data': 'لا توجد بيانات رحلات',
'TOTAL RIDES': 'إجمالي الرحلات',
// Helpers
'unrated': 'بدون تقييم',
// Login error
'The server accepted the request but issued no code and no token.': 'قبل الخادم الطلب لكنه لم يُصدر رمزاً ولا توكن.',
'Enter the 3-digit code.': 'أدخل الرمز المكوّن من ٣ أرقام.',
'Verifying…': 'جارٍ التحقق…',
'Session ended: ': 'انتهت الجلسة: ',
'n/a': 'غير متاح',
};
const t = (key) => (lang === 'ar' ? (AR[key] || key) : key);
@@ -710,7 +861,7 @@
function handleApiError(err, context) {
console.error(`[${context}]`, err);
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
signOut(`Session ended: ${err.message}`);
signOut(t('Session ended: ') + err.message);
return true;
}
setConnection('error', t('Data unavailable'));
@@ -761,11 +912,11 @@
if (payload?.status === 'otp_required') {
pendingPhone = phone;
el.otpPhoneText.textContent = `Enter the 3-digit code sent to ${payload.phone || 'your WhatsApp'}.`;
el.otpPhoneText.textContent = t('Enter the 3-digit code sent to ') + (payload.phone || t('your WhatsApp')) + '.';
el.otpModal.classList.add('active');
el.otpInput.value = '';
el.otpInput.focus();
toast(payload.message || 'Verification code sent.', 'info');
toast(payload.message || t('Verification code sent.'), 'info');
} else if (payload?.jwt) {
// Trusted-device renewal path — no OTP required.
establishSession(payload);
@@ -786,11 +937,11 @@
async function onVerifyOtp() {
const otp = el.otpInput.value.trim();
if (otp.length < 3) {
toast('Enter the 3-digit code.', 'warning');
toast(t('Enter the 3-digit code.'), 'warning');
return;
}
busy(el.submitOtpBtn, true, 'Verifying…');
busy(el.submitOtpBtn, true, t('Verifying…'));
try {
const form = new FormData();
form.append('otp', otp);
@@ -798,7 +949,7 @@
form.append('aud', 'admin');
const payload = await api('/Admin/auth/verify_login.php', { method: 'POST', body: form, auth: false });
if (!payload?.jwt) throw new ApiError('No token issued.', 0);
if (!payload?.jwt) throw new ApiError(t('No token issued.'), 0);
el.otpModal.classList.remove('active');
establishSession(payload);
@@ -823,7 +974,7 @@
};
localStorage.setItem(SESSION_KEY, JSON.stringify(session));
showConsole();
toast(`Welcome back, ${session.name}.`, 'success');
toast(t('Welcome back,') + ' ' + session.name + '.', 'success');
loadEverything();
}
@@ -881,8 +1032,8 @@
el.lastUpdated.textContent = t('Updated') + ' ' + new Date().toLocaleTimeString();
} else if (session) {
setConnection('error', t('No data from API'));
el.lastUpdated.textContent = 'No data — check diagnostics';
toast('Every endpoint failed. Open “Session & Security” → Run diagnostics.', 'danger');
el.lastUpdated.textContent = t('No data — check diagnostics');
toast(t('Every endpoint failed.') + ' ' + t('Open “Session & Security” → Run diagnostics.'), 'danger');
}
});
}
@@ -1052,8 +1203,8 @@
el.ridesTableBody.innerHTML = rides.map((r) => `
<tr>
<td><strong>#${esc(r.id)}</strong></td>
<td>${esc(r.passenger_full_name || 'Unknown')}</td>
<td>${esc(r.driver_full_name || 'Unassigned')}</td>
<td>${esc(r.passenger_full_name || t('Unknown'))}</td>
<td>${esc(r.driver_full_name || t('Unassigned'))}</td>
<td class="route-cell">${routeCell(r)}</td>
<td><strong>${fmtMoney(rideFare(r))}</strong></td>
<td><span class="badge ${badgeClass(r.status)}">${esc(labelStatus(r.status))}</span></td>
@@ -1147,8 +1298,8 @@
const payload = await api('/Admin/AdminCaptain/getCaptainDetailsByEmailOrIDOrPhone.php', { params });
const rows = normaliseRows(payload);
renderDrivers(rows, rows.length);
el.driversMeta.textContent = `Search results for “${term}”`;
if (!rows.length) tableMessage(el.driversTableBody, 8, `No captain matches “${term}”.`);
el.driversMeta.textContent = t('Search results for') + ` “${term}”`;
if (!rows.length) tableMessage(el.driversTableBody, 8, t('No captain matches') + ` “${term}”.`);
} catch (err) {
if (!handleApiError(err, 'captain-lookup')) tableMessage(el.driversTableBody, 8, err.message, true);
}
@@ -1163,7 +1314,7 @@
const payload = await api('/Admin/getPassengerbyEmail.php', { params });
const rows = normaliseRows(payload);
renderPassengers(rows);
if (!rows.length) tableMessage(el.passengersTableBody, 8, `No passenger matches “${term}”.`);
if (!rows.length) tableMessage(el.passengersTableBody, 8, t('No passenger matches') + ` “${term}”.`);
} catch (err) {
if (!handleApiError(err, 'passenger-lookup')) tableMessage(el.passengersTableBody, 8, err.message, true);
}
@@ -1180,12 +1331,12 @@
// ── Profile drawers ──────────────────────────────────────────────────────
async function openCaptainProfile(id) {
openProfile(`Captain #${id}`, () =>
openProfile(t('Captain #') + id, () =>
api('/Admin/AdminCaptain/getCaptainDetailsById.php', { params: { driver_id: id } }));
}
async function openPassengerProfile(id) {
openProfile(`Passenger #${id}`, () =>
openProfile(t('Passenger #') + id, () =>
api('/Admin/getPassengerDetailsByPassengerID.php', { params: { passengerID: id } }));
}
@@ -1196,13 +1347,13 @@
<h3>${esc(title)}</h3>
<button class="btn-icon" onclick="closeModal()"><i class="ph ph-x"></i></button>
</div>
<div class="table-msg">Loading profile…</div>`;
<div class="table-msg">${t('Loading profile…')}</div>`;
$('detailsModal').classList.add('active');
try {
const payload = await fetcher();
const record = normaliseRows(payload)[0];
if (!record) throw new ApiError('No profile returned for this record.', 0);
if (!record) throw new ApiError(t('No profile returned for this record.'), 0);
const entries = Object.entries(record)
.filter(([k]) => !/token|password|fingerprint/i.test(k))
@@ -2585,7 +2736,7 @@
<div class="card">
<div class="card-header"><h3 class="card-title">${t('Add a staff account')}</h3></div>
<p class="card-note">
${lang === 'ar' ? 'يُنشئ حساب دخول لأدوات سير للمشرفين. اختر كلمة المرور مع العضو الجديد، أو دعهم يغيّروها عند أول تسجيل دخول —她是 مشفرة ولا يمكن قراءتها.' : 'Creates a login for the Siro admin tools. Choose the password with the new member present, or have them change it at first sign-in — it is stored hashed and cannot be read back.'}
${lang === 'ar' ? 'يُنشئ حساب دخول لأدوات سير للمشرفين. اختر كلمة المرور مع العضو الجديد، أو دعهم يغيّروها عند أول تسجيل دخول — هي مشفرة ولا يمكن قراءتها.' : 'Creates a login for the Siro admin tools. Choose the password with the new member present, or have them change it at first sign-in — it is stored hashed and cannot be read back.'}
</p>
<div class="tariff-grid">
<label class="tariff-field">