返回 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 //!
14 //! The event loop dispatches focused translation requests through the exact
15 //! provider/model client frozen for the originating turn. The dedicated agent
16 //! receives only the source text and returns only the translation — no tool
17 //! calls or conversation history.
18 //!
19
20 /// Heuristic threshold: if more than this fraction of alphabetic characters
21 /// are Latin (A-Z / a-z), the text is considered English.
22 const ENGLISH_LATIN_RATIO_THRESHOLD: f64 = 0.6;
23
24 /// Minimum number of alphabetic characters required before applying the
25 /// heuristic — avoids false positives on short mixed-language strings.
26 const MIN_ALPHA_CHARS_FOR_DETECTION: usize = 10;
27
28 /// How many Latin-letter "information units" each CJK character is worth.
29 /// A single CJK character carries roughly the information of a short English
30 /// word (2–4 letters), so we weight CJK at 3× for fair comparison.
31 const CJK_CHAR_WEIGHT: usize = 3;
32
33 /// Detect if text content is predominantly English and should be translated.
34 ///
35 /// The heuristic compares CJK characters (weighted) against Latin letters.
36 /// CJK characters carry much more information per glyph, so a string with
37 /// even a modest number of Chinese characters among English words will not
38 /// be flagged.
39 #[must_use]
40 pub fn needs_translation(text: &str) -> bool {
41 let mut latin_count = 0usize;
42 let mut cjk_count = 0usize;
43
44 for ch in text.chars() {
45 if ch.is_ascii_alphabetic() {
46 latin_count += 1;
47 } else if is_cjk(ch) {
48 cjk_count += 1;
49 }
50 }
51
52 let total_alpha = latin_count + (cjk_count * CJK_CHAR_WEIGHT);
53
54 if total_alpha < MIN_ALPHA_CHARS_FOR_DETECTION {
55 return false;
56 }
57
58 // If weighted CJK dominates, it's already Chinese — no translation needed.
59 if (cjk_count * CJK_CHAR_WEIGHT) > latin_count {
60 return false;
61 }
62
63 let ratio = latin_count as f64 / total_alpha as f64;
64 ratio >= ENGLISH_LATIN_RATIO_THRESHOLD
65 }
66
67 /// Check if a character is in the CJK Unified Ideographs block or is a
68 /// common Chinese/Japanese/Korean character.
69 fn is_cjk(ch: char) -> bool {
70 matches!(
71 ch,
72 '\u{4E00}'..='\u{9FFF}' // CJK Unified Ideographs
73 | '\u{3400}'..='\u{4DBF}' // CJK Unified Ideographs Extension A
74 | '\u{2E80}'..='\u{2EFF}' // CJK Radicals Supplement
75 | '\u{3000}'..='\u{303F}' // CJK Symbols and Punctuation
76 | '\u{FF00}'..='\u{FFEF}' // Halfwidth and Fullwidth Forms
77 | '\u{3040}'..='\u{309F}' // Hiragana
78 | '\u{30A0}'..='\u{30FF}' // Katakana
79 )
80 }
81
82 #[cfg(test)]
83 mod tests {
84 use super::*;
85
86 #[test]
87 fn short_text_avoids_false_positive() {
88 assert!(!needs_translation("hi"));
89 assert!(!needs_translation("ok"));
90 }
91
92 #[test]
93 fn english_text_detected() {
94 assert!(needs_translation(
95 "This is a message from the assistant explaining how the code works."
96 ));
97 }
98
99 #[test]
100 fn chinese_text_not_detected() {
101 assert!(!needs_translation(
102 "这是助手的一条中文回复,解释了代码的工作原理。"
103 ));
104 }
105
106 #[test]
107 fn mixed_mostly_english_detected() {
108 assert!(needs_translation(
109 "The function handle_request takes a Request param and returns a Response."
110 ));
111 }
112
113 #[test]
114 fn mixed_mostly_chinese_not_detected() {
115 assert!(!needs_translation(
116 "这个 handle_request 函数接收一个 Request 参数并返回 Response。"
117 ));
118 }
119
120 #[test]
121 fn code_with_short_labels_not_falsely_detected() {
122 assert!(!needs_translation("let x = 1; let y = 2;"));
123 }
124
125 #[test]
126 fn long_english_code_is_detected() {
127 assert!(needs_translation(
128 "function calculateTotalRevenueForQuarterlyReport() { return; }"
129 ));
130 }
131
132 #[test]
133 fn js_comments_in_english_detected() {
134 assert!(needs_translation(
135 "// This is a JavaScript function that handles user authentication\nfunction login() {}"
136 ));
137 }
138 }
139
139 lines RUST