feat(translation): inherit active style pack (#984)
* feat(translation): inherit active style pack * fix(translation): resolve style prompt conflicts
This commit is contained in:
1 parent
ec33bd5dd2
commit
1abc7f595b
10 files changed
+194
-43
No files matched your search
@@ -4158,11 +4158,35 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_polish_translate_prompt_contains_markers_and_target() {
|
||||
let p = build_polish_translate_system_prompt("日本語");
|
||||
assert!(p.contains(POLISH_TRANSLATE_SRC_MARKER));
|
||||
assert!(p.contains(POLISH_TRANSLATE_TGT_MARKER));
|
||||
assert!(p.contains("日本語"));
|
||||
fn translation_prompt_inherits_active_style_and_preserves_structure() {
|
||||
let style_prompt = "# STYLE_PACK_976\n按主题整理为编号列表。\n\n{{HOTWORDS}}";
|
||||
let combined = build_polish_translate_system_prompt(style_prompt, "English");
|
||||
let (system_prompt, _) = crate::polish::compose_polish_prompts(
|
||||
"原始转写",
|
||||
PolishMode::Structured,
|
||||
&["OpenLess".to_string()],
|
||||
&combined,
|
||||
&["简体中文".to_string(), "English".to_string()],
|
||||
ChineseScriptPreference::Auto,
|
||||
crate::types::OutputLanguagePreference::Auto,
|
||||
Some("GitHub"),
|
||||
Some("已有上下文<|OPENLESS_CURSOR|>"),
|
||||
true,
|
||||
);
|
||||
|
||||
assert!(system_prompt.contains("STYLE_PACK_976"));
|
||||
assert!(system_prompt.contains("OpenLess"));
|
||||
assert!(!system_prompt.contains("{{HOTWORDS}}"));
|
||||
assert!(system_prompt.contains("English"));
|
||||
assert!(system_prompt.contains(POLISH_TRANSLATE_SRC_MARKER));
|
||||
assert!(system_prompt.contains(POLISH_TRANSLATE_TGT_MARKER));
|
||||
assert!(system_prompt.contains("# ASR 纠错"));
|
||||
assert!(system_prompt.contains("Token"));
|
||||
assert!(!system_prompt.contains("只输出最终英文译文"));
|
||||
assert!(!system_prompt.contains("不得输出中文"));
|
||||
assert!(system_prompt.contains("列表、编号、段落和 Markdown 结构"));
|
||||
assert!(system_prompt.contains("<cursor_context>"));
|
||||
assert!(system_prompt.contains("# 多轮上下文使用规则"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -4546,6 +4546,7 @@ pub(super) async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
|
||||
&translation_target,
|
||||
mode,
|
||||
&hotword_strs,
|
||||
&style_system_prompt,
|
||||
&working_languages,
|
||||
chinese_script_preference,
|
||||
output_language_preference,
|
||||
|
||||
@@ -331,23 +331,34 @@ pub(super) async fn translate_text(
|
||||
pub(super) const POLISH_TRANSLATE_SRC_MARKER: &str = "[[OPENLESS_POLISHED_SOURCE]]";
|
||||
pub(super) const POLISH_TRANSLATE_TGT_MARKER: &str = "[[OPENLESS_TRANSLATION]]";
|
||||
|
||||
/// 合成"先润色源文、再翻译"的系统提示词:在原翻译 prompt 之上追加"额外输出润色后源文"
|
||||
/// 与严格两段格式(覆盖原 prompt 末尾的"只输出译文")。译文仍是要插入用户光标的主产物,
|
||||
/// 故完整保留原翻译规则;润色后的源文只作对话上下文用,轻量清理即可。
|
||||
pub(super) fn build_polish_translate_system_prompt(target_language: &str) -> String {
|
||||
let base = crate::polish::prompts::translate_system_prompt(target_language);
|
||||
/// 合成"按当前风格润色源文、再翻译"的系统提示词。当前风格包决定源文的结构与语气,
|
||||
/// 翻译规则负责把该结果忠实转换为目标语言;末尾的严格两段格式覆盖两套 prompt 各自的
|
||||
/// "只输出正文"约束。译文用于插入,风格化源文只写入历史供后续上下文复用。
|
||||
pub(super) fn build_polish_translate_system_prompt(
|
||||
style_system_prompt: &str,
|
||||
target_language: &str,
|
||||
) -> String {
|
||||
let translation_rules = crate::polish::prompts::translate_system_prompt_rules(target_language);
|
||||
format!(
|
||||
"{base}\n\n\
|
||||
# 额外输出:润色后的源文(仅用于对话上下文,不展示给用户)\n\
|
||||
在译文之前,先把上面的原始转写**按它本来的语言**润色一遍:去掉口癖(嗯 / 那个 / um)、\
|
||||
补必要标点、纠正明显的识别错误,但**不翻译、不改写风格、不增删意思**。\n\n\
|
||||
# 输出格式(覆盖上面\u{201C}只输出译文\u{201D}的说明,严格遵守)\n\
|
||||
"# 任务(按当前风格润色并翻译)\n\
|
||||
先完整执行下方的当前风格包规则,把原始 ASR 转写整理为同语言的风格化源文;\
|
||||
再把该风格化源文翻译成\u{300C}{lang}\u{300D}。翻译对象是风格化源文,不是原始转写。\n\n\
|
||||
# 当前风格包规则\n\
|
||||
{style}\n\n\
|
||||
# 翻译规则\n\
|
||||
{translation_rules}\n\n\
|
||||
# 两阶段约束\n\
|
||||
- 风格包决定内容的组织方式、语气和信息密度;翻译不得把它还原成普通连续段落。\n\
|
||||
- 译文必须保留风格化源文的列表、编号、段落和 Markdown 结构,并忠实保留原意。\n\
|
||||
- 风格化源文保持原语言;最终译文只使用\u{300C}{lang}\u{300D}表达需要翻译的正文。\n\n\
|
||||
# 输出格式(优先级最高,覆盖上面所有\u{201C}只输出正文\u{201D}的说明)\n\
|
||||
严格按下面两段输出,两个标记必须原样出现、各占一行,标记之外不要有任何多余文字:\n\
|
||||
{src}\n\
|
||||
(这里放润色后的源文,保持原语言)\n\
|
||||
(这里放按当前风格包完整润色后的源文,保持原语言)\n\
|
||||
{tgt}\n\
|
||||
(这里放翻译成\u{300C}{lang}\u{300D}的译文)",
|
||||
base = base,
|
||||
(这里放保留相同风格与结构的\u{300C}{lang}\u{300D}译文)",
|
||||
style = style_system_prompt.trim(),
|
||||
translation_rules = translation_rules,
|
||||
src = POLISH_TRANSLATE_SRC_MARKER,
|
||||
tgt = POLISH_TRANSLATE_TGT_MARKER,
|
||||
lang = target_language,
|
||||
@@ -386,6 +397,7 @@ pub(super) async fn polish_and_translate_or_passthrough(
|
||||
target_language: &str,
|
||||
mode: PolishMode,
|
||||
hotwords: &[String],
|
||||
style_system_prompt: &str,
|
||||
working_languages: &[String],
|
||||
chinese_script_preference: ChineseScriptPreference,
|
||||
output_language_preference: OutputLanguagePreference,
|
||||
@@ -397,7 +409,7 @@ pub(super) async fn polish_and_translate_or_passthrough(
|
||||
llm_elapsed_ms: &mut Option<u64>,
|
||||
multimodal: bool,
|
||||
) -> (String, Option<String>, Option<String>) {
|
||||
let system_prompt = build_polish_translate_system_prompt(target_language);
|
||||
let system_prompt = build_polish_translate_system_prompt(style_system_prompt, target_language);
|
||||
match polish_text(
|
||||
&raw.text,
|
||||
mode,
|
||||
|
||||
@@ -2155,28 +2155,53 @@ pub mod prompts {
|
||||
/// 此 prompt 之上还有 working_languages_premise 拼出的"# 上下文"前提。
|
||||
///
|
||||
/// target_language == "English"(含 "美式英文" / "英文" / "english" 等别名)时整段切到
|
||||
/// EN_TRANSLATE_SYSTEM_PROMPT —— 不再走通用 base,避免通用规则与 EN 专属的「ASR 纠错优先
|
||||
/// EN_TRANSLATE_SYSTEM_RULES —— 不再走通用 base,避免通用规则与 EN 专属的「ASR 纠错优先
|
||||
/// + 中→英技术词规范化」相互稀释。来源:社区「重写为英文」prompt,精简整合后整体注入。
|
||||
pub fn translate_system_prompt(target_language: &str) -> String {
|
||||
// issue #609 F-02:翻译路径与 polish 路径对齐——在系统提示末尾追加对抗式注入防御措辞。
|
||||
// 本函数是所有翻译路径(OpenAI 兼容 / Gemini 的 compose_translate_prompts、Codex
|
||||
// translate_to、润色+翻译合一的 build_polish_translate_system_prompt)写给模型的唯一
|
||||
// base,把防御嵌在这里令每个调用方自动覆盖,杜绝调用点遗漏。LLM 不是安全边界,纵深防御。
|
||||
// translate_to)写给模型的唯一 base,把防御嵌在这里令每个调用方自动覆盖,杜绝调用点遗漏。
|
||||
// LLM 不是安全边界,纵深防御。
|
||||
let base = translate_system_prompt_base(target_language);
|
||||
format!("{}\n\n{}", base, polish_injection_defense())
|
||||
}
|
||||
|
||||
/// 可嵌入其它工作流的翻译规则,不包含单段翻译的输出格式约束。
|
||||
///
|
||||
/// 润色+翻译流程需要同时输出原语言风格化源文和目标语言译文;复用
|
||||
/// translate_system_prompt 会把“只输出译文 / 不得输出中文”等单段输出规则一并带入,
|
||||
/// 与两段格式冲突。因此这里只复用 ASR 纠错、术语和忠实翻译规则。
|
||||
pub fn translate_system_prompt_rules(target_language: &str) -> String {
|
||||
translate_system_prompt_rules_base(target_language)
|
||||
}
|
||||
|
||||
fn translate_system_prompt_base(target_language: &str) -> String {
|
||||
let rules = translate_system_prompt_rules_base(target_language);
|
||||
if is_english_target(target_language) {
|
||||
return EN_TRANSLATE_SYSTEM_PROMPT.to_string();
|
||||
return format!(
|
||||
"{rules}\n\n{output}",
|
||||
output = EN_TRANSLATE_OUTPUT_INSTRUCTIONS
|
||||
);
|
||||
}
|
||||
format!(
|
||||
"# 任务(翻译输出)\n\
|
||||
把下面收到的一段语音转写翻译成 \u{300C}{lang}\u{300D}。\n\
|
||||
这是用户对着语音输入工具说的话——他正在某个 app 的输入框前,\
|
||||
转译结果会直接被插入到光标位置。\n\
|
||||
\n\
|
||||
# 翻译规则\n\
|
||||
转译结果会直接被插入到光标位置。\n\n\
|
||||
{rules}\n\n\
|
||||
{output}",
|
||||
lang = target_language,
|
||||
rules = rules,
|
||||
output = COMMON_TRANSLATE_OUTPUT_INSTRUCTIONS,
|
||||
)
|
||||
}
|
||||
|
||||
fn translate_system_prompt_rules_base(target_language: &str) -> String {
|
||||
if is_english_target(target_language) {
|
||||
return EN_TRANSLATE_SYSTEM_RULES.to_string();
|
||||
}
|
||||
format!(
|
||||
"# 翻译规则\n\
|
||||
## 必须保留原文(不要翻译)\n\
|
||||
- 人名、地名、品牌名(OpenAI、Tauri、字节跳动、张三 等)。\n\
|
||||
- 代码标识符、技术术语(useState、async/await、HTTP、Rust crate 名 等)。\n\
|
||||
@@ -2196,15 +2221,15 @@ pub mod prompts {
|
||||
## 边界 case\n\
|
||||
- 转写非常短(一两个字)也照译,\u{4E0D}因为短就硬补内容。\n\
|
||||
- 转写是命令式(\"加个空格 / 删除最后一行\")时,照原意翻译,\u{4E0D}改成陈述句。\n\
|
||||
- 转写全是 fillers(\"嗯嗯啊那个\")时,输出空字符串。\n\
|
||||
\n\
|
||||
# 输出\n\
|
||||
只输出翻译后的正文,\u{4E0D}带 \u{300C}翻译:\u{300D}\u{300C}译文:\u{300D}\u{300C}Translation:\u{300D}之类前缀,\
|
||||
\u{4E0D}加引号、\u{4E0D}加 markdown 围栏。",
|
||||
lang = target_language
|
||||
- 转写全是 fillers(\"嗯嗯啊那个\")时,输出空字符串。",
|
||||
lang = target_language,
|
||||
)
|
||||
}
|
||||
|
||||
const COMMON_TRANSLATE_OUTPUT_INSTRUCTIONS: &str = "# 输出\n\
|
||||
只输出翻译后的正文,\u{4E0D}带 \u{300C}翻译:\u{300D}\u{300C}译文:\u{300D}\u{300C}Translation:\u{300D}之类前缀,\
|
||||
\u{4E0D}加引号、\u{4E0D}加 markdown 围栏。";
|
||||
|
||||
/// target_language 是否指向英语 —— 容忍用户在偏好里写 "English" / "english" / "美式英文" /
|
||||
/// "英文" / "British English" 等几种写法。匹配松一点没坏处:误命中只会让模型走 EN 专属
|
||||
/// prompt,对纯中文 / 日文等目标本来就不会被选中。
|
||||
@@ -2227,7 +2252,7 @@ pub mod prompts {
|
||||
/// - 比通用翻译 prompt 更窄、更强:ASR 纠错优先于逐字翻译;英文要求自然 idiomatic,
|
||||
/// 不接受 Chinglish 直译。
|
||||
/// - 来源:社区「重写为英文」prompt(imported.573e86a1bcf44dbb...),整合精简后注入。
|
||||
const EN_TRANSLATE_SYSTEM_PROMPT: &str = "# 任务(中文转写 → 英文翻译)\n\
|
||||
const EN_TRANSLATE_SYSTEM_RULES: &str = "# 任务(中文转写 → 英文翻译)\n\
|
||||
你是一名中译英助手,专门处理语音识别(ASR)后的中文技术文本。\n\
|
||||
用户的转写不是可靠原文:可能有错别字、同音字、近音字、断句缺失、术语误识别、\
|
||||
英文术语被中文音译。**你的任务不是逐字翻译,而是先理解用户真实意图,纠正显然的识别错误,\
|
||||
@@ -2240,7 +2265,6 @@ pub mod prompts {
|
||||
3. 把中文音译还原为标准英文技术术语。\n\
|
||||
4. 整理混乱、口语化或重复的表达。\n\
|
||||
5. 在不改变用户真实意图的前提下,翻译成自然、专业的英文。\n\
|
||||
6. **只输出最终英文译文**。\n\
|
||||
\n\
|
||||
# ASR 纠错(按置信度分级)\n\
|
||||
- 高置信度(错误明显、正确写法唯一)→ 直接替换,不保留原词、不加说明。\n\
|
||||
@@ -2287,13 +2311,13 @@ pub mod prompts {
|
||||
\n\
|
||||
# 禁止\n\
|
||||
1. \u{4E0D}得逐字翻译明显错误的 ASR 文本。\n\
|
||||
2. \u{4E0D}得输出中文(不要给出中文润色稿、对比表、原文回显)。\n\
|
||||
3. \u{4E0D}得输出解释、修改说明、change log、思路过程。\n\
|
||||
4. \u{4E0D}得为了流畅而删减重要信息,也\u{4E0D}得添加用户未表达过的新事实、链接、路径、字段、步骤。\n\
|
||||
5. \u{4E0D}得改变用户真实意图。\n\
|
||||
\n\
|
||||
# 输出\n\
|
||||
只输出最终英文译文。\u{4E0D}带 \u{300C}翻译:\u{300D}\u{300C}译文:\u{300D}\u{300C}Translation:\u{300D}\
|
||||
2. \u{4E0D}得输出解释、修改说明、change log、思路过程。\n\
|
||||
3. \u{4E0D}得为了流畅而删减重要信息,也\u{4E0D}得添加用户未表达过的新事实、链接、路径、字段、步骤。\n\
|
||||
4. \u{4E0D}得改变用户真实意图。";
|
||||
|
||||
const EN_TRANSLATE_OUTPUT_INSTRUCTIONS: &str = "# 输出\n\
|
||||
只输出最终英文译文。\u{4E0D}得输出中文(不要给出中文润色稿、对比表、原文回显)。\
|
||||
\u{4E0D}带 \u{300C}翻译:\u{300D}\u{300C}译文:\u{300D}\u{300C}Translation:\u{300D}\
|
||||
\u{4E4B}\u{7C7B}前缀,\u{4E0D}加引号、\u{4E0D}加 markdown 围栏、\u{4E0D}加代码 fence。";
|
||||
}
|
||||
|
||||
@@ -3843,7 +3867,7 @@ mod tests {
|
||||
#[test]
|
||||
fn injection_defense_present_in_translate_system_prompt() {
|
||||
// issue #609 F-02:翻译路径(EN 专用 / 通用 base)必须与 polish 路径一样带对抗式注入防御。
|
||||
// 覆盖英文目标(走 EN_TRANSLATE_SYSTEM_PROMPT)与非英文目标(走通用 base)两条分支。
|
||||
// 覆盖英文目标(走 EN_TRANSLATE_SYSTEM_RULES)与非英文目标(走通用 base)两条分支。
|
||||
for target in ["English", "繁体中文", "日本語"] {
|
||||
let p = prompts::translate_system_prompt(target);
|
||||
assert!(
|
||||
@@ -3932,7 +3956,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn translate_prompt_swaps_to_en_dedicated_when_target_is_english() {
|
||||
// 英文目标:整段切到 EN_TRANSLATE_SYSTEM_PROMPT,不再带通用 base 的 \"# 任务(翻译输出)\" 标题。
|
||||
// 英文目标:整段切到 EN_TRANSLATE_SYSTEM_RULES,不再带通用 base 的 \"# 任务(翻译输出)\" 标题。
|
||||
let en = prompts::translate_system_prompt("English");
|
||||
assert!(
|
||||
en.contains("# 任务(中文转写 → 英文翻译)"),
|
||||
|
||||
@@ -629,6 +629,11 @@ export const en: typeof zhCN = {
|
||||
disabled: 'Disabled (Shift does nothing)',
|
||||
sameAsWorking: 'The target matches your only working language, so translation cannot take effect — Shift will just run a normal polish. Pick a different target, or add another working language above.',
|
||||
},
|
||||
style: {
|
||||
title: 'Translation style',
|
||||
desc: 'Automatically inherits the active style pack from the Style page.',
|
||||
unavailable: 'Unavailable',
|
||||
},
|
||||
save: {
|
||||
workingFailed: 'Failed to save working languages. Please try again.',
|
||||
targetFailed: 'Failed to save translation target. Please try again.',
|
||||
|
||||
@@ -631,6 +631,11 @@ export const ja: typeof zhCN = {
|
||||
disabled: '無効(Shift で翻訳を発動しない)',
|
||||
sameAsWorking: 'ターゲット言語が唯一の作業言語と同じため、翻訳は発動しません(Shift を押しても通常の整文になります)。別のターゲットを選ぶか、上で作業言語を追加してください。',
|
||||
},
|
||||
style: {
|
||||
title: '翻訳スタイル',
|
||||
desc: '「スタイル」ページで現在有効なスタイルパックを自動的に引き継ぎます。',
|
||||
unavailable: '取得できません',
|
||||
},
|
||||
save: {
|
||||
workingFailed: '作業言語の保存に失敗しました。もう一度お試しください。',
|
||||
targetFailed: '翻訳ターゲット言語の保存に失敗しました。もう一度お試しください。',
|
||||
|
||||
@@ -631,6 +631,11 @@ export const ko: typeof zhCN = {
|
||||
disabled: '비활성화 (Shift 로 번역 발동 안 함)',
|
||||
sameAsWorking: '대상 언어가 유일한 작업 언어와 같아 번역이 실행되지 않습니다. Shift 를 눌러도 일반 정리로 처리됩니다. 다른 대상 언어를 고르거나 위에서 작업 언어를 추가하세요.',
|
||||
},
|
||||
style: {
|
||||
title: '번역 스타일',
|
||||
desc: '「스타일」 페이지에서 현재 활성화된 스타일 팩을 자동으로 사용합니다.',
|
||||
unavailable: '사용할 수 없음',
|
||||
},
|
||||
save: {
|
||||
workingFailed: '작업 언어 저장에 실패했습니다. 다시 시도하세요.',
|
||||
targetFailed: '번역 대상 언어 저장에 실패했습니다. 다시 시도하세요.',
|
||||
|
||||
@@ -627,6 +627,11 @@ export const zhCN = {
|
||||
disabled: '不启用(Shift 按下不触发翻译)',
|
||||
sameAsWorking: '目标语言与你唯一的工作语言相同,翻译不会生效:按 Shift 仍按普通润色处理。换一个目标语言,或在上方多勾选一个工作语言。',
|
||||
},
|
||||
style: {
|
||||
title: '翻译风格',
|
||||
desc: '自动继承「风格」页当前激活的风格包。',
|
||||
unavailable: '暂不可用',
|
||||
},
|
||||
save: {
|
||||
workingFailed: '工作语言保存失败,请重试。',
|
||||
targetFailed: '翻译目标语言保存失败,请重试。',
|
||||
|
||||
@@ -629,6 +629,11 @@ export const zhTW: typeof zhCN = {
|
||||
disabled: '不啓用(Shift 按下不觸發翻譯)',
|
||||
sameAsWorking: '目標語言與你唯一的工作語言相同,翻譯不會生效:按 Shift 仍按普通潤色處理。換一個目標語言,或在上方多勾選一個工作語言。',
|
||||
},
|
||||
style: {
|
||||
title: '翻譯風格',
|
||||
desc: '自動沿用「風格」頁目前啓用的風格包。',
|
||||
unavailable: '暫時無法取得',
|
||||
},
|
||||
save: {
|
||||
workingFailed: '工作語言保存失敗,請重試。',
|
||||
targetFailed: '翻譯目標語言保存失敗,請重試。',
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Card, PageHeader } from './_atoms';
|
||||
import { SavedToast } from '../components/SavedToast';
|
||||
import { SelectLite } from '../components/ui/SelectLite';
|
||||
import { listStylePacks } from '../lib/ipc';
|
||||
import { SUPPORTED_LANGUAGES } from '../lib/types';
|
||||
import { isTranslationEnabled, isTranslationTargetRedundant } from '../lib/translationTarget';
|
||||
import { useHotkeySettings } from '../state/HotkeySettingsContext';
|
||||
@@ -22,12 +23,36 @@ export function Translation() {
|
||||
const { prefs, loading, error, refresh, updatePrefs: savePrefs } = useHotkeySettings();
|
||||
const [saveState, setSaveState] = useState<SaveState>('idle');
|
||||
const [saveMessage, setSaveMessage] = useState('');
|
||||
const [activeStylePackName, setActiveStylePackName] = useState<string | null>(null);
|
||||
const [stylePackLoadFailed, setStylePackLoadFailed] = useState(false);
|
||||
const statusTimer = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (statusTimer.current !== null) window.clearTimeout(statusTimer.current);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const activeStylePackId = prefs?.activeStylePackId;
|
||||
let cancelled = false;
|
||||
setActiveStylePackName(null);
|
||||
setStylePackLoadFailed(false);
|
||||
void listStylePacks()
|
||||
.then(packs => {
|
||||
if (cancelled) return;
|
||||
const activePack =
|
||||
packs.find(pack => pack.active && pack.enabled) ??
|
||||
packs.find(pack => pack.id === activeStylePackId && pack.enabled);
|
||||
setActiveStylePackName(activePack?.name ?? null);
|
||||
setStylePackLoadFailed(!activePack);
|
||||
})
|
||||
.catch(loadError => {
|
||||
console.warn('[translation] failed to load active style pack', loadError);
|
||||
if (!cancelled) setStylePackLoadFailed(true);
|
||||
});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [prefs?.activeStylePackId]);
|
||||
|
||||
const showSaveStatus = (state: SaveState, message: string, temporary = false) => {
|
||||
if (statusTimer.current !== null) {
|
||||
window.clearTimeout(statusTimer.current);
|
||||
@@ -237,6 +262,46 @@ export function Translation() {
|
||||
ariaLabel={t('translation.target.title')}
|
||||
style={{ width: '100%', maxWidth: 360, fontSize: 13 }}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
marginTop: 12,
|
||||
paddingTop: 12,
|
||||
borderTop: '0.5px solid var(--ol-line)',
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--ol-ink-2)' }}>
|
||||
{t('translation.style.title')}
|
||||
</div>
|
||||
<div style={{ marginTop: 2, fontSize: 11.5, color: 'var(--ol-ink-4)', lineHeight: 1.5 }}>
|
||||
{t('translation.style.desc')}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
role="status"
|
||||
style={{
|
||||
flex: '0 0 auto',
|
||||
maxWidth: 180,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
padding: '3px 9px',
|
||||
borderRadius: 999,
|
||||
background: 'rgba(37,99,235,0.08)',
|
||||
color: 'var(--ol-blue)',
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{stylePackLoadFailed
|
||||
? t('translation.style.unavailable')
|
||||
: activeStylePackName ?? t('common.loading')}
|
||||
</span>
|
||||
</div>
|
||||
{redundantTarget && (
|
||||
<div
|
||||
role="status"
|
||||
|
||||
Reference in new issue
Block a user