让界面语言直接驱动日文/韩文输出偏好并避免翻译链路冲突

在现有简繁联动基础上扩展 ja/ko 本地化与 outputLanguagePreference,保持不新增额外设置项。前端在语言切换时统一同步 chineseScriptPreference + outputLanguagePreference;后端在 polish/QA 链路注入输出语言偏好,但翻译链路固定以 target_language 为唯一约束,避免 UI 语言与翻译目标冲突。

Constraint: 用户要求最小修改且不新增独立输出语言开关

Rejected: 新增单独输出语言设置项 | 与既定交互约束冲突

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: 翻译链路不要再次注入 output language 偏好,除非先定义与 target_language 的优先级规则

Tested: npm run -s build

Tested: cargo test -q (72 passed)

Not-tested: 真机端到端切换 ja/ko 后的实际模型输出体验
This commit is contained in:
H-Chris233 committed 2026-05-05 20:28:38 +08:00
1 parent c4dba80f34
commit 04536830a2
15 files changed
+259 -26

No files matched your search

+3 -1
View File
@@ -13,7 +13,8 @@ use crate::persistence::{CredentialAccount, CredentialsSnapshot, CredentialsVaul
use crate::polish::{LLMError, OpenAICompatibleConfig, OpenAICompatibleLLMProvider};
use crate::types::{
ChineseScriptPreference, CredentialsStatus, DictationSession, DictionaryEntry,
HotkeyCapability, HotkeyStatus, PolishMode, QaHotkeyBinding, UserPreferences,
HotkeyCapability, HotkeyStatus, OutputLanguagePreference, PolishMode, QaHotkeyBinding,
UserPreferences,
VocabPresetStore, WindowsImeStatus,
};
@@ -230,6 +231,7 @@ async fn validate_llm_provider() -> Result<(), String> {
&[],
&[],
ChineseScriptPreference::Auto,
OutputLanguagePreference::Auto,
None,
)
.await
+19 -1
View File
@@ -32,7 +32,8 @@ use crate::recorder::{Recorder, RecorderError};
use crate::selection::{capture_selection, SelectionContext};
use crate::types::{
CapsulePayload, CapsuleState, ChineseScriptPreference, DictationSession, HotkeyCapability,
HotkeyMode, HotkeyStatus, HotkeyStatusState, InsertStatus, PolishMode,
HotkeyMode, HotkeyStatus, HotkeyStatusState, InsertStatus, OutputLanguagePreference,
PolishMode,
};
#[cfg(target_os = "windows")]
use crate::windows_ime_ipc::ImeSubmitTarget;
@@ -477,6 +478,7 @@ impl Coordinator {
let prefs = self.inner.prefs.get();
let working_languages = prefs.working_languages;
let chinese_script_preference = prefs.chinese_script_preference;
let output_language_preference = prefs.output_language_preference;
// repolish 是历史记录里手动重新润色,不再绑定原 session 的前台 app;
// 当下用户调起的 app 才是相关上下文(如果可拿)。
let front_app = capture_frontmost_app();
@@ -486,6 +488,7 @@ impl Coordinator {
&hotwords,
&working_languages,
chinese_script_preference,
output_language_preference,
front_app.as_deref(),
)
.await
@@ -1645,6 +1648,7 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
let hotword_strs = enabled_phrases(inner);
let working_languages = prefs.working_languages.clone();
let chinese_script_preference = prefs.chinese_script_preference;
let output_language_preference = prefs.output_language_preference;
let front_app = inner.state.lock().front_app.clone();
let translation_target = prefs.translation_target_language.trim().to_string();
let translation_active =
@@ -1661,6 +1665,7 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
&translation_target,
&working_languages,
chinese_script_preference,
output_language_preference,
front_app.as_deref(),
)
.await
@@ -1671,6 +1676,7 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
&hotword_strs,
&working_languages,
chinese_script_preference,
output_language_preference,
front_app.as_deref(),
)
.await
@@ -2216,6 +2222,7 @@ async fn polish_or_passthrough(
hotwords: &[String],
working_languages: &[String],
chinese_script_preference: ChineseScriptPreference,
output_language_preference: OutputLanguagePreference,
front_app: Option<&str>,
) -> (String, Option<String>) {
if mode == PolishMode::Raw {
@@ -2227,6 +2234,7 @@ async fn polish_or_passthrough(
hotwords,
working_languages,
chinese_script_preference,
output_language_preference,
front_app,
)
.await
@@ -2246,6 +2254,7 @@ async fn polish_text(
hotwords: &[String],
working_languages: &[String],
chinese_script_preference: ChineseScriptPreference,
output_language_preference: OutputLanguagePreference,
front_app: Option<&str>,
) -> anyhow::Result<String> {
let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default();
@@ -2267,6 +2276,7 @@ async fn polish_text(
hotwords,
working_languages,
chinese_script_preference,
output_language_preference,
front_app,
)
.await?)
@@ -2278,6 +2288,7 @@ async fn translate_or_passthrough(
target_language: &str,
working_languages: &[String],
chinese_script_preference: ChineseScriptPreference,
output_language_preference: OutputLanguagePreference,
front_app: Option<&str>,
) -> (String, Option<String>) {
match translate_text(
@@ -2285,6 +2296,7 @@ async fn translate_or_passthrough(
target_language,
working_languages,
chinese_script_preference,
output_language_preference,
front_app,
)
.await
@@ -2303,6 +2315,7 @@ async fn translate_text(
target_language: &str,
working_languages: &[String],
chinese_script_preference: ChineseScriptPreference,
output_language_preference: OutputLanguagePreference,
front_app: Option<&str>,
) -> anyhow::Result<String> {
let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default();
@@ -2323,6 +2336,7 @@ async fn translate_text(
target_language,
working_languages,
chinese_script_preference,
output_language_preference,
front_app,
)
.await?)
@@ -2651,6 +2665,7 @@ async fn end_qa_session(inner: &Arc<Inner>) -> Result<(), String> {
let prefs = inner.prefs.get();
let working_languages = prefs.working_languages.clone();
let chinese_script_preference = prefs.chinese_script_preference;
let output_language_preference = prefs.output_language_preference;
let (messages_for_llm, front_app) = {
let st = inner.qa_state.lock();
(st.messages.clone(), st.front_app.clone())
@@ -2690,6 +2705,7 @@ async fn end_qa_session(inner: &Arc<Inner>) -> Result<(), String> {
&messages_for_llm,
&working_languages,
chinese_script_preference,
output_language_preference,
front_app.as_deref(),
on_delta,
should_cancel,
@@ -2838,6 +2854,7 @@ async fn answer_chat_dispatch<F, C>(
messages: &[crate::types::QaChatMessage],
working_languages: &[String],
chinese_script_preference: ChineseScriptPreference,
output_language_preference: OutputLanguagePreference,
front_app: Option<&str>,
on_delta: F,
should_cancel: C,
@@ -2862,6 +2879,7 @@ where
messages,
working_languages,
chinese_script_preference,
output_language_preference,
front_app,
on_delta,
should_cancel,
+52 -5
View File
@@ -10,7 +10,9 @@ use std::time::Duration;
use serde_json::{json, Value};
use thiserror::Error;
use crate::types::{ChineseScriptPreference, PolishMode, QaChatMessage};
use crate::types::{
ChineseScriptPreference, OutputLanguagePreference, PolishMode, QaChatMessage,
};
const DEFAULT_TEMPERATURE: f32 = 0.3;
const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 30;
@@ -91,11 +93,17 @@ impl OpenAICompatibleLLMProvider {
hotwords: &[String],
working_languages: &[String],
chinese_script_preference: ChineseScriptPreference,
output_language_preference: OutputLanguagePreference,
front_app: Option<&str>,
) -> Result<String, LLMError> {
let mut system_prompt = compose_system_prompt(mode, hotwords);
if let Some(premise) =
context_premise(working_languages, chinese_script_preference, front_app)
context_premise(
working_languages,
chinese_script_preference,
output_language_preference,
front_app,
)
{
system_prompt = format!("{}\n\n{}", premise, system_prompt);
}
@@ -112,6 +120,7 @@ impl OpenAICompatibleLLMProvider {
messages: &[QaChatMessage],
working_languages: &[String],
chinese_script_preference: ChineseScriptPreference,
output_language_preference: OutputLanguagePreference,
front_app: Option<&str>,
on_delta: F,
should_cancel: C,
@@ -122,7 +131,12 @@ impl OpenAICompatibleLLMProvider {
{
let mut system_prompt = prompts::qa_system_prompt();
if let Some(premise) =
context_premise(working_languages, chinese_script_preference, front_app)
context_premise(
working_languages,
chinese_script_preference,
output_language_preference,
front_app,
)
{
system_prompt = format!("{}\n\n{}", premise, system_prompt);
}
@@ -138,11 +152,19 @@ impl OpenAICompatibleLLMProvider {
target_language: &str,
working_languages: &[String],
chinese_script_preference: ChineseScriptPreference,
_output_language_preference: OutputLanguagePreference,
front_app: Option<&str>,
) -> Result<String, LLMError> {
let mut system_prompt = prompts::translate_system_prompt(target_language);
// 翻译模式必须以 target_language 为唯一输出语言约束,避免和 UI 驱动的
// output_language_preference 发生冲突(例如 UI=ja, target=en)。
if let Some(premise) =
context_premise(working_languages, chinese_script_preference, front_app)
context_premise(
working_languages,
chinese_script_preference,
OutputLanguagePreference::Auto,
front_app,
)
{
system_prompt = format!("{}\n\n{}", premise, system_prompt);
}
@@ -390,6 +412,7 @@ fn chat_completions_url(base_url: &str) -> String {
fn context_premise(
working_languages: &[String],
chinese_script_preference: ChineseScriptPreference,
output_language_preference: OutputLanguagePreference,
front_app: Option<&str>,
) -> Option<String> {
let langs: Vec<&str> = working_languages
@@ -411,7 +434,27 @@ fn context_premise(
ChineseScriptPreference::Auto => None,
};
if langs.is_empty() && app.is_none() && script_line.is_none() {
let output_language_line = match output_language_preference {
OutputLanguagePreference::ZhCn => {
Some("最终输出语言偏好:简体中文。若回答可用中文表达,请优先使用简体中文。".to_string())
}
OutputLanguagePreference::ZhTw => {
Some("最終輸出語言偏好:繁體中文。若回答可用中文表達,請優先使用繁體中文。".to_string())
}
OutputLanguagePreference::En => {
Some("Output language preference: English. Prefer English when producing the final answer.".to_string())
}
OutputLanguagePreference::Ja => {
Some("出力言語の優先設定:日本語。最終回答は可能な限り日本語で出力してください。".to_string())
}
OutputLanguagePreference::Ko => {
Some("출력 언어 선호: 한국어. 최종 답변은 가능하면 한국어로 작성해 주세요.".to_string())
}
OutputLanguagePreference::Auto => None,
};
if langs.is_empty() && app.is_none() && script_line.is_none() && output_language_line.is_none()
{
return None;
}
@@ -430,6 +473,9 @@ fn context_premise(
if let Some(line) = script_line {
lines.push(line);
}
if let Some(line) = output_language_line {
lines.push(line);
}
Some(lines.join("\n"))
}
@@ -1016,6 +1062,7 @@ mod tests {
&[],
&[],
ChineseScriptPreference::Auto,
OutputLanguagePreference::Auto,
None,
)
.await
+17
View File
@@ -33,6 +33,18 @@ pub enum ChineseScriptPreference {
Traditional,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub enum OutputLanguagePreference {
#[default]
Auto,
ZhCn,
ZhTw,
En,
Ja,
Ko,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum InsertStatus {
@@ -131,6 +143,10 @@ pub struct UserPreferences {
/// 由前端「界面语言」选择同步驱动(简体/繁体),详见 issue #259。
#[serde(default)]
pub chinese_script_preference: ChineseScriptPreference,
/// 最终输出语言偏好(不额外暴露为 UI 开关):
/// 由前端「界面语言」选择同步驱动:zh-CN/zh-TW/en/ja/ko,其他为 Auto。
#[serde(default)]
pub output_language_preference: OutputLanguagePreference,
/// 划词语音问答(QA)的全局快捷键。`None` = 关闭功能;`Some(...)` 时
/// coordinator 用 global-hotkey crate 注册组合键(modifier + 主键)。
/// 默认 Cmd+Shift+; (macOS) / Ctrl+Shift+; (Windows)。详见 issue #118。
@@ -194,6 +210,7 @@ impl Default for UserPreferences {
working_languages: default_working_languages(),
translation_target_language: String::new(),
chinese_script_preference: ChineseScriptPreference::Auto,
output_language_preference: OutputLanguagePreference::Auto,
qa_hotkey: default_qa_hotkey(),
qa_save_history: false,
local_asr_active_model: default_local_asr_model(),
@@ -14,6 +14,7 @@ import { openExternal } from '../lib/ipc';
import {
FOLLOW_SYSTEM,
getLocalePreference,
outputPrefsForLocale,
setLocalePreference,
type SupportedLocale,
} from '../i18n';
@@ -318,11 +319,15 @@ function LanguagePicker() {
const apply = async (next: SupportedLocale | typeof FOLLOW_SYSTEM) => {
setPref(next);
const resolved = await setLocalePreference(next);
const chineseScriptPreference =
resolved === 'zh-CN' ? 'simplified' : resolved === 'zh-TW' ? 'traditional' : 'auto';
const localePrefs = outputPrefsForLocale(resolved);
await updatePrefs(current => {
if (current.chineseScriptPreference === chineseScriptPreference) return current;
return { ...current, chineseScriptPreference };
if (
current.chineseScriptPreference === localePrefs.chineseScriptPreference &&
current.outputLanguagePreference === localePrefs.outputLanguagePreference
) {
return current;
}
return { ...current, ...localePrefs };
});
};
@@ -343,6 +348,8 @@ function LanguagePicker() {
<option value="zh-CN">{t('settings.language.zh')}</option>
<option value="zh-TW">{t('settings.language.zhTW')}</option>
<option value="en">{t('settings.language.en')}</option>
<option value="ja">{t('settings.language.ja')}</option>
<option value="ko">{t('settings.language.ko')}</option>
</select>
);
}
+2
View File
@@ -403,6 +403,8 @@ export const en: typeof zhCN = {
zh: '简体中文',
zhTW: '繁體中文',
en: 'English',
ja: '日本語',
ko: '한국어',
restartHint: 'Some native menus (system tray, etc.) may require an app restart to fully switch.',
},
about: {
+48 -2
View File
@@ -11,10 +11,13 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { en } from './en';
import { ja } from './ja';
import { ko } from './ko';
import { zhCN } from './zh-CN';
import { zhTW } from './zh-TW';
import type { UserPreferences } from '../lib/types';
export const SUPPORTED_LOCALES = ['zh-CN', 'zh-TW', 'en'] as const;
export const SUPPORTED_LOCALES = ['zh-CN', 'zh-TW', 'en', 'ja', 'ko'] as const;
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
export const LOCALE_STORAGE_KEY = 'ol.locale';
@@ -27,6 +30,8 @@ function detectSystemLocale(): SupportedLocale {
if (nav.includes('hant') || nav.includes('tw') || nav.includes('hk') || nav.includes('mo')) return 'zh-TW';
return 'zh-CN';
}
if (nav.startsWith('ja')) return 'ja';
if (nav.startsWith('ko')) return 'ko';
return 'en';
}
@@ -38,7 +43,7 @@ function resolveLocalePreference(pref: SupportedLocale | typeof FOLLOW_SYSTEM_VA
function getStoredLocale(): SupportedLocale | null {
if (typeof window === 'undefined') return null;
const raw = window.localStorage.getItem(LOCALE_STORAGE_KEY);
return raw === 'zh-CN' || raw === 'zh-TW' || raw === 'en' ? raw : null;
return raw === 'zh-CN' || raw === 'zh-TW' || raw === 'en' || raw === 'ja' || raw === 'ko' ? raw : null;
}
const initialLng: SupportedLocale = getStoredLocale() ?? detectSystemLocale();
@@ -48,6 +53,8 @@ void i18n.use(initReactI18next).init({
'zh-CN': { translation: zhCN },
'zh-TW': { translation: zhTW },
en: { translation: en },
ja: { translation: ja },
ko: { translation: ko },
},
lng: initialLng,
fallbackLng: 'zh-CN',
@@ -85,3 +92,42 @@ export async function setLocalePreference(
}
export const FOLLOW_SYSTEM = FOLLOW_SYSTEM_VALUE;
export function outputPrefsForLocale(
resolved: SupportedLocale,
): Pick<UserPreferences, 'chineseScriptPreference' | 'outputLanguagePreference'> {
if (resolved === 'zh-CN') {
return {
chineseScriptPreference: 'simplified',
outputLanguagePreference: 'zhCn',
};
}
if (resolved === 'zh-TW') {
return {
chineseScriptPreference: 'traditional',
outputLanguagePreference: 'zhTw',
};
}
if (resolved === 'en') {
return {
chineseScriptPreference: 'auto',
outputLanguagePreference: 'en',
};
}
if (resolved === 'ja') {
return {
chineseScriptPreference: 'auto',
outputLanguagePreference: 'ja',
};
}
if (resolved === 'ko') {
return {
chineseScriptPreference: 'auto',
outputLanguagePreference: 'ko',
};
}
return {
chineseScriptPreference: 'auto',
outputLanguagePreference: 'auto',
};
}
+35
View File
@@ -0,0 +1,35 @@
import type { zhCN } from './zh-CN';
import { en } from './en';
// Japanese locale scaffold (incremental translation over English baseline).
export const ja: typeof zhCN = {
...en,
app: {
...en.app,
tagline: '自然に話し、きれいに書く',
},
settings: {
...en.settings,
language: {
...en.settings.language,
title: '表示言語',
desc: 'UI の表示言語を切り替えます。現在のセッションに即時反映され、次回起動時も維持されます。',
label: '言語',
labelDesc: '「システムに従う」を選ぶと OS の言語に合わせます。',
followSystem: 'システムに従う',
zh: '简体中文',
zhTW: '繁體中文',
en: 'English',
ja: '日本語',
ko: '한국어',
restartHint: '一部のネイティブメニュー(トレイ等)は再起動後に反映されます。',
},
},
modal: {
...en.modal,
personalize: {
...en.modal.personalize,
language: '表示言語',
},
},
};
+35
View File
@@ -0,0 +1,35 @@
import type { zhCN } from './zh-CN';
import { en } from './en';
// Korean locale scaffold (incremental translation over English baseline).
export const ko: typeof zhCN = {
...en,
app: {
...en.app,
tagline: '자연스럽게 말하고, 정확하게 작성하세요',
},
settings: {
...en.settings,
language: {
...en.settings.language,
title: '인터페이스 언어',
desc: 'UI 표시 언어를 전환합니다. 현재 세션에 즉시 반영되며 다음 실행에도 유지됩니다.',
label: '언어',
labelDesc: '"시스템 따라가기"를 선택하면 OS 언어를 따릅니다.',
followSystem: '시스템 따라가기',
zh: '简体中文',
zhTW: '繁體中文',
en: 'English',
ja: '日本語',
ko: '한국어',
restartHint: '일부 네이티브 메뉴(트레이 등)는 앱 재시작 후 반영될 수 있습니다.',
},
},
modal: {
...en.modal,
personalize: {
...en.modal.personalize,
language: '인터페이스 언어',
},
},
};
+2
View File
@@ -401,6 +401,8 @@ export const zhCN = {
zh: '简体中文',
zhTW: '繁體中文',
en: 'English',
ja: '日本語',
ko: '한국어',
restartHint: '部分原生菜单(系统托盘等)可能需要重启 App 才会切换。',
},
about: {
+2
View File
@@ -403,6 +403,8 @@ export const zhTW: typeof zhCN = {
zh: '簡體中文',
zhTW: '繁體中文',
en: 'English',
ja: '日本語',
ko: '한국어',
restartHint: '部分原生菜單(系統托盤等)可能需要重啓 App 纔會切換。',
},
about: {
+1
View File
@@ -52,6 +52,7 @@ const mockSettings: UserPreferences = {
workingLanguages: ['简体中文'],
translationTargetLanguage: '',
chineseScriptPreference: 'auto',
outputLanguagePreference: 'auto',
qaHotkey: { primary: ';', modifiers: ['cmd', 'shift'] },
qaSaveHistory: false,
localAsrActiveModel: 'qwen3-asr-0.6b',
+2
View File
@@ -122,6 +122,8 @@ export interface UserPreferences {
translationTargetLanguage: string;
/** 中文输出字形偏好:由界面语言(简/繁)自动同步,不单独暴露设置项。 */
chineseScriptPreference: 'auto' | 'simplified' | 'traditional';
/** 最终输出语言偏好:由界面语言自动同步,不单独暴露设置项。 */
outputLanguagePreference: 'auto' | 'zhCn' | 'zhTw' | 'en' | 'ja' | 'ko';
/** 划词语音问答快捷键。null = 未启用。详见 issue #118。 */
qaHotkey: QaHotkeyBinding | null;
/** 是否把 Q&A 历史写到本地存档。详见 issue #118。 */
+11 -4
View File
@@ -38,6 +38,7 @@ import { useHotkeySettings } from '../state/HotkeySettingsContext';
import i18n, {
FOLLOW_SYSTEM,
getLocalePreference,
outputPrefsForLocale,
setLocalePreference,
type SupportedLocale,
} from '../i18n';
@@ -1098,11 +1099,15 @@ function LanguageSection() {
const apply = async (next: SupportedLocale | typeof FOLLOW_SYSTEM) => {
setPref(next);
const resolved = await setLocalePreference(next);
const chineseScriptPreference =
resolved === 'zh-CN' ? 'simplified' : resolved === 'zh-TW' ? 'traditional' : 'auto';
const localePrefs = outputPrefsForLocale(resolved);
await updatePrefs(current => {
if (current.chineseScriptPreference === chineseScriptPreference) return current;
return { ...current, chineseScriptPreference };
if (
current.chineseScriptPreference === localePrefs.chineseScriptPreference &&
current.outputLanguagePreference === localePrefs.outputLanguagePreference
) {
return current;
}
return { ...current, ...localePrefs };
});
};
@@ -1120,6 +1125,8 @@ function LanguageSection() {
<option value="zh-CN">{t('settings.language.zh')}</option>
<option value="zh-TW">{t('settings.language.zhTW')}</option>
<option value="en">{t('settings.language.en')}</option>
<option value="ja">{t('settings.language.ja')}</option>
<option value="ko">{t('settings.language.ko')}</option>
</select>
</SettingRow>
<div style={{ fontSize: 11, color: 'var(--ol-ink-4)', marginTop: 8, lineHeight: 1.6 }}>
@@ -10,7 +10,7 @@ import {
} from 'react';
import { getHotkeyCapability, getSettings, setSettings } from '../lib/ipc';
import type { HotkeyBinding, HotkeyCapability, UserPreferences } from '../lib/types';
import i18n from '../i18n';
import i18n, { outputPrefsForLocale, type SupportedLocale } from '../i18n';
interface HotkeySettingsContextValue {
prefs: UserPreferences | null;
@@ -64,19 +64,29 @@ export function HotkeySettingsProvider({ children }: { children: ReactNode }) {
const currentPrefs = latestPrefsRef.current;
if (!currentPrefs) return;
const lang = (i18n.resolvedLanguage || i18n.language || '').toLowerCase();
const nextScript: UserPreferences['chineseScriptPreference'] =
const resolvedLocale: SupportedLocale =
lang.startsWith('zh-tw') || lang.includes('hant')
? 'traditional'
? 'zh-TW'
: lang.startsWith('zh-cn') || lang.startsWith('zh')
? 'simplified'
: 'auto';
if (currentPrefs.chineseScriptPreference === nextScript) return;
const merged = { ...currentPrefs, chineseScriptPreference: nextScript };
? 'zh-CN'
: lang.startsWith('ja')
? 'ja'
: lang.startsWith('ko')
? 'ko'
: 'en';
const nextLocalePrefs = outputPrefsForLocale(resolvedLocale);
if (
currentPrefs.chineseScriptPreference === nextLocalePrefs.chineseScriptPreference &&
currentPrefs.outputLanguagePreference === nextLocalePrefs.outputLanguagePreference
) {
return;
}
const merged = { ...currentPrefs, ...nextLocalePrefs };
latestPrefsRef.current = merged;
setPrefs(merged);
void queueSetSettings(current => ({ ...current, chineseScriptPreference: nextScript })).catch(
void queueSetSettings(current => ({ ...current, ...nextLocalePrefs })).catch(
error => {
console.warn('[settings] sync chineseScriptPreference failed', error);
console.warn('[settings] sync locale output preferences failed', error);
},
);
}, [prefs, queueSetSettings]);