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:
@@ -1,14 +1,16 @@
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
|
||||
export default function ProtectedRoute({ children }) {
|
||||
const { t } = useTranslation();
|
||||
const { isAuthenticated, loading } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="auth-loading">
|
||||
<p>Loading...</p>
|
||||
<p>{t("common.loading")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
"phoneUnavailable": "الهاتف غير متوفر",
|
||||
"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": "اللغة",
|
||||
"arabic": "العربية",
|
||||
"english": "الإنجليزية"
|
||||
},
|
||||
"common": {
|
||||
"loading": "جاري التحميل..."
|
||||
},
|
||||
"payment": {
|
||||
"title": "المدفوعات (تجريبي)",
|
||||
"subtitle": "إرسال عملية دفع عبر Moyasar لحجز موجود.",
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
"phoneUnavailable": "Phone unavailable",
|
||||
"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",
|
||||
"arabic": "العربية",
|
||||
"english": "English"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"payment": {
|
||||
"title": "Payment (Beta)",
|
||||
"subtitle": "Send a Moyasar payment for an existing booking.",
|
||||
|
||||
@@ -118,7 +118,7 @@ export default function BookPage() {
|
||||
<option value="">{t("book.selectStaff")}</option>
|
||||
{salon.staff?.map((s) => (
|
||||
<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>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -4,11 +4,12 @@ import { useTranslation } from "react-i18next";
|
||||
import { apiGet } from "../api/client";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import ProtectedRoute from "../components/ProtectedRoute";
|
||||
import { getActiveLocale } from "../i18n/index";
|
||||
|
||||
function formatDateTime(iso) {
|
||||
function formatDateTime(iso, locale) {
|
||||
if (!iso) return "";
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString(undefined, {
|
||||
return d.toLocaleString(locale, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
});
|
||||
@@ -51,7 +52,7 @@ export default function BookingsPage() {
|
||||
</div>
|
||||
<p className="booking-service">{b.service_name}</p>
|
||||
<p className="booking-time">
|
||||
{formatDateTime(b.start_time)} – {formatDateTime(b.end_time)}
|
||||
{formatDateTime(b.start_time, getActiveLocale())} – {formatDateTime(b.end_time, getActiveLocale())}
|
||||
</p>
|
||||
<p className="booking-price">
|
||||
{b.price_amount} {b.currency}
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function SalonDetailPage() {
|
||||
<h2>{t("salon.staff")}</h2>
|
||||
<ul className="staff-list">
|
||||
{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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user