让界面语言直接驱动日文/韩文输出偏好并避免翻译链路冲突
在现有简繁联动基础上扩展 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:
1 parent
c4dba80f34
commit
04536830a2
15 files changed
+259
-26
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
@@ -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: '表示言語',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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: '인터페이스 언어',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -401,6 +401,8 @@ export const zhCN = {
|
||||
zh: '简体中文',
|
||||
zhTW: '繁體中文',
|
||||
en: 'English',
|
||||
ja: '日本語',
|
||||
ko: '한국어',
|
||||
restartHint: '部分原生菜单(系统托盘等)可能需要重启 App 才会切换。',
|
||||
},
|
||||
about: {
|
||||
|
||||
@@ -403,6 +403,8 @@ export const zhTW: typeof zhCN = {
|
||||
zh: '簡體中文',
|
||||
zhTW: '繁體中文',
|
||||
en: 'English',
|
||||
ja: '日本語',
|
||||
ko: '한국어',
|
||||
restartHint: '部分原生菜單(系統托盤等)可能需要重啓 App 纔會切換。',
|
||||
},
|
||||
about: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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。 */
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in new issue
Block a user