feat(i18n): persist language preference and thread through chat, memoir, TTS
- Add users.language_preference (Alembic 0018, default zh); capture at signup/SMS only; expose on auth and profile APIs - Lite English prompts for chat and memoir; localized stage labels and agent names (Life Echo / 岁月知己) - Tencent TTS: language-aware synthesis, ModelType=1 for 501004, English chunking - WebSocket pipeline: emit all AGENT_RESPONSE segments when TTS cancels; INFO logs for tts_this_turn and TTS decisions; on-demand TTS logging - Expo: device language on auth, i18n tiers/agent name, [SPLIT] streaming UX fixes - Tests for migration, prompts, pipeline, router tts_this_turn, reply segments Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -24,6 +24,16 @@ CODE_LENGTH = 6
|
||||
CODE_EXPIRE_MINUTES = 5
|
||||
RATE_LIMIT_SECONDS = 60
|
||||
|
||||
_VALID_LANGUAGES = {"zh", "en"}
|
||||
|
||||
|
||||
def _normalize_language(lang: str | None) -> str:
|
||||
"""Normalize device language token; default to zh on missing/unknown."""
|
||||
if not lang:
|
||||
return "zh"
|
||||
s = str(lang).strip().lower()
|
||||
return s if s in _VALID_LANGUAGES else "zh"
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
def __init__(self, message: str, code: str = "AUTH_ERROR"):
|
||||
@@ -120,6 +130,7 @@ class AuthService:
|
||||
password: str,
|
||||
nickname: str,
|
||||
email: str | None = None,
|
||||
language: str | None = None,
|
||||
) -> dict:
|
||||
"""Register new user. Returns {user, access_token, refresh_token}."""
|
||||
if await repo.get_user_by_phone(phone, self._db):
|
||||
@@ -137,6 +148,7 @@ class AuthService:
|
||||
nickname=nickname,
|
||||
subscription_type="free",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
language_preference=_normalize_language(language),
|
||||
)
|
||||
await repo.create_user(user, self._db)
|
||||
tokens = await self._issue_tokens(user_id)
|
||||
@@ -206,6 +218,7 @@ class AuthService:
|
||||
code: str,
|
||||
device_info: str = "",
|
||||
nickname: str | None = None,
|
||||
language: str | None = None,
|
||||
) -> dict:
|
||||
"""SMS login (auto-register if new). Returns {user, access_token, refresh_token, is_new_user}."""
|
||||
success = False
|
||||
@@ -219,7 +232,10 @@ class AuthService:
|
||||
raise AuthError(message, "INVALID_SMS_CODE")
|
||||
|
||||
return await self._sms_login_after_code_verified(
|
||||
phone, device_info=device_info, nickname=nickname
|
||||
phone,
|
||||
device_info=device_info,
|
||||
nickname=nickname,
|
||||
language=language,
|
||||
)
|
||||
|
||||
async def _sms_login_after_code_verified(
|
||||
@@ -228,8 +244,12 @@ class AuthService:
|
||||
*,
|
||||
device_info: str = "",
|
||||
nickname: str | None = None,
|
||||
language: str | None = None,
|
||||
) -> dict:
|
||||
"""SMS 已校验通过后:查找或创建用户并签发令牌。"""
|
||||
"""SMS 已校验通过后:查找或创建用户并签发令牌。
|
||||
|
||||
``language`` 仅在「新用户」分支下写入;命中已有用户时不覆盖偏好。
|
||||
"""
|
||||
user = await repo.get_user_by_phone(phone, self._db)
|
||||
is_new_user = user is None
|
||||
|
||||
@@ -242,6 +262,7 @@ class AuthService:
|
||||
nickname=(nickname or "").strip(),
|
||||
subscription_type="free",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
language_preference=_normalize_language(language),
|
||||
)
|
||||
await repo.create_user(user, self._db)
|
||||
|
||||
@@ -257,10 +278,14 @@ class AuthService:
|
||||
phone: str,
|
||||
device_info: str = "",
|
||||
nickname: str | None = None,
|
||||
language: str | None = None,
|
||||
) -> dict:
|
||||
"""跳过短信校验的登录/自动注册(仅由 mock 路由在配置允许时调用)。"""
|
||||
return await self._sms_login_after_code_verified(
|
||||
phone, device_info=device_info, nickname=nickname
|
||||
phone,
|
||||
device_info=device_info,
|
||||
nickname=nickname,
|
||||
language=language,
|
||||
)
|
||||
|
||||
async def register_with_sms(
|
||||
@@ -271,6 +296,7 @@ class AuthService:
|
||||
nickname: str,
|
||||
email: str | None = None,
|
||||
device_info: str = "",
|
||||
language: str | None = None,
|
||||
) -> dict:
|
||||
"""SMS register. Returns {user, access_token, refresh_token}."""
|
||||
success, message = await self._verify_sms_code(phone, code, "register")
|
||||
@@ -292,6 +318,7 @@ class AuthService:
|
||||
nickname=nickname,
|
||||
subscription_type="free",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
language_preference=_normalize_language(language),
|
||||
)
|
||||
await repo.create_user(user, self._db)
|
||||
tokens = await self._issue_tokens(user_id, device_info)
|
||||
|
||||
Reference in New Issue
Block a user