diff --git a/docs/usage.rst b/docs/usage.rst index 6dd1525..b7e2908 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -53,6 +53,16 @@ Configuration This would create ``SMSVerification`` table, which is used to store ``phone_number``, ``session_token`` and ``security_code``. +i18n Support +------------ + +If you would like to internationalize the verification messages, localize ``MESSAGE`` for all the desired locales +that you want to support as per Django internationalization guidelines. Then, from the client, in the call to +the ``phone-register`` API endpoint, specify the desired verification message language by setting the ``language`` +POST data to one of the locale codes that you support. ``PhoneVerificationService`` will then try to localize +the message for the specified language code and if found send it to the SMS backend. If a translation for the +specified language is not found, value of ``MESSAGE`` option would be sent. + Usage ----- diff --git a/phone_verify/api.py b/phone_verify/api.py index 97a70b1..c36867a 100644 --- a/phone_verify/api.py +++ b/phone_verify/api.py @@ -21,7 +21,8 @@ def register(self, request): serializer = PhoneSerializer(data=request.data) serializer.is_valid(raise_exception=True) session_token = send_security_code_and_generate_session_token( - str(serializer.validated_data["phone_number"]) + str(serializer.validated_data["phone_number"]), + serializer.validated_data['language'] ) return response.Ok({"session_token": session_token}) diff --git a/phone_verify/serializers.py b/phone_verify/serializers.py index 6e67014..400140d 100644 --- a/phone_verify/serializers.py +++ b/phone_verify/serializers.py @@ -19,6 +19,7 @@ class PhoneSerializer(serializers.Serializer): phone_number = PhoneNumberField() + language = serializers.CharField(default='') class SMSVerificationSerializer(serializers.Serializer): diff --git a/phone_verify/services.py b/phone_verify/services.py index 5324150..f84da18 100644 --- a/phone_verify/services.py +++ b/phone_verify/services.py @@ -6,6 +6,7 @@ # Third Party Stuff from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from django.utils.translation import override, gettext # phone_verify stuff from .backends import get_sms_backend @@ -27,10 +28,11 @@ class PhoneVerificationService(object): verification_message = phone_settings.get("MESSAGE", DEFAULT_MESSAGE) - def __init__(self, phone_number, backend=None): + def __init__(self, phone_number, backend=None, language='en'): self._check_required_settings() if backend is None: self.backend = get_sms_backend(phone_number=phone_number) + self.language = language def send_verification(self, number, security_code): """ @@ -39,10 +41,17 @@ def send_verification(self, number, security_code): :param number: the phone number of recipient. """ message = self._generate_message(security_code) - self.backend.send_sms(number, message) def _generate_message(self, security_code): + if self.language: + with override(self.language): + verification_message = gettext(self.verification_message) + return verification_message.format( + app=settings.PHONE_VERIFICATION.get("APP_NAME", DEFAULT_APP_NAME), + security_code=security_code, + ) + return self.verification_message.format( app=settings.PHONE_VERIFICATION.get("APP_NAME", DEFAULT_APP_NAME), security_code=security_code, @@ -67,12 +76,12 @@ def _check_required_settings(self): ) -def send_security_code_and_generate_session_token(phone_number): +def send_security_code_and_generate_session_token(phone_number, language=''): sms_backend = get_sms_backend(phone_number) security_code, session_token = sms_backend.create_security_code_and_session_token( phone_number ) - service = PhoneVerificationService(phone_number=phone_number) + service = PhoneVerificationService(phone_number=phone_number, language=language) try: service.send_verification(phone_number, security_code) except service.backend.exception_class as exc: diff --git a/tests/test_api.py b/tests/test_api.py index f7e3142..857beba 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -40,6 +40,34 @@ def test_phone_registration_sends_message(client, mocker, backend): ) +def test_phone_registration_sends_i18n_message(client, mocker, backend): + with override_settings(PHONE_VERIFICATION=backend): + zh_verification_message = "歡迎使用 {app}! 請使用安全碼 {security_code} 繼續。" + mocker.patch('phone_verify.services.gettext', return_value=zh_verification_message) + + backend_service = backend.get("BACKEND") + mock_backend_send_sms = mocker.patch(f"{backend_service}.send_sms") + + url = reverse("phone-register") + phone_number = PHONE_NUMBER + data = {"phone_number": phone_number, "language": "zh-hant"} + + response = client.post(url, data) + + assert response.status_code == 200 + assert "session_token" in response.data + SMSVerification = apps.get_model("phone_verify", "SMSVerification") + sms_verification = SMSVerification.objects.get( + session_token=response.data["session_token"], phone_number=phone_number + ) + assert sms_verification + actual_message = zh_verification_message.format( + app=backend['APP_NAME'], + security_code=sms_verification.security_code + ) + mock_backend_send_sms.assert_called_with("+13478379634", actual_message) + + def test_security_code_session_token_verification_api(client, backend): with override_settings(PHONE_VERIFICATION=backend): f.create_verification( diff --git a/tests/test_services.py b/tests/test_services.py index 17fa854..e20008e 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -31,6 +31,19 @@ def test_message_generation_and_sending_service(client, mocker, backend): assert mock_api.called +def test_i18n_message_generation_and_sending_service(client, mocker, backend): + with override_settings(PHONE_VERIFICATION=backend): + zh_verification_message = "歡迎使用 {app}! 請使用安全碼 {security_code} 繼續。" + mocker.patch('phone_verify.services.gettext', return_value=zh_verification_message) + service = PhoneVerificationService(phone_number="+13478379634", language='zh-hant') + backend_service = backend.get("BACKEND") + mock_api = mocker.patch(f"{backend_service}.send_sms") + service.send_verification("+13478379634", "123456") + actual_message = zh_verification_message.format( + app=backend['APP_NAME'], security_code="123456" + ) + mock_api.assert_called_with("+13478379634", actual_message) + def test_exception_is_logged_when_raised(client, mocker, backend): with override_settings(PHONE_VERIFICATION=backend): diff --git a/tox.ini b/tox.ini index fa5a7bd..a7a816f 100644 --- a/tox.ini +++ b/tox.ini @@ -25,4 +25,4 @@ deps = django32: Django>=3.2,<3.2.5 django40: Django>=4.0 commands = - pytest --cov -v --tb=native + pytest --cov -v --tb=native {posargs}