Add internationalization support and enhance language handling
All checks were successful
CI / lint-and-test (push) Successful in 14s
All checks were successful
CI / lint-and-test (push) Successful in 14s
- Introduced a new i18n module for managing translations and language normalization, supporting both Russian and English. - Updated various handlers and services to utilize the new translation functions for user-facing messages, improving user experience based on language preferences. - Enhanced error handling and response messages to be language-aware, ensuring appropriate feedback is provided to users in their preferred language. - Added tests for the i18n module to validate language detection and translation functionality. - Updated the example environment file to include a default language configuration.
This commit is contained in:
@@ -19,13 +19,15 @@ def client():
|
||||
def test_duties_invalid_date_format(client):
|
||||
r = client.get("/api/duties", params={"from": "2025-01-01", "to": "invalid"})
|
||||
assert r.status_code == 400
|
||||
assert "YYYY-MM-DD" in r.json()["detail"]
|
||||
detail = r.json()["detail"]
|
||||
assert "from" in detail.lower() and "to" in detail.lower()
|
||||
|
||||
|
||||
def test_duties_from_after_to(client):
|
||||
r = client.get("/api/duties", params={"from": "2025-02-01", "to": "2025-01-01"})
|
||||
assert r.status_code == 400
|
||||
assert "from" in r.json()["detail"].lower() or "позже" in r.json()["detail"]
|
||||
detail = r.json()["detail"].lower()
|
||||
assert "from" in detail or "to" in detail or "after" in detail or "позже" in detail
|
||||
|
||||
|
||||
@patch("duty_teller.api.dependencies._is_private_client")
|
||||
@@ -54,7 +56,7 @@ def test_duties_200_when_skip_auth(mock_fetch, client):
|
||||
|
||||
@patch("duty_teller.api.dependencies.validate_init_data_with_reason")
|
||||
def test_duties_403_when_init_data_invalid(mock_validate, client):
|
||||
mock_validate.return_value = (None, "hash_mismatch")
|
||||
mock_validate.return_value = (None, "hash_mismatch", "en")
|
||||
r = client.get(
|
||||
"/api/duties",
|
||||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||||
@@ -62,13 +64,18 @@ def test_duties_403_when_init_data_invalid(mock_validate, client):
|
||||
)
|
||||
assert r.status_code == 403
|
||||
detail = r.json()["detail"]
|
||||
assert "авторизации" in detail or "Неверные" in detail or "Неверная" in detail
|
||||
assert (
|
||||
"signature" in detail.lower()
|
||||
or "авторизации" in detail
|
||||
or "Неверные" in detail
|
||||
or "Неверная" in detail
|
||||
)
|
||||
|
||||
|
||||
@patch("duty_teller.api.dependencies.validate_init_data_with_reason")
|
||||
@patch("duty_teller.api.dependencies.config.can_access_miniapp")
|
||||
def test_duties_403_when_username_not_allowed(mock_can_access, mock_validate, client):
|
||||
mock_validate.return_value = ("someuser", "ok")
|
||||
mock_validate.return_value = ("someuser", "ok", "en")
|
||||
mock_can_access.return_value = False
|
||||
with patch("duty_teller.api.app.fetch_duties_response") as mock_fetch:
|
||||
r = client.get(
|
||||
@@ -77,14 +84,16 @@ def test_duties_403_when_username_not_allowed(mock_can_access, mock_validate, cl
|
||||
headers={"X-Telegram-Init-Data": "user=xxx&hash=yyy"},
|
||||
)
|
||||
assert r.status_code == 403
|
||||
assert "Доступ запрещён" in r.json()["detail"]
|
||||
assert (
|
||||
"Access denied" in r.json()["detail"] or "Доступ запрещён" in r.json()["detail"]
|
||||
)
|
||||
mock_fetch.assert_not_called()
|
||||
|
||||
|
||||
@patch("duty_teller.api.dependencies.validate_init_data_with_reason")
|
||||
@patch("duty_teller.api.dependencies.config.can_access_miniapp")
|
||||
def test_duties_200_with_allowed_user(mock_can_access, mock_validate, client):
|
||||
mock_validate.return_value = ("alloweduser", "ok")
|
||||
mock_validate.return_value = ("alloweduser", "ok", "en")
|
||||
mock_can_access.return_value = True
|
||||
with patch("duty_teller.api.app.fetch_duties_response") as mock_fetch:
|
||||
mock_fetch.return_value = [
|
||||
|
||||
76
tests/test_i18n.py
Normal file
76
tests/test_i18n.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Unit tests for duty_teller.i18n: get_lang, t, fallback to en."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
from duty_teller.i18n import get_lang, t
|
||||
|
||||
|
||||
def test_get_lang_none_returns_en():
|
||||
assert get_lang(None) == "en"
|
||||
|
||||
|
||||
def test_get_lang_ru_returns_ru():
|
||||
user = MagicMock()
|
||||
user.language_code = "ru"
|
||||
assert get_lang(user) == "ru"
|
||||
|
||||
|
||||
def test_get_lang_ru_ru_returns_ru():
|
||||
user = MagicMock()
|
||||
user.language_code = "ru-RU"
|
||||
assert get_lang(user) == "ru"
|
||||
|
||||
|
||||
def test_get_lang_en_returns_en():
|
||||
user = MagicMock()
|
||||
user.language_code = "en"
|
||||
assert get_lang(user) == "en"
|
||||
|
||||
|
||||
def test_get_lang_uk_returns_en():
|
||||
user = MagicMock()
|
||||
user.language_code = "uk"
|
||||
assert get_lang(user) == "en"
|
||||
|
||||
|
||||
def test_get_lang_empty_returns_en():
|
||||
user = MagicMock()
|
||||
user.language_code = ""
|
||||
assert get_lang(user) == "en"
|
||||
|
||||
|
||||
def test_get_lang_missing_attr_returns_en():
|
||||
user = MagicMock(spec=[]) # no language_code
|
||||
assert get_lang(user) == "en"
|
||||
|
||||
|
||||
def test_t_en_start_greeting():
|
||||
result = t("en", "start.greeting")
|
||||
assert "help" in result.lower()
|
||||
assert "bot" in result.lower()
|
||||
|
||||
|
||||
def test_t_ru_start_greeting():
|
||||
result = t("ru", "start.greeting")
|
||||
assert "Привет" in result or "бот" in result or "календар" in result
|
||||
|
||||
|
||||
def test_t_fallback_to_en_when_key_missing_for_ru():
|
||||
"""When key exists in en but not in ru, fallback to en."""
|
||||
result = t("ru", "start.greeting")
|
||||
assert len(result) > 0
|
||||
# start.greeting exists in both; use a key that might be en-only for fallback
|
||||
result_any = t("ru", "errors.generic")
|
||||
assert "error" in result_any.lower() or "ошибк" in result_any.lower()
|
||||
|
||||
|
||||
def test_t_placeholder_substitution():
|
||||
assert t("en", "set_phone.saved", phone="+7 999") == "Phone saved: +7 999"
|
||||
assert "999" in t("ru", "set_phone.saved", phone="+7 999")
|
||||
|
||||
|
||||
def test_t_unknown_lang_normalized_to_en():
|
||||
assert "help" in t("de", "start.greeting").lower() or "Hi" in t(
|
||||
"de", "start.greeting"
|
||||
)
|
||||
Reference in New Issue
Block a user