feat: add Arabic translations and fix frontend i18n gaps
- Add backend/locale/ar_SA/LC_MESSAGES/django.po with Arabic (ar-sa) translations
for all 62 user-facing error/validation strings across accounts, bookings,
payments, and notifications apps; compile to django.mo
- Add common.loading and salon.unknownStaff keys to both ar-sa.json and en.json
- ProtectedRoute: replace hardcoded "Loading..." with t("common.loading")
- BookPage, SalonDetailPage: replace `Staff ${s.id}` fallback with
t("salon.unknownStaff", { id: s.id })
- BookingsPage: pass getActiveLocale() to toLocaleString so date/time
format matches the active app language
All 35 backend tests and 7 frontend tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,251 @@
|
|||||||
|
# Arabic (Saudi Arabia) translations for Salon booking platform.
|
||||||
|
# Copyright (C) 2026
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: 1.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-03-02 00:45+0300\n"
|
||||||
|
"PO-Revision-Date: 2026-03-02 00:45+0300\n"
|
||||||
|
"Last-Translator: Claude\n"
|
||||||
|
"Language-Team: Arabic (Saudi Arabia)\n"
|
||||||
|
"Language: ar_SA\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:26
|
||||||
|
msgid "Too many OTP requests. Try again later."
|
||||||
|
msgstr "طلبات رمز التحقق كثيرة جداً. حاول مرة أخرى لاحقاً."
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:32
|
||||||
|
msgid "Please wait before requesting another code."
|
||||||
|
msgstr "يرجى الانتظار قبل طلب رمز آخر."
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:71
|
||||||
|
msgid "Twilio credentials are not configured"
|
||||||
|
msgstr "لم يتم تكوين بيانات اعتماد Twilio"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:85
|
||||||
|
msgid "Twilio WhatsApp sender is not configured"
|
||||||
|
msgstr "لم يتم تكوين مُرسِل WhatsApp لـ Twilio"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:100
|
||||||
|
msgid "Unifonic credentials are not configured"
|
||||||
|
msgstr "لم يتم تكوين بيانات اعتماد Unifonic"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:104
|
||||||
|
msgid "Unifonic SMS adapter not implemented yet"
|
||||||
|
msgstr "محول SMS لـ Unifonic لم يتم تنفيذه بعد"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:109
|
||||||
|
msgid "Unifonic WhatsApp sender is not configured"
|
||||||
|
msgstr "لم يتم تكوين مُرسِل WhatsApp لـ Unifonic"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:110
|
||||||
|
msgid "Unifonic WhatsApp adapter not implemented yet"
|
||||||
|
msgstr "محول WhatsApp لـ Unifonic لم يتم تنفيذه بعد"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:126
|
||||||
|
msgid "Authentica API key is not configured"
|
||||||
|
msgstr "لم يتم تكوين مفتاح API لـ Authentica"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:149 apps/accounts/services/otp.py:162
|
||||||
|
msgid "Authentica request failed"
|
||||||
|
msgstr "فشل طلب Authentica"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:159
|
||||||
|
#, python-format
|
||||||
|
msgid "Authentica request failed: %(status)s %(body)s"
|
||||||
|
msgstr "فشل طلب Authentica: %(status)s %(body)s"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:168 apps/accounts/services/otp.py:276
|
||||||
|
msgid "Unsupported OTP channel"
|
||||||
|
msgstr "قناة رمز التحقق غير مدعومة"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:179
|
||||||
|
#, python-format
|
||||||
|
msgid "Authentica verify failed: %(response)s"
|
||||||
|
msgstr "فشل التحقق بـ Authentica: %(response)s"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:184
|
||||||
|
msgid "Authentica sender name is not configured"
|
||||||
|
msgstr "لم يتم تكوين اسم مُرسِل Authentica"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:195
|
||||||
|
msgid "Authentica WhatsApp messaging is not supported"
|
||||||
|
msgstr "مراسلة WhatsApp غير مدعومة لـ Authentica"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:209
|
||||||
|
#, python-format
|
||||||
|
msgid "Unknown OTP provider: %(provider)s"
|
||||||
|
msgstr "مزود رمز التحقق غير معروف: %(provider)s"
|
||||||
|
|
||||||
|
#: apps/accounts/services/otp.py:256
|
||||||
|
#, python-format
|
||||||
|
msgid "Your verification code is %(code)s. It expires in %(minutes)s minutes."
|
||||||
|
msgstr "رمز التحقق الخاص بك هو %(code)s. ينتهي في %(minutes)s دقيقة."
|
||||||
|
|
||||||
|
#: apps/accounts/services/phone.py:8
|
||||||
|
msgid "Phone number is required"
|
||||||
|
msgstr "رقم الهاتف مطلوب"
|
||||||
|
|
||||||
|
#: apps/accounts/services/phone.py:17
|
||||||
|
msgid "Invalid phone number format"
|
||||||
|
msgstr "تنسيق رقم الهاتف غير صالح"
|
||||||
|
|
||||||
|
#: apps/accounts/services/phone.py:28
|
||||||
|
msgid "Phone number must be in E.164 format or a valid Saudi mobile"
|
||||||
|
msgstr "يجب أن يكون رقم الهاتف بصيغة E.164 أو رقم جوال سعودي صالح"
|
||||||
|
|
||||||
|
#: apps/accounts/views.py:75 apps/accounts/views.py:138
|
||||||
|
msgid "Invalid or expired code"
|
||||||
|
msgstr "الرمز غير صالح أو منتهي الصلاحية"
|
||||||
|
|
||||||
|
#: apps/accounts/views.py:82
|
||||||
|
msgid "Phone verified"
|
||||||
|
msgstr "تم التحقق من رقم الهاتف"
|
||||||
|
|
||||||
|
#: apps/accounts/views.py:99
|
||||||
|
msgid "Email already in use."
|
||||||
|
msgstr "البريد الإلكتروني مستخدم بالفعل."
|
||||||
|
|
||||||
|
#: apps/accounts/views.py:142
|
||||||
|
msgid "User not found"
|
||||||
|
msgstr "المستخدم غير موجود"
|
||||||
|
|
||||||
|
#: apps/accounts/views.py:164
|
||||||
|
msgid "Social login not configured yet. Add OAuth provider config."
|
||||||
|
msgstr "لم يتم تكوين تسجيل الدخول الاجتماعي بعد. أضف إعداد مزود OAuth."
|
||||||
|
|
||||||
|
#: apps/bookings/serializers.py:54
|
||||||
|
msgid "Only staff or managers can confirm bookings."
|
||||||
|
msgstr "يمكن للموظفين والمدراء فقط تأكيد الحجوزات."
|
||||||
|
|
||||||
|
#: apps/bookings/serializers.py:56
|
||||||
|
msgid "Only staff or managers can complete bookings."
|
||||||
|
msgstr "يمكن للموظفين والمدراء فقط إكمال الحجوزات."
|
||||||
|
|
||||||
|
#: apps/bookings/serializers.py:58
|
||||||
|
msgid "You are not allowed to cancel this booking."
|
||||||
|
msgstr "لا يُسمح لك بإلغاء هذا الحجز."
|
||||||
|
|
||||||
|
#: apps/bookings/serializers.py:101 apps/bookings/services.py:49
|
||||||
|
msgid "Booking overlaps an existing appointment"
|
||||||
|
msgstr "يتداخل الحجز مع موعد قائم"
|
||||||
|
|
||||||
|
#: apps/bookings/services.py:13
|
||||||
|
msgid "Staff is required for booking"
|
||||||
|
msgstr "يجب تحديد موظف للحجز"
|
||||||
|
|
||||||
|
#: apps/bookings/services.py:16
|
||||||
|
msgid "Selected staff does not belong to this salon"
|
||||||
|
msgstr "الموظف المختار لا ينتمي إلى هذا الصالون"
|
||||||
|
|
||||||
|
#: apps/bookings/services.py:19
|
||||||
|
msgid "End time must be after start time"
|
||||||
|
msgstr "يجب أن يكون وقت الانتهاء بعد وقت البدء"
|
||||||
|
|
||||||
|
#: apps/bookings/services.py:23
|
||||||
|
msgid "End time must match service duration"
|
||||||
|
msgstr "يجب أن يتطابق وقت الانتهاء مع مدة الخدمة"
|
||||||
|
|
||||||
|
#: apps/bookings/services.py:40
|
||||||
|
msgid "Booking is outside staff availability"
|
||||||
|
msgstr "الحجز خارج أوقات توفر الموظف"
|
||||||
|
|
||||||
|
#: apps/notifications/services.py:31
|
||||||
|
#, python-format
|
||||||
|
msgid "Unknown notification provider: %(provider)s"
|
||||||
|
msgstr "مزود الإشعارات غير معروف: %(provider)s"
|
||||||
|
|
||||||
|
#: apps/notifications/services.py:47
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"Your booking request is received for %(service)s at %(salon)s on %(start)s."
|
||||||
|
msgstr "تم استلام طلب حجزك لخدمة %(service)s في %(salon)s بتاريخ %(start)s."
|
||||||
|
|
||||||
|
#: apps/notifications/services.py:55
|
||||||
|
#, python-format
|
||||||
|
msgid "Your booking is confirmed for %(service)s at %(salon)s on %(start)s."
|
||||||
|
msgstr "تم تأكيد حجزك لخدمة %(service)s في %(salon)s بتاريخ %(start)s."
|
||||||
|
|
||||||
|
#: apps/notifications/services.py:63
|
||||||
|
#, python-format
|
||||||
|
msgid "Your booking was cancelled for %(service)s at %(salon)s on %(start)s."
|
||||||
|
msgstr "تم إلغاء حجزك لخدمة %(service)s في %(salon)s بتاريخ %(start)s."
|
||||||
|
|
||||||
|
#: apps/notifications/services.py:70
|
||||||
|
#, python-format
|
||||||
|
msgid "Booking update for %(service)s at %(salon)s on %(start)s."
|
||||||
|
msgstr "تحديث الحجز لخدمة %(service)s في %(salon)s بتاريخ %(start)s."
|
||||||
|
|
||||||
|
#: apps/notifications/services.py:85
|
||||||
|
msgid "Unsupported notification channel"
|
||||||
|
msgstr "قناة الإشعارات غير مدعومة"
|
||||||
|
|
||||||
|
#: apps/payments/serializers.py:49
|
||||||
|
msgid "Booking not found"
|
||||||
|
msgstr "الحجز غير موجود"
|
||||||
|
|
||||||
|
#: apps/payments/serializers.py:56 apps/payments/services/payments.py:79
|
||||||
|
msgid "Provider integration not implemented"
|
||||||
|
msgstr "تكامل المزود غير مُنفَّذ"
|
||||||
|
|
||||||
|
#: apps/payments/serializers.py:58
|
||||||
|
msgid "Payment source is required"
|
||||||
|
msgstr "مصدر الدفع مطلوب"
|
||||||
|
|
||||||
|
#: apps/payments/serializers.py:61
|
||||||
|
msgid "Payment source type is required"
|
||||||
|
msgstr "نوع مصدر الدفع مطلوب"
|
||||||
|
|
||||||
|
#: apps/payments/serializers.py:64
|
||||||
|
msgid "Card data must not be sent to the backend; use frontend tokenization"
|
||||||
|
msgstr "لا يجب إرسال بيانات البطاقة إلى الخادم؛ استخدم التحويل إلى رمز في الواجهة الأمامية"
|
||||||
|
|
||||||
|
#: apps/payments/serializers.py:67
|
||||||
|
msgid "Callback URL is required for token payments"
|
||||||
|
msgstr "رابط الاستجابة مطلوب لمدفوعات الرمز"
|
||||||
|
|
||||||
|
#: apps/payments/services/payments.py:84
|
||||||
|
msgid "Idempotency key already used"
|
||||||
|
msgstr "مفتاح الإيدمبوتنسي مستخدم بالفعل"
|
||||||
|
|
||||||
|
#: apps/payments/services/payments.py:89
|
||||||
|
msgid "Unsupported payment source type"
|
||||||
|
msgstr "نوع مصدر الدفع غير مدعوم"
|
||||||
|
|
||||||
|
#: apps/payments/services/payments.py:130
|
||||||
|
#: apps/payments/services/payments.py:141
|
||||||
|
msgid "Payment provider error"
|
||||||
|
msgstr "خطأ في مزود الدفع"
|
||||||
|
|
||||||
|
#: apps/payments/views.py:50
|
||||||
|
msgid "Not allowed"
|
||||||
|
msgstr "غير مسموح"
|
||||||
|
|
||||||
|
#: apps/payments/views.py:70
|
||||||
|
msgid "Webhook secret not configured"
|
||||||
|
msgstr "لم يتم تكوين رمز الـ webhook"
|
||||||
|
|
||||||
|
#: apps/payments/views.py:73
|
||||||
|
msgid "Invalid webhook signature"
|
||||||
|
msgstr "توقيع الـ webhook غير صالح"
|
||||||
|
|
||||||
|
#: apps/payments/views.py:79
|
||||||
|
msgid "Missing payment reference"
|
||||||
|
msgstr "مرجع الدفع مفقود"
|
||||||
|
|
||||||
|
#: apps/payments/views.py:84
|
||||||
|
msgid "Payment not found"
|
||||||
|
msgstr "لم يتم العثور على الدفعة"
|
||||||
|
|
||||||
|
#: apps/payments/views.py:88
|
||||||
|
msgid "Event ignored"
|
||||||
|
msgstr "تم تجاهل الحدث"
|
||||||
|
|
||||||
|
#: apps/payments/views.py:89
|
||||||
|
msgid "Webhook processed"
|
||||||
|
msgstr "تمت معالجة الـ webhook"
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import { Navigate, useLocation } from "react-router-dom";
|
import { Navigate, useLocation } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
|
||||||
export default function ProtectedRoute({ children }) {
|
export default function ProtectedRoute({ children }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { isAuthenticated, loading } = useAuth();
|
const { isAuthenticated, loading } = useAuth();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="auth-loading">
|
<div className="auth-loading">
|
||||||
<p>Loading...</p>
|
<p>{t("common.loading")}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,14 @@
|
|||||||
"phoneUnavailable": "الهاتف غير متوفر",
|
"phoneUnavailable": "الهاتف غير متوفر",
|
||||||
"viewDetails": "عرض التفاصيل والحجز"
|
"viewDetails": "عرض التفاصيل والحجز"
|
||||||
},
|
},
|
||||||
"nav":{"home":"الرئيسية","book":"احجز","pay":"ادفع","profile":"الحساب","bookings":"حجوزاتي","login":"تسجيل الدخول","logout":"تسجيل الخروج"},"book":{"title":"احجزي خدمة","placeholder":"تدفق الحجز قريباً.","cta":"احجزي الآن","selectSalon":"اختاري صالوناً من الصفحة الرئيسية للحجز.","service":"الخدمة","staff":"الفريق","date":"التاريخ","time":"الوقت","notes":"ملاحظات","notesPlaceholder":"ملاحظات اختيارية","selectService":"اختاري الخدمة","selectStaff":"اختاري الفني","submit":"تأكيد الحجز","submitting":"جاري الحجز...","errors":{"fillAll":"يرجى تعبئة جميع الحقول.","invalidTime":"تاريخ أو وقت غير صالح.","generic":"فشل الحجز."}},"salon":{"services":"الخدمات","staff":"الفريق"},"profile":{"title":"الحساب","placeholder":"الملف والحجوزات قريباً.","myBookings":"حجوزاتي","noContact":"لا توجد معلومات اتصال"},"bookings":{"title":"حجوزاتي","subtitle":"مواعيدك القادمة والسابقة.","empty":"لا توجد حجوزات بعد.","pay":"ادفع الآن"},"paymentReturn":{"title":"حالة الدفع","success":"تم الدفع بنجاح","successMessage":"تم إتمام الدفع. ستستلمين تأكيداً عبر SMS أو واتساب.","checkStatus":"تحققي من بريدك أو الصالون لحالة الدفع.","reference":"المرجع: {{id}}","viewBookings":"عرض حجوزاتي"},"auth":{"title":"تسجيل الدخول","subtitle":"أدخلي رقم جوالك لاستلام رمز التحقق.","phone":"رقم الجوال","channel":"الإرسال عبر","sms":"رسالة نصية","whatsapp":"واتساب","sendCode":"إرسال الرمز","sending":"جاري الإرسال...","verifyTitle":"أدخلي الرمز","verifySubtitle":"أرسلنا رمزاً إلى {{phone}}","code":"رمز التحقق","verify":"تحقق","verifying":"جاري التحقق...","back":"تغيير الرقم","errors":{"generic":"حدث خطأ.","retryAfter":"انتظري {{seconds}} ثانية قبل المحاولة مرة أخرى."}},"locale": {
|
"nav":{"home":"الرئيسية","book":"احجز","pay":"ادفع","profile":"الحساب","bookings":"حجوزاتي","login":"تسجيل الدخول","logout":"تسجيل الخروج"},"book":{"title":"احجزي خدمة","placeholder":"تدفق الحجز قريباً.","cta":"احجزي الآن","selectSalon":"اختاري صالوناً من الصفحة الرئيسية للحجز.","service":"الخدمة","staff":"الفريق","date":"التاريخ","time":"الوقت","notes":"ملاحظات","notesPlaceholder":"ملاحظات اختيارية","selectService":"اختاري الخدمة","selectStaff":"اختاري الفني","submit":"تأكيد الحجز","submitting":"جاري الحجز...","errors":{"fillAll":"يرجى تعبئة جميع الحقول.","invalidTime":"تاريخ أو وقت غير صالح.","generic":"فشل الحجز."}},"salon":{"services":"الخدمات","staff":"الفريق","unknownStaff":"موظف {{id}}"},"profile":{"title":"الحساب","placeholder":"الملف والحجوزات قريباً.","myBookings":"حجوزاتي","noContact":"لا توجد معلومات اتصال"},"bookings":{"title":"حجوزاتي","subtitle":"مواعيدك القادمة والسابقة.","empty":"لا توجد حجوزات بعد.","pay":"ادفع الآن"},"paymentReturn":{"title":"حالة الدفع","success":"تم الدفع بنجاح","successMessage":"تم إتمام الدفع. ستستلمين تأكيداً عبر SMS أو واتساب.","checkStatus":"تحققي من بريدك أو الصالون لحالة الدفع.","reference":"المرجع: {{id}}","viewBookings":"عرض حجوزاتي"},"auth":{"title":"تسجيل الدخول","subtitle":"أدخلي رقم جوالك لاستلام رمز التحقق.","phone":"رقم الجوال","channel":"الإرسال عبر","sms":"رسالة نصية","whatsapp":"واتساب","sendCode":"إرسال الرمز","sending":"جاري الإرسال...","verifyTitle":"أدخلي الرمز","verifySubtitle":"أرسلنا رمزاً إلى {{phone}}","code":"رمز التحقق","verify":"تحقق","verifying":"جاري التحقق...","back":"تغيير الرقم","errors":{"generic":"حدث خطأ.","retryAfter":"انتظري {{seconds}} ثانية قبل المحاولة مرة أخرى."}},"locale": {
|
||||||
"label": "اللغة",
|
"label": "اللغة",
|
||||||
"arabic": "العربية",
|
"arabic": "العربية",
|
||||||
"english": "الإنجليزية"
|
"english": "الإنجليزية"
|
||||||
},
|
},
|
||||||
|
"common": {
|
||||||
|
"loading": "جاري التحميل..."
|
||||||
|
},
|
||||||
"payment": {
|
"payment": {
|
||||||
"title": "المدفوعات (تجريبي)",
|
"title": "المدفوعات (تجريبي)",
|
||||||
"subtitle": "إرسال عملية دفع عبر Moyasar لحجز موجود.",
|
"subtitle": "إرسال عملية دفع عبر Moyasar لحجز موجود.",
|
||||||
|
|||||||
@@ -16,11 +16,14 @@
|
|||||||
"phoneUnavailable": "Phone unavailable",
|
"phoneUnavailable": "Phone unavailable",
|
||||||
"viewDetails": "View details & book"
|
"viewDetails": "View details & book"
|
||||||
},
|
},
|
||||||
"nav":{"home":"Home","book":"Book","pay":"Pay","profile":"Profile","bookings":"My bookings","login":"Sign in","logout":"Sign out"},"book":{"title":"Book a Service","placeholder":"Booking flow coming soon.","cta":"Book now","selectSalon":"Select a salon from the home page to book.","service":"Service","staff":"Staff","date":"Date","time":"Time","notes":"Notes","notesPlaceholder":"Optional notes","selectService":"Select service","selectStaff":"Select staff","submit":"Confirm booking","submitting":"Booking...","errors":{"fillAll":"Please fill all required fields.","invalidTime":"Invalid date or time.","generic":"Booking failed."}},"salon":{"services":"Services","staff":"Staff"},"profile":{"title":"Profile","placeholder":"Profile and bookings coming soon.","myBookings":"My bookings","noContact":"No contact info"},"bookings":{"title":"My bookings","subtitle":"Your upcoming and past appointments.","empty":"No bookings yet.","pay":"Pay now"},"paymentReturn":{"title":"Payment status","success":"Payment successful","successMessage":"Your payment was completed. You will receive a confirmation by SMS or WhatsApp.","checkStatus":"Check your email or the salon for payment status.","reference":"Reference: {{id}}","viewBookings":"View my bookings"},"auth":{"title":"Sign in","subtitle":"Enter your phone number to receive a verification code.","phone":"Phone number","channel":"Send via","sms":"SMS","whatsapp":"WhatsApp","sendCode":"Send code","sending":"Sending...","verifyTitle":"Enter code","verifySubtitle":"We sent a code to {{phone}}","code":"Verification code","verify":"Verify","verifying":"Verifying...","back":"Change number","errors":{"generic":"Something went wrong.","retryAfter":"Please wait {{seconds}} seconds before trying again."}},"locale": {
|
"nav":{"home":"Home","book":"Book","pay":"Pay","profile":"Profile","bookings":"My bookings","login":"Sign in","logout":"Sign out"},"book":{"title":"Book a Service","placeholder":"Booking flow coming soon.","cta":"Book now","selectSalon":"Select a salon from the home page to book.","service":"Service","staff":"Staff","date":"Date","time":"Time","notes":"Notes","notesPlaceholder":"Optional notes","selectService":"Select service","selectStaff":"Select staff","submit":"Confirm booking","submitting":"Booking...","errors":{"fillAll":"Please fill all required fields.","invalidTime":"Invalid date or time.","generic":"Booking failed."}},"salon":{"services":"Services","staff":"Staff","unknownStaff":"Staff {{id}}"},"profile":{"title":"Profile","placeholder":"Profile and bookings coming soon.","myBookings":"My bookings","noContact":"No contact info"},"bookings":{"title":"My bookings","subtitle":"Your upcoming and past appointments.","empty":"No bookings yet.","pay":"Pay now"},"paymentReturn":{"title":"Payment status","success":"Payment successful","successMessage":"Your payment was completed. You will receive a confirmation by SMS or WhatsApp.","checkStatus":"Check your email or the salon for payment status.","reference":"Reference: {{id}}","viewBookings":"View my bookings"},"auth":{"title":"Sign in","subtitle":"Enter your phone number to receive a verification code.","phone":"Phone number","channel":"Send via","sms":"SMS","whatsapp":"WhatsApp","sendCode":"Send code","sending":"Sending...","verifyTitle":"Enter code","verifySubtitle":"We sent a code to {{phone}}","code":"Verification code","verify":"Verify","verifying":"Verifying...","back":"Change number","errors":{"generic":"Something went wrong.","retryAfter":"Please wait {{seconds}} seconds before trying again."}},"locale": {
|
||||||
"label": "Language",
|
"label": "Language",
|
||||||
"arabic": "العربية",
|
"arabic": "العربية",
|
||||||
"english": "English"
|
"english": "English"
|
||||||
},
|
},
|
||||||
|
"common": {
|
||||||
|
"loading": "Loading..."
|
||||||
|
},
|
||||||
"payment": {
|
"payment": {
|
||||||
"title": "Payment (Beta)",
|
"title": "Payment (Beta)",
|
||||||
"subtitle": "Send a Moyasar payment for an existing booking.",
|
"subtitle": "Send a Moyasar payment for an existing booking.",
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export default function BookPage() {
|
|||||||
<option value="">{t("book.selectStaff")}</option>
|
<option value="">{t("book.selectStaff")}</option>
|
||||||
{salon.staff?.map((s) => (
|
{salon.staff?.map((s) => (
|
||||||
<option key={s.id} value={s.id}>
|
<option key={s.id} value={s.id}>
|
||||||
{s.name || s.title || `Staff ${s.id}`}
|
{s.name || s.title || t("salon.unknownStaff", { id: s.id })}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { apiGet } from "../api/client";
|
import { apiGet } from "../api/client";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import ProtectedRoute from "../components/ProtectedRoute";
|
import ProtectedRoute from "../components/ProtectedRoute";
|
||||||
|
import { getActiveLocale } from "../i18n/index";
|
||||||
|
|
||||||
function formatDateTime(iso) {
|
function formatDateTime(iso, locale) {
|
||||||
if (!iso) return "";
|
if (!iso) return "";
|
||||||
const d = new Date(iso);
|
const d = new Date(iso);
|
||||||
return d.toLocaleString(undefined, {
|
return d.toLocaleString(locale, {
|
||||||
dateStyle: "medium",
|
dateStyle: "medium",
|
||||||
timeStyle: "short",
|
timeStyle: "short",
|
||||||
});
|
});
|
||||||
@@ -51,7 +52,7 @@ export default function BookingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="booking-service">{b.service_name}</p>
|
<p className="booking-service">{b.service_name}</p>
|
||||||
<p className="booking-time">
|
<p className="booking-time">
|
||||||
{formatDateTime(b.start_time)} – {formatDateTime(b.end_time)}
|
{formatDateTime(b.start_time, getActiveLocale())} – {formatDateTime(b.end_time, getActiveLocale())}
|
||||||
</p>
|
</p>
|
||||||
<p className="booking-price">
|
<p className="booking-price">
|
||||||
{b.price_amount} {b.currency}
|
{b.price_amount} {b.currency}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function SalonDetailPage() {
|
|||||||
<h2>{t("salon.staff")}</h2>
|
<h2>{t("salon.staff")}</h2>
|
||||||
<ul className="staff-list">
|
<ul className="staff-list">
|
||||||
{salon.staff?.map((s) => (
|
{salon.staff?.map((s) => (
|
||||||
<li key={s.id}>{s.name || s.title || `Staff ${s.id}`}</li>
|
<li key={s.id}>{s.name || s.title || t("salon.unknownStaff", { id: s.id })}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user