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:
@@ -5,9 +5,11 @@ import { useCallback } from 'react';
|
||||
import { AuthError } from '@/core/api/types';
|
||||
import { tokenManager } from '@/core/auth/token-manager';
|
||||
import { disposeAllBackgroundConversationWs } from '@/features/conversation/conversation-ws-background-pool';
|
||||
import { getDeviceLanguage } from '@/i18n';
|
||||
|
||||
import { authApi } from './api';
|
||||
import type {
|
||||
LanguagePreference,
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
SessionState,
|
||||
@@ -19,6 +21,17 @@ import type {
|
||||
UserInfo,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Resolve the device language to send at sign-up. Backend only persists
|
||||
* language_preference on first user creation; subsequent logins ignore it.
|
||||
*/
|
||||
function withDeviceLanguage<T extends { language?: LanguagePreference }>(
|
||||
body: T,
|
||||
): T {
|
||||
if (body.language) return body;
|
||||
return { ...body, language: getDeviceLanguage() };
|
||||
}
|
||||
|
||||
// ─── Query keys ───
|
||||
|
||||
export const authKeys = {
|
||||
@@ -139,7 +152,8 @@ export function useSmsLogin() {
|
||||
const onSuccess = usePostAuthSetup();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (body: SmsLoginRequest) => authApi.loginWithSms(body),
|
||||
mutationFn: (body: SmsLoginRequest) =>
|
||||
authApi.loginWithSms(withDeviceLanguage(body)),
|
||||
onSuccess,
|
||||
});
|
||||
}
|
||||
@@ -150,7 +164,8 @@ export function useRegister() {
|
||||
const onSuccess = usePostAuthSetup();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (body: RegisterRequest) => authApi.register(body),
|
||||
mutationFn: (body: RegisterRequest) =>
|
||||
authApi.register(withDeviceLanguage(body)),
|
||||
onSuccess,
|
||||
});
|
||||
}
|
||||
@@ -161,7 +176,8 @@ export function useSmsRegister() {
|
||||
const onSuccess = usePostAuthSetup();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (body: SmsRegisterRequest) => authApi.registerWithSms(body),
|
||||
mutationFn: (body: SmsRegisterRequest) =>
|
||||
authApi.registerWithSms(withDeviceLanguage(body)),
|
||||
onSuccess,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// ─── Response types ───
|
||||
|
||||
export type LanguagePreference = 'zh' | 'en';
|
||||
|
||||
export interface TokenResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
@@ -14,6 +16,7 @@ export interface UserInfo {
|
||||
avatar_url: string | null;
|
||||
subscription_type: string;
|
||||
created_at: string;
|
||||
language_preference?: LanguagePreference;
|
||||
}
|
||||
|
||||
// ─── Request types ───
|
||||
@@ -30,6 +33,7 @@ export interface RegisterRequest {
|
||||
nickname: string;
|
||||
email?: string;
|
||||
agreed_to_terms: boolean;
|
||||
language?: LanguagePreference;
|
||||
}
|
||||
|
||||
export type SmsPurpose =
|
||||
@@ -48,6 +52,7 @@ export interface SmsLoginRequest {
|
||||
code: string;
|
||||
agreed_to_terms: boolean;
|
||||
nickname?: string;
|
||||
language?: LanguagePreference;
|
||||
}
|
||||
|
||||
export interface SmsRegisterRequest {
|
||||
@@ -57,6 +62,7 @@ export interface SmsRegisterRequest {
|
||||
nickname: string;
|
||||
email?: string;
|
||||
agreed_to_terms: boolean;
|
||||
language?: LanguagePreference;
|
||||
}
|
||||
|
||||
export interface ResetPasswordRequest {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { File, Paths } from 'expo-file-system';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { AppState, type AppStateStatus } from 'react-native';
|
||||
|
||||
import i18n from '@/i18n';
|
||||
import type { WsConnectionState } from '@/core/ws/types';
|
||||
|
||||
import { conversationApi } from './api';
|
||||
@@ -111,7 +112,7 @@ export function useCreateConversation() {
|
||||
const now = Date.now();
|
||||
const item: ConversationListItem = {
|
||||
id: newConversation.id,
|
||||
title: '岁月知己',
|
||||
title: i18n.t('agentName', { ns: 'conversation' }),
|
||||
avatarUrl: null,
|
||||
latestMessagePreview: '',
|
||||
latestMessageTime: now,
|
||||
@@ -247,6 +248,14 @@ export function useRealtimeSession({
|
||||
setAwaitingAssistantReply(false);
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 空文本 + 未完成时不能写成 `{text: '', isComplete: false}`:
|
||||
* UI 会渲染一只空 `StreamingBubbles`(pulsing 气泡 + 光标),看上去与
|
||||
* 「正在回复…」typing 气泡难以区分,且会一直挂在底部不消失。
|
||||
*/
|
||||
if (text.length === 0) {
|
||||
return;
|
||||
}
|
||||
setStreamingMessage({ text, isComplete });
|
||||
},
|
||||
[],
|
||||
|
||||
Reference in New Issue
Block a user