Skip to content

Commit ee855a6

Browse files
feat: add internationalization support for verification messages
Add support for localizing SMS verification messages based on the Accept-Language HTTP header. The library now automatically detects the user's preferred language and sends verification messages in that language using Django's translation system. Changes: - Extract language from Accept-Language header in API endpoint - Add language parameter to PhoneVerificationService - Implement message translation using Django's override and gettext - Add comprehensive i18n documentation with setup guide - Add tests for i18n functionality in API and service layers Co-authored-by: Hari Mahadevan <[email protected]>
1 parent 9a0fcb4 commit ee855a6

File tree

6 files changed

+211
-5
lines changed

6 files changed

+211
-5
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Release Notes
66

77
Added
88
"""""
9+
- **Internationalization (i18n)**: Added support for localizing verification messages based on the ``Accept-Language`` HTTP header. The library now automatically detects the user's preferred language and sends verification messages in that language using Django's translation system. Contributed by `Hari Mahadevan <https://github.com/harikvpy>`_.
910
- **Documentation**: Completely overhauled documentation to professional, enterprise-grade quality:
1011

1112
- **Getting Started Guide** - Expanded with prerequisites, step-by-step configuration, environment variables, and testing instructions

docs/advanced_examples.rst

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,149 @@ Implementation
570570
...
571571
}
572572
573+
Internationalization (i18n) Support
574+
------------------------------------
575+
576+
Send verification messages in different languages based on user preferences.
577+
578+
Overview
579+
^^^^^^^^
580+
581+
The library automatically detects the user's language from the ``Accept-Language`` HTTP header
582+
and localizes the verification message accordingly. This is useful for applications serving
583+
users in multiple countries or regions.
584+
585+
Setup
586+
^^^^^
587+
588+
First, ensure Django's internationalization is enabled in your ``settings.py``:
589+
590+
.. code-block:: python
591+
592+
# settings.py
593+
USE_I18N = True
594+
LANGUAGE_CODE = 'en-us'
595+
596+
LANGUAGES = [
597+
('en', 'English'),
598+
('es', 'Spanish'),
599+
('fr', 'French'),
600+
('zh-hant', 'Traditional Chinese'),
601+
# Add more languages as needed
602+
]
603+
604+
LOCALE_PATHS = [
605+
BASE_DIR / 'locale',
606+
]
607+
608+
Create Translation Files
609+
^^^^^^^^^^^^^^^^^^^^^^^^^
610+
611+
Create translation files for your verification message. The message template in ``PHONE_VERIFICATION['MESSAGE']``
612+
will be automatically translated:
613+
614+
.. code-block:: bash
615+
616+
# Create locale directories
617+
mkdir -p locale/es/LC_MESSAGES
618+
mkdir -p locale/fr/LC_MESSAGES
619+
mkdir -p locale/zh_Hant/LC_MESSAGES
620+
621+
# Generate message files
622+
django-admin makemessages -l es
623+
django-admin makemessages -l fr
624+
django-admin makemessages -l zh_Hant
625+
626+
Edit the generated ``.po`` files to add translations:
627+
628+
.. code-block:: po
629+
630+
# locale/es/LC_MESSAGES/django.po
631+
msgid "Welcome to {app}! Please use security code {security_code} to proceed."
632+
msgstr "¡Bienvenido a {app}! Por favor usa el código de seguridad {security_code} para continuar."
633+
634+
# locale/fr/LC_MESSAGES/django.po
635+
msgid "Welcome to {app}! Please use security code {security_code} to proceed."
636+
msgstr "Bienvenue sur {app}! Veuillez utiliser le code de sécurité {security_code} pour continuer."
637+
638+
# locale/zh_Hant/LC_MESSAGES/django.po
639+
msgid "Welcome to {app}! Please use security code {security_code} to proceed."
640+
msgstr "歡迎使用 {app}! 請使用安全碼 {security_code} 繼續。"
641+
642+
Compile the translations:
643+
644+
.. code-block:: bash
645+
646+
django-admin compilemessages
647+
648+
Usage
649+
^^^^^
650+
651+
The library automatically reads the ``Accept-Language`` header from HTTP requests and sends
652+
the verification message in the user's preferred language:
653+
654+
.. code-block:: javascript
655+
656+
// Frontend: Set Accept-Language header
657+
fetch('/api/phone/register/', {
658+
method: 'POST',
659+
headers: {
660+
'Content-Type': 'application/json',
661+
'Accept-Language': 'es' // Spanish
662+
},
663+
body: JSON.stringify({
664+
phone_number: '+1234567890'
665+
})
666+
});
667+
668+
The SMS sent to the user will automatically be in Spanish if you've provided a translation.
669+
670+
Programmatic Usage
671+
^^^^^^^^^^^^^^^^^^
672+
673+
You can also specify the language programmatically when using the service directly:
674+
675+
.. code-block:: python
676+
677+
from phone_verify.services import send_security_code_and_generate_session_token
678+
679+
# Send verification in French
680+
session_token = send_security_code_and_generate_session_token(
681+
phone_number="+1234567890",
682+
language="fr"
683+
)
684+
685+
# Or using the service class directly
686+
from phone_verify.services import PhoneVerificationService
687+
688+
service = PhoneVerificationService(
689+
phone_number="+1234567890",
690+
language="zh-hant" # Traditional Chinese
691+
)
692+
693+
backend = service.backend
694+
security_code, session_token = backend.create_security_code_and_session_token(
695+
phone_number="+1234567890"
696+
)
697+
698+
service.send_verification("+1234567890", security_code)
699+
700+
Language Code Format
701+
^^^^^^^^^^^^^^^^^^^^
702+
703+
The library accepts standard language codes:
704+
705+
- Simple codes: ``en``, ``es``, ``fr``, ``de``, ``ja``, ``zh``
706+
- Locale-specific: ``en-US``, ``en-GB``, ``zh-Hans`` (Simplified Chinese), ``zh-Hant`` (Traditional Chinese)
707+
- The first language in comma-separated ``Accept-Language`` headers is used
708+
- Quality values (``q=``) are ignored; only the first language is considered
709+
710+
Fallback Behavior
711+
^^^^^^^^^^^^^^^^^
712+
713+
If a translation is not available for the requested language, the library falls back to
714+
the default message defined in ``PHONE_VERIFICATION['MESSAGE']``.
715+
573716
Phone Number Update Flow
574717
-------------------------
575718

phone_verify/api.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,18 @@ class VerificationViewSet(viewsets.GenericViewSet):
2020
def register(self, request):
2121
serializer = PhoneSerializer(data=request.data)
2222
serializer.is_valid(raise_exception=True)
23+
24+
# Extract language from Accept-Language header
25+
# Format: "en-US,en;q=0.9,es;q=0.8" -> take first language "en-US"
26+
accept_language = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
27+
language = None
28+
if accept_language:
29+
# Take first language, strip quality params (e.g., "en-US;q=0.9" -> "en-US")
30+
language = accept_language.split(',')[0].split(';')[0].strip() or None
31+
2332
session_token = send_security_code_and_generate_session_token(
24-
str(serializer.validated_data["phone_number"])
33+
str(serializer.validated_data["phone_number"]),
34+
language=language
2535
)
2636
return response.Ok({"session_token": session_token})
2737

phone_verify/services.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# Third Party Stuff
77
from django.conf import settings
88
from django.core.exceptions import ImproperlyConfigured
9+
from django.utils.translation import gettext, override
910

1011
# phone_verify stuff
1112
from .backends import get_sms_backend
@@ -21,7 +22,7 @@
2122

2223
class PhoneVerificationService(object):
2324

24-
def __init__(self, phone_number, backend=None):
25+
def __init__(self, phone_number, backend=None, language=None):
2526
try:
2627
self.phone_settings = settings.PHONE_VERIFICATION
2728
except AttributeError as e:
@@ -33,6 +34,7 @@ def __init__(self, phone_number, backend=None):
3334
self.backend = backend
3435

3536
self.verification_message = self.phone_settings.get("MESSAGE", DEFAULT_MESSAGE)
37+
self.language = language
3638

3739
def send_verification(self, number, security_code, context=None):
3840
"""
@@ -60,7 +62,13 @@ def _generate_message(self, security_code, context=None):
6062
if context:
6163
format_context.update(context)
6264

63-
return self.verification_message.format(**format_context)
65+
# Apply i18n if language is specified
66+
verification_message = self.verification_message
67+
if self.language:
68+
with override(self.language):
69+
verification_message = gettext(self.verification_message)
70+
71+
return verification_message.format(**format_context)
6472

6573
def _check_required_settings(self):
6674
required_settings = {
@@ -81,12 +89,12 @@ def _check_required_settings(self):
8189
)
8290

8391

84-
def send_security_code_and_generate_session_token(phone_number):
92+
def send_security_code_and_generate_session_token(phone_number, language=None):
8593
sms_backend = get_sms_backend(phone_number)
8694
security_code, session_token = sms_backend.create_security_code_and_session_token(
8795
phone_number
8896
)
89-
service = PhoneVerificationService(phone_number=phone_number)
97+
service = PhoneVerificationService(phone_number=phone_number, language=language)
9098
try:
9199
service.send_verification(phone_number, security_code)
92100
except service.backend.exception_class as exc:

tests/test_api.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,32 @@ def test_verified_security_code(client, backend):
202202
response_data = json.loads(json.dumps(response.data))
203203
assert response.status_code == 200
204204
assert response.data["message"] == "Security code is valid."
205+
206+
207+
def test_phone_registration_with_accept_language_header(client, mocker, backend):
208+
with override_settings(PHONE_VERIFICATION=backend):
209+
zh_verification_message = "歡迎使用 {app}! 請使用安全碼 {security_code} 繼續。"
210+
mocker.patch('phone_verify.services.gettext', return_value=zh_verification_message)
211+
212+
backend_service = backend.get("BACKEND")
213+
mock_backend_send_sms = mocker.patch(f"{backend_service}.send_sms")
214+
215+
url = reverse("phone-register")
216+
phone_number = PHONE_NUMBER
217+
data = {"phone_number": phone_number}
218+
219+
# Send request with Accept-Language header
220+
response = client.post(url, data, HTTP_ACCEPT_LANGUAGE="zh-hant")
221+
222+
assert response.status_code == 200
223+
assert "session_token" in response.data
224+
SMSVerification = apps.get_model("phone_verify", "SMSVerification")
225+
sms_verification = SMSVerification.objects.get(
226+
session_token=response.data["session_token"], phone_number=phone_number
227+
)
228+
assert sms_verification
229+
actual_message = zh_verification_message.format(
230+
app=backend['APP_NAME'],
231+
security_code=sms_verification.security_code
232+
)
233+
mock_backend_send_sms.assert_called_with("+13478379634", actual_message)

tests/test_services.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,18 @@ def test_generate_message_from_custom_backend(settings):
131131
msg = svc._generate_message("999999", context={"extra": "runtime"})
132132

133133
assert msg == "Custom: 999999 / runtime"
134+
135+
136+
@pytest.mark.django_db
137+
def test_i18n_message_generation_and_sending_service(client, mocker, backend):
138+
with override_settings(PHONE_VERIFICATION=backend):
139+
zh_verification_message = "歡迎使用 {app}! 請使用安全碼 {security_code} 繼續。"
140+
mocker.patch('phone_verify.services.gettext', return_value=zh_verification_message)
141+
service = PhoneVerificationService(phone_number="+13478379634", language='zh-hant')
142+
backend_service = backend.get("BACKEND")
143+
mock_api = mocker.patch(f"{backend_service}.send_sms")
144+
service.send_verification("+13478379634", "123456")
145+
actual_message = zh_verification_message.format(
146+
app=backend['APP_NAME'], security_code="123456"
147+
)
148+
mock_api.assert_called_with("+13478379634", actual_message)

0 commit comments

Comments
 (0)