Updated PLANS.md, AGENTS.md, and arabic-localization.md to reflect the “foundations now, full translations later” approach and marked progress accordingly.

Implemented localization foundations across backend and frontend (locale settings/middleware, preferred language, i18n wiring, RTL support, minimal Arabic UI strings, Accept-Language).
Added targeted backend and frontend tests plus a risks note for pending full translation coverage.
This commit is contained in:
2026-02-28 11:48:58 +03:00
parent fd90af33b3
commit d40bb10876
27 changed files with 407 additions and 68 deletions
+17 -13
View File
@@ -8,6 +8,8 @@ from django.conf import settings
from django.contrib.auth.hashers import check_password, make_password
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from apps.accounts.models import OtpChannel, OtpPurpose, PhoneOTP
logger = logging.getLogger(__name__)
@@ -21,13 +23,13 @@ class OtpSendResult:
class OtpRateLimitError(RuntimeError):
def __init__(self, retry_after_seconds: int):
super().__init__("Too many OTP requests. Try again later.")
super().__init__(_("Too many OTP requests. Try again later."))
self.retry_after_seconds = retry_after_seconds
class OtpCooldownError(RuntimeError):
def __init__(self, retry_after_seconds: int):
super().__init__("Please wait before requesting another code.")
super().__init__(_("Please wait before requesting another code."))
self.retry_after_seconds = retry_after_seconds
@@ -56,17 +58,17 @@ class TwilioOtpProvider(BaseOtpProvider):
def _assert_config(self) -> None:
if not self.account_sid or not self.auth_token or not self.from_number:
raise ValueError("Twilio credentials are not configured")
raise ValueError(_("Twilio credentials are not configured"))
def send_sms(self, to_number: str, message: str) -> None:
self._assert_config()
raise NotImplementedError("Twilio SMS adapter not implemented yet")
raise NotImplementedError(_("Twilio SMS adapter not implemented yet"))
def send_whatsapp(self, to_number: str, message: str) -> None:
self._assert_config()
if not self.whatsapp_from:
raise ValueError("Twilio WhatsApp sender is not configured")
raise NotImplementedError("Twilio WhatsApp adapter not implemented yet")
raise ValueError(_("Twilio WhatsApp sender is not configured"))
raise NotImplementedError(_("Twilio WhatsApp adapter not implemented yet"))
class UnifonicOtpProvider(BaseOtpProvider):
@@ -77,17 +79,17 @@ class UnifonicOtpProvider(BaseOtpProvider):
def _assert_config(self) -> None:
if not self.app_sid or not self.sender_id:
raise ValueError("Unifonic credentials are not configured")
raise ValueError(_("Unifonic credentials are not configured"))
def send_sms(self, to_number: str, message: str) -> None:
self._assert_config()
raise NotImplementedError("Unifonic SMS adapter not implemented yet")
raise NotImplementedError(_("Unifonic SMS adapter not implemented yet"))
def send_whatsapp(self, to_number: str, message: str) -> None:
self._assert_config()
if not self.whatsapp_sender:
raise ValueError("Unifonic WhatsApp sender is not configured")
raise NotImplementedError("Unifonic WhatsApp adapter not implemented yet")
raise ValueError(_("Unifonic WhatsApp sender is not configured"))
raise NotImplementedError(_("Unifonic WhatsApp adapter not implemented yet"))
PROVIDERS = {
@@ -101,7 +103,7 @@ def get_provider() -> BaseOtpProvider:
provider_key = settings.OTP_PROVIDER
provider_cls = PROVIDERS.get(provider_key)
if not provider_cls:
raise ValueError(f"Unknown OTP provider: {provider_key}")
raise ValueError(_("Unknown OTP provider: %(provider)s") % {"provider": provider_key})
return provider_cls()
@@ -147,13 +149,15 @@ def create_and_send_otp(phone_number: str, channel: str, purpose: str = OtpPurpo
expires_at=PhoneOTP.expiry_at(),
)
message = f"Your verification code is {code}. It expires in {settings.OTP_EXPIRY_MINUTES} minutes."
message = _(
"Your verification code is %(code)s. It expires in %(minutes)s minutes."
) % {"code": code, "minutes": settings.OTP_EXPIRY_MINUTES}
if channel == OtpChannel.SMS:
provider.send_sms(phone_number, message)
elif channel == OtpChannel.WHATSAPP:
provider.send_whatsapp(phone_number, message)
else:
raise ValueError("Unsupported OTP channel")
raise ValueError(_("Unsupported OTP channel"))
return OtpSendResult(request_id=str(otp.id), expires_at=otp.expires_at.isoformat())
+5 -3
View File
@@ -1,9 +1,11 @@
import re
from django.utils.translation import gettext as _
def normalize_phone_number(raw_phone: str) -> str:
if not raw_phone:
raise ValueError("Phone number is required")
raise ValueError(_("Phone number is required"))
phone = re.sub(r"[\s\-\(\)]", "", raw_phone)
if phone.startswith("00"):
@@ -12,7 +14,7 @@ def normalize_phone_number(raw_phone: str) -> str:
if phone.startswith("+"):
digits = phone[1:]
if not digits.isdigit() or not (8 <= len(digits) <= 15):
raise ValueError("Invalid phone number format")
raise ValueError(_("Invalid phone number format"))
return "+" + digits
digits = re.sub(r"\D", "", phone)
@@ -23,4 +25,4 @@ def normalize_phone_number(raw_phone: str) -> str:
if digits.startswith("5") and len(digits) == 9:
return "+966" + digits
raise ValueError("Phone number must be in E.164 format or a valid Saudi mobile")
raise ValueError(_("Phone number must be in E.164 format or a valid Saudi mobile"))