1pub mod config;
53pub mod error;
54pub mod translator;
55pub mod types;
56
57pub use config::TranslationConfig;
58pub use error::{TranslationError, TranslationResult};
59pub use translator::Translator;
60pub use types::{TextAnalysis, TextType};
61
62pub async fn translate_text_simple(text: &str, target_lang: &str) -> TranslationResult<String> {
64 let config = TranslationConfig::from_env().unwrap_or_default();
65 let translator = Translator::new(config)?;
66 translator.translate_text(text, target_lang).await
67}
68
69pub async fn translate_html_simple(html: &str, target_lang: &str) -> TranslationResult<String> {
71 let config = TranslationConfig::from_env().unwrap_or_default();
72 let translator = Translator::new(config)?;
73 translator.translate_html(html, target_lang).await
74}
75
76pub fn should_translate(text: &str, target_lang: &str) -> bool {
78 let analysis = analyze_text(text);
79 analysis.is_translatable && analysis.detected_lang.as_deref() != Some(target_lang)
80}
81
82pub fn analyze_text(text: &str) -> TextAnalysis {
84 use types::*;
85
86 if text.trim().is_empty() {
87 return TextAnalysis {
88 is_translatable: false,
89 detected_lang: None,
90 confidence: 0.0,
91 text_type: TextType::Empty,
92 };
93 }
94
95 if text.starts_with("http") || text.contains("://") {
97 return TextAnalysis {
98 is_translatable: false,
99 detected_lang: None,
100 confidence: 1.0,
101 text_type: TextType::Url,
102 };
103 }
104
105 if text.contains('@') && text.split_whitespace().count() == 1 {
107 return TextAnalysis {
108 is_translatable: false,
109 detected_lang: None,
110 confidence: 1.0,
111 text_type: TextType::Email,
112 };
113 }
114
115 let code_chars = text.chars().filter(|&c| "{}[]();".contains(c)).count();
117 let code_ratio = code_chars as f32 / text.len() as f32;
118 if code_ratio > 0.1 {
119 return TextAnalysis {
120 is_translatable: false,
121 detected_lang: None,
122 confidence: code_ratio,
123 text_type: TextType::Code,
124 };
125 }
126
127 let chinese_chars = text.chars().filter(|c| is_chinese_char(*c)).count();
129 let chinese_ratio = chinese_chars as f32 / text.chars().count() as f32;
130
131 if chinese_ratio > 0.5 {
132 return TextAnalysis {
133 is_translatable: false,
134 detected_lang: Some("zh".to_string()),
135 confidence: chinese_ratio,
136 text_type: TextType::Content,
137 };
138 }
139
140 TextAnalysis {
142 is_translatable: true,
143 detected_lang: Some("en".to_string()),
144 confidence: 1.0 - chinese_ratio,
145 text_type: TextType::Content,
146 }
147}
148
149fn is_chinese_char(c: char) -> bool {
151 matches!(c, '\u{4e00}'..='\u{9fff}' | '\u{3400}'..='\u{4dbf}' | '\u{f900}'..='\u{faff}')
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_chinese_char_detection() {
160 assert!(is_chinese_char('中'));
161 assert!(is_chinese_char('文'));
162 assert!(!is_chinese_char('a'));
163 assert!(!is_chinese_char('1'));
164 }
165
166 #[test]
167 fn test_should_translate() {
168 assert!(should_translate("Hello world", "zh"));
170
171 assert!(!should_translate("你好世界", "zh"));
173
174 assert!(!should_translate("https://example.com", "zh"));
176
177 assert!(!should_translate("test@example.com", "zh"));
179
180 assert!(!should_translate("function() { return true; }", "zh"));
182 }
183
184 #[test]
185 fn test_analyze_text() {
186 let analysis = analyze_text("Hello world");
188 assert!(analysis.is_translatable);
189 assert_eq!(analysis.detected_lang, Some("en".to_string()));
190 assert_eq!(analysis.text_type, TextType::Content);
191
192 let analysis = analyze_text("你好世界");
194 assert!(!analysis.is_translatable);
195 assert_eq!(analysis.detected_lang, Some("zh".to_string()));
196 assert_eq!(analysis.text_type, TextType::Content);
197
198 let analysis = analyze_text("https://example.com");
200 assert!(!analysis.is_translatable);
201 assert_eq!(analysis.text_type, TextType::Url);
202
203 let analysis = analyze_text("test@example.com");
205 assert!(!analysis.is_translatable);
206 assert_eq!(analysis.text_type, TextType::Email);
207
208 let analysis = analyze_text("function() { return true; }");
210 assert!(!analysis.is_translatable);
211 assert_eq!(analysis.text_type, TextType::Code);
212
213 let analysis = analyze_text("");
215 assert!(!analysis.is_translatable);
216 assert_eq!(analysis.text_type, TextType::Empty);
217 }
218
219 #[test]
220 fn test_config_validation() {
221 let config = TranslationConfig::default();
223 assert!(config.validate().is_ok());
224
225 let mut config = TranslationConfig::default();
227 config.api_url = "".to_string();
228 assert!(config.validate().is_err());
229
230 let mut config = TranslationConfig::default();
232 config.api_url = "invalid-url".to_string();
233 assert!(config.validate().is_err());
234
235 let mut config = TranslationConfig::default();
237 config.max_concurrent_requests = 0;
238 assert!(config.validate().is_err());
239 }
240}