Merge response language selector into main

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-05-10 11:57:04 +08:00
commit 99c964e0ca
5 changed files with 63 additions and 0 deletions

View File

@ -669,6 +669,9 @@ export const en = {
'settings.general.appearance.dark': 'Dark',
'settings.general.languageTitle': 'Language',
'settings.general.languageDescription': 'Choose the display language for the application.',
'settings.general.responseLangTitle': 'Response Language',
'settings.general.responseLangDescription': 'Instruct Claude to always respond in a specific language.',
'settings.general.responseLangDefault': 'Default (English)',
'settings.general.effortTitle': 'Effort Level',
'settings.general.effortDescription': 'Controls how much computation the model uses.',
'settings.general.effort.low': 'Low',

View File

@ -671,6 +671,9 @@ export const zh: Record<TranslationKey, string> = {
'settings.general.appearance.dark': '暗色',
'settings.general.languageTitle': '语言',
'settings.general.languageDescription': '选择应用程序的显示语言。',
'settings.general.responseLangTitle': '回复语言',
'settings.general.responseLangDescription': '指定 Claude 始终以某种语言回复。',
'settings.general.responseLangDefault': '默认(英语)',
'settings.general.effortTitle': '推理强度',
'settings.general.effortDescription': '控制模型使用的计算量。',
'settings.general.effort.low': '低',

View File

@ -1368,6 +1368,8 @@ function GeneralSettings() {
disableH5Access,
regenerateH5AccessToken,
updateH5AccessSettings,
responseLanguage,
setResponseLanguage,
} = useSettingsStore()
const t = useTranslation()
const [webSearchDraft, setWebSearchDraft] = useState(webSearch)
@ -1426,6 +1428,31 @@ function GeneralSettings() {
{ value: 'zh', label: '中文' },
]
const RESPONSE_LANGUAGES: Array<{ value: string; label: string }> = [
{ value: '', label: t('settings.general.responseLangDefault') },
{ value: 'english', label: 'English' },
{ value: 'chinese', label: '中文 (Chinese)' },
{ value: 'japanese', label: '日本語 (Japanese)' },
{ value: 'korean', label: '한국어 (Korean)' },
{ value: 'spanish', label: 'Español (Spanish)' },
{ value: 'french', label: 'Français (French)' },
{ value: 'german', label: 'Deutsch (German)' },
{ value: 'portuguese', label: 'Português (Portuguese)' },
{ value: 'italian', label: 'Italiano (Italian)' },
{ value: 'russian', label: 'Русский (Russian)' },
{ value: 'dutch', label: 'Nederlands (Dutch)' },
{ value: 'polish', label: 'Polski (Polish)' },
{ value: 'turkish', label: 'Türkçe (Turkish)' },
{ value: 'hindi', label: 'हिन्दी (Hindi)' },
{ value: 'indonesian', label: 'Bahasa Indonesia' },
{ value: 'ukrainian', label: 'Українська (Ukrainian)' },
{ value: 'greek', label: 'Ελληνικά (Greek)' },
{ value: 'czech', label: 'Čeština (Czech)' },
{ value: 'danish', label: 'Dansk (Danish)' },
{ value: 'swedish', label: 'Svenska (Swedish)' },
{ value: 'norwegian', label: 'Norsk (Norwegian)' },
]
const THEMES: Array<{ value: ThemeMode; label: string }> = [
{ value: 'light', label: t('settings.general.appearance.light') },
{ value: 'dark', label: t('settings.general.appearance.dark') },
@ -1577,6 +1604,21 @@ function GeneralSettings() {
))}
</div>
{/* Response Language */}
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.responseLangTitle')}</h2>
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">{t('settings.general.responseLangDescription')}</p>
<div className="flex gap-2 mb-8">
<select
value={responseLangDraft}
onChange={(e) => void setResponseLanguage(e.target.value)}
className="flex-1 px-3 py-2 text-sm rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-container-low)] text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-brand)] transition-colors cursor-pointer"
>
{RESPONSE_LANGUAGES.map(({ value, label }) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</div>
{/* Effort Level */}
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.effortTitle')}</h2>
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">{t('settings.general.effortDescription')}</p>

View File

@ -32,6 +32,7 @@ type SettingsStore = {
webSearch: WebSearchSettings
h5Access: H5AccessSettings
h5AccessError: string | null
responseLanguage: string
isLoading: boolean
error: string | null
@ -53,6 +54,7 @@ type SettingsStore = {
allowedOrigins?: string[]
publicBaseUrl?: string | null
}) => Promise<void>
setResponseLanguage: (language: string) => Promise<void>
}
const DEFAULT_H5_ACCESS_SETTINGS: H5AccessSettings = {
@ -76,6 +78,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
webSearch: { mode: 'auto', tavilyApiKey: '', braveApiKey: '' },
h5Access: DEFAULT_H5_ACCESS_SETTINGS,
h5AccessError: null,
responseLanguage: '',
isLoading: false,
error: null,
@ -106,6 +109,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
webSearch: normalizeWebSearchSettings(userSettings.webSearch),
h5Access: h5AccessResult.settings,
h5AccessError: h5AccessResult.error,
responseLanguage: typeof userSettings.language === 'string' ? userSettings.language : '',
isLoading: false,
error: null,
})
@ -274,6 +278,16 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
throw error
}
},
setResponseLanguage: async (language) => {
const prev = get().responseLanguage
set({ responseLanguage: language })
try {
await settingsApi.updateUser({ language: language || undefined })
} catch {
set({ responseLanguage: prev })
}
},
}))
function normalizeWebSearchSettings(settings: WebSearchSettings | undefined): WebSearchSettings {

View File

@ -36,5 +36,6 @@ export type UserSettings = {
skipWebFetchPreflight?: boolean
desktopNotificationsEnabled?: boolean
webSearch?: WebSearchSettings
language?: string
[key: string]: unknown
}