返回 CodeWhale
translation.rs
根目录 / crates / tui / src / tui / translation.rs
1 //! Post-hoc translation interception layer.
2 //!
3 //! When output translation is enabled (`/translate`), this module provides
4 //! the interception logic that detects English model output and replaces it
5 //! with Chinese translations before display. The primary mechanism is the
6 //! system prompt instruction in `prompts.rs`; this module is the fallback
7 //! for model output that leaks English despite the instruction.
8 //!
9 //! ## Architecture
10 //!
11 //! - `needs_translation()` — heuristic to detect if text is predominantly
12 //! English and should be translated.
13 //! - `translate_text()` — calls the current session model through a
14 //! shared `DeepSeekClient` to translate text to the current locale. The dedicated
15 //! translation agent receives only the source text and returns only the
16 //! translation — no tool calls, no conversation history.
17 //! - `TranslationStatus` — tracks per-message translation status in the UI.
18
19 use anyhow::Result;
20
21 use crate::client::DeepSeekClient;
22
23 /// Heuristic threshold: if more than this fraction of alphabetic characters
24 /// are Latin (A-Z / a-z), the text is considered English.
25 const ENGLISH_LATIN_RATIO_THRESHOLD: f64 = 0.6;
26
27 /// Minimum number of alphabetic characters required before applying the
28 /// heuristic — avoids false positives on short mixed-language strings.
29 const MIN_ALPHA_CHARS_FOR_DETECTION: usize = 10;
30
31 /// How many Latin-letter "information units" each CJK character is worth.
32 /// A single CJK character carries roughly the information of a short English
33 /// word (2–4 letters), so we weight CJK at 3× for fair comparison.
34 const CJK_CHAR_WEIGHT: usize = 3;
35
36 /// Detect if text content is predominantly English and should be translated.
37 ///
38 /// The heuristic compares CJK characters (weighted) against Latin letters.
39 /// CJK characters carry much more information per glyph, so a string with
40 /// even a modest number of Chinese characters among English words will not
41 /// be flagged.
42 #[must_use]
43 pub fn needs_translation(text: &str) -> bool {
44 let mut latin_count = 0usize;
45 let mut cjk_count = 0usize;
46
47 for ch in text.chars() {
48 if ch.is_ascii_alphabetic() {
49 latin_count += 1;
50 } else if is_cjk(ch) {
51 cjk_count += 1;
52 }
53 }
54
55 let total_alpha = latin_count + (cjk_count * CJK_CHAR_WEIGHT);
56
57 if total_alpha < MIN_ALPHA_CHARS_FOR_DETECTION {
58 return false;
59 }
60
61 // If weighted CJK dominates, it's already Chinese — no translation needed.
62 if (cjk_count * CJK_CHAR_WEIGHT) > latin_count {
63 return false;
64 }
65
66 let ratio = latin_count as f64 / total_alpha as f64;
67 ratio >= ENGLISH_LATIN_RATIO_THRESHOLD
68 }
69
70 /// Check if a character is in the CJK Unified Ideographs block or is a
71 /// common Chinese/Japanese/Korean character.
72 fn is_cjk(ch: char) -> bool {
73 matches!(
74 ch,
75 '\u{4E00}'..='\u{9FFF}' // CJK Unified Ideographs
76 | '\u{3400}'..='\u{4DBF}' // CJK Unified Ideographs Extension A
77 | '\u{2E80}'..='\u{2EFF}' // CJK Radicals Supplement
78 | '\u{3000}'..='\u{303F}' // CJK Symbols and Punctuation
79 | '\u{FF00}'..='\u{FFEF}' // Halfwidth and Fullwidth Forms
80 | '\u{3040}'..='\u{309F}' // Hiragana
81 | '\u{30A0}'..='\u{30FF}' // Katakana
82 )
83 }
84
85 /// Translate text to the requested target language using a dedicated
86 /// translation agent.
87 ///
88 /// This is a lightweight, focused API call — no streaming, no tool calls,
89 /// no conversation history. The agent's only role is translation.
90 ///
91 /// # Errors
92 ///
93 /// Returns an error if the API call fails or the response is malformed.
94 pub async fn translate_text(
95 text: &str,
96 client: &DeepSeekClient,
97 model: &str,
98 target_language: &str,
99 ) -> Result<String> {
100 client.translate(text, model, target_language).await
101 }
102
103 /// Status of a translation operation for a single message.
104 #[derive(Debug, Clone, PartialEq, Eq)]
105 #[allow(dead_code)]
106 pub enum TranslationStatus {
107 /// No translation needed (already Chinese or not enough text).
108 NotNeeded,
109 /// Translation is pending — the original English is still displayed
110 /// with an indicator.
111 Pending,
112 /// Translation completed successfully.
113 Done,
114 /// Translation failed — original English displayed with fallback note.
115 Failed,
116 }
117
118 #[cfg(test)]
119 mod tests {
120 use super::*;
121
122 #[test]
123 fn short_text_avoids_false_positive() {
124 assert!(!needs_translation("hi"));
125 assert!(!needs_translation("ok"));
126 }
127
128 #[test]
129 fn english_text_detected() {
130 assert!(needs_translation(
131 "This is a message from the assistant explaining how the code works."
132 ));
133 }
134
135 #[test]
136 fn chinese_text_not_detected() {
137 assert!(!needs_translation(
138 "这是助手的一条中文回复,解释了代码的工作原理。"
139 ));
140 }
141
142 #[test]
143 fn mixed_mostly_english_detected() {
144 assert!(needs_translation(
145 "The function handle_request takes a Request param and returns a Response."
146 ));
147 }
148
149 #[test]
150 fn mixed_mostly_chinese_not_detected() {
151 assert!(!needs_translation(
152 "这个 handle_request 函数接收一个 Request 参数并返回 Response。"
153 ));
154 }
155
156 #[test]
157 fn code_with_short_labels_not_falsely_detected() {
158 assert!(!needs_translation("let x = 1; let y = 2;"));
159 }
160
161 #[test]
162 fn long_english_code_is_detected() {
163 assert!(needs_translation(
164 "function calculateTotalRevenueForQuarterlyReport() { return; }"
165 ));
166 }
167
168 #[test]
169 fn js_comments_in_english_detected() {
170 assert!(needs_translation(
171 "// This is a JavaScript function that handles user authentication\nfunction login() {}"
172 ));
173 }
174 }
175
175 lines RUST