feat: unify language handling across the application

- Updated the language configuration to use a single source of truth from `DEFAULT_LANGUAGE` for the bot, API, and Mini App, eliminating auto-detection from user settings.
- Refactored the `get_lang` function to always return `DEFAULT_LANGUAGE`, ensuring consistent language usage throughout the application.
- Modified the handling of language in various components, including API responses and UI elements, to reflect the new language management approach.
- Enhanced documentation and comments to clarify the changes in language handling.
- Added unit tests to verify the new language handling behavior and ensure coverage for the updated functionality.
This commit is contained in:
2026-03-02 23:05:28 +03:00
parent 54446d7b0f
commit 67ba9826c7
21 changed files with 446 additions and 205 deletions

View File

@@ -6,7 +6,7 @@ import json
import time
from urllib.parse import unquote
from duty_teller.i18n.lang import normalize_lang
import duty_teller.config as config
# Telegram algorithm: https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app
# Data-check string: sorted key=value with URL-decoded values, then HMAC-SHA256(WebAppData, token) as secret.
@@ -48,12 +48,12 @@ def validate_init_data_with_reason(
Returns:
Tuple (telegram_user_id, username, reason, lang). reason is one of: "ok",
"empty", "no_hash", "hash_mismatch", "auth_date_expired", "no_user",
"user_invalid", "no_user_id". lang is from user.language_code normalized
to 'ru' or 'en'; 'en' when no user. On success: (user.id, username or None,
"ok", lang).
"user_invalid", "no_user_id". lang is always config.DEFAULT_LANGUAGE.
On success: (user.id, username or None, "ok", lang).
"""
lang = config.DEFAULT_LANGUAGE
if not init_data or not bot_token:
return (None, None, "empty", "en")
return (None, None, "empty", lang)
init_data = init_data.strip()
params = {}
for part in init_data.split("&"):
@@ -65,7 +65,7 @@ def validate_init_data_with_reason(
params[key] = value
hash_val = params.pop("hash", None)
if not hash_val:
return (None, None, "no_hash", "en")
return (None, None, "no_hash", lang)
data_pairs = sorted(params.items())
# Data-check string: key=value with URL-decoded values (per Telegram example)
data_string = "\n".join(f"{k}={unquote(v)}" for k, v in data_pairs)
@@ -81,27 +81,26 @@ def validate_init_data_with_reason(
digestmod=hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(computed.lower(), hash_val.lower()):
return (None, None, "hash_mismatch", "en")
return (None, None, "hash_mismatch", lang)
if max_age_seconds is not None and max_age_seconds > 0:
auth_date_raw = params.get("auth_date")
if not auth_date_raw:
return (None, None, "auth_date_expired", "en")
return (None, None, "auth_date_expired", lang)
try:
auth_date = int(float(auth_date_raw))
except (ValueError, TypeError):
return (None, None, "auth_date_expired", "en")
return (None, None, "auth_date_expired", lang)
if time.time() - auth_date > max_age_seconds:
return (None, None, "auth_date_expired", "en")
return (None, None, "auth_date_expired", lang)
user_raw = params.get("user")
if not user_raw:
return (None, None, "no_user", "en")
return (None, None, "no_user", lang)
try:
user = json.loads(unquote(user_raw))
except (json.JSONDecodeError, TypeError):
return (None, None, "user_invalid", "en")
return (None, None, "user_invalid", lang)
if not isinstance(user, dict):
return (None, None, "user_invalid", "en")
lang = normalize_lang(user.get("language_code"))
return (None, None, "user_invalid", lang)
raw_id = user.get("id")
if raw_id is None:
return (None, None, "no_user_id", lang)