subx_cli/core/
language.rs1use regex::Regex;
17use std::collections::HashMap;
18use std::path::Path;
19
20#[derive(Debug, Clone, PartialEq)]
22pub enum LanguageSource {
23 Directory,
25 Filename,
27 Extension,
29}
30impl Default for LanguageDetector {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct LanguageInfo {
39 pub code: String,
41 pub source: LanguageSource,
43 pub confidence: f32,
45}
46
47pub struct LanguageDetector {
49 language_codes: HashMap<String, String>,
50 directory_patterns: Vec<String>,
51 filename_patterns: Vec<Regex>,
52}
53
54impl LanguageDetector {
55 pub fn new() -> Self {
59 let mut language_codes = HashMap::new();
61 language_codes.insert("tc".to_string(), "tc".to_string());
63 language_codes.insert("繁中".to_string(), "tc".to_string()); language_codes.insert("繁體".to_string(), "tc".to_string()); language_codes.insert("cht".to_string(), "tc".to_string());
66 language_codes.insert("sc".to_string(), "sc".to_string());
68 language_codes.insert("简中".to_string(), "sc".to_string()); language_codes.insert("简体".to_string(), "sc".to_string()); language_codes.insert("chs".to_string(), "sc".to_string());
71 language_codes.insert("traditional-chinese".to_string(), "tc".to_string());
76 language_codes.insert("traditional_chinese".to_string(), "tc".to_string());
77 language_codes.insert("zh-hant".to_string(), "tc".to_string());
78 language_codes.insert("zh_hant".to_string(), "tc".to_string());
79 language_codes.insert("simplified-chinese".to_string(), "sc".to_string());
80 language_codes.insert("simplified_chinese".to_string(), "sc".to_string());
81 language_codes.insert("zh-hans".to_string(), "sc".to_string());
82 language_codes.insert("zh_hans".to_string(), "sc".to_string());
83 language_codes.insert("en".to_string(), "en".to_string());
85 language_codes.insert("英文".to_string(), "en".to_string()); language_codes.insert("english".to_string(), "en".to_string());
87 language_codes.insert("eng".to_string(), "en".to_string());
88 language_codes.insert("ja".to_string(), "ja".to_string());
90 language_codes.insert("jpn".to_string(), "ja".to_string());
91 language_codes.insert("japanese".to_string(), "ja".to_string());
92 language_codes.insert("ko".to_string(), "ko".to_string());
93 language_codes.insert("kor".to_string(), "ko".to_string());
94 language_codes.insert("korean".to_string(), "ko".to_string());
95 language_codes.insert("fr".to_string(), "fr".to_string());
96 language_codes.insert("fra".to_string(), "fr".to_string());
97 language_codes.insert("fre".to_string(), "fr".to_string());
98 language_codes.insert("french".to_string(), "fr".to_string());
99 language_codes.insert("de".to_string(), "de".to_string());
100 language_codes.insert("deu".to_string(), "de".to_string());
101 language_codes.insert("ger".to_string(), "de".to_string());
102 language_codes.insert("german".to_string(), "de".to_string());
103 language_codes.insert("es".to_string(), "es".to_string());
104 language_codes.insert("spa".to_string(), "es".to_string());
105 language_codes.insert("spanish".to_string(), "es".to_string());
106 language_codes.insert("pt".to_string(), "pt".to_string());
107 language_codes.insert("por".to_string(), "pt".to_string());
108 language_codes.insert("portuguese".to_string(), "pt".to_string());
109 language_codes.insert("ru".to_string(), "ru".to_string());
110 language_codes.insert("rus".to_string(), "ru".to_string());
111 language_codes.insert("russian".to_string(), "ru".to_string());
112
113 let filename_patterns = vec![
114 Regex::new(r"\.([a-z]{2,3})\.").unwrap(), Regex::new(r"_([a-z]{2,3})\.").unwrap(), Regex::new(r"-([a-z]{2,3})\.").unwrap(), ];
118
119 Self {
120 language_codes,
121 directory_patterns: vec!["tc".to_string(), "sc".to_string(), "en".to_string()],
122 filename_patterns,
123 }
124 }
125 pub fn detect_from_path(&self, path: &Path) -> Option<LanguageInfo> {
131 if let Some(lang) = self.detect_from_directory(path) {
132 return Some(lang);
133 }
134 if let Some(lang) = self.detect_from_filename(path) {
135 return Some(lang);
136 }
137 None
138 }
139
140 pub fn normalize(&self, raw: &str) -> Option<String> {
163 let trimmed = raw.trim();
164 if trimmed.is_empty() {
165 return None;
166 }
167 let lower = trimmed.to_lowercase();
168 if let Some(code) = self.language_codes.get(&lower) {
169 return Some(code.clone());
170 }
171 if let Some(code) = self.language_codes.get(trimmed) {
174 return Some(code.clone());
175 }
176 Some(lower)
177 }
178
179 pub fn get_primary_language(&self, path: &Path) -> Option<String> {
185 self.detect_all_languages(path)
186 .into_iter()
187 .next()
188 .map(|lang| lang.code)
189 }
190
191 pub fn detect_all_languages(&self, path: &Path) -> Vec<LanguageInfo> {
195 let mut langs = Vec::new();
196 if let Some(dir_lang) = self.detect_from_directory(path) {
197 langs.push(dir_lang);
198 }
199 if let Some(file_lang) = self.detect_from_filename(path) {
200 langs.push(file_lang);
201 }
202 langs.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
203 langs.dedup_by(|a, b| a.code == b.code);
204 langs
205 }
206
207 fn detect_from_directory(&self, path: &Path) -> Option<LanguageInfo> {
208 for comp in path.components() {
209 if let Some(s) = comp.as_os_str().to_str() {
210 let key = s.to_lowercase();
211 if let Some(code) = self.language_codes.get(&key) {
212 return Some(LanguageInfo {
213 code: code.clone(),
214 source: LanguageSource::Directory,
215 confidence: 0.9,
216 });
217 }
218 }
219 }
220 None
221 }
222
223 fn detect_from_filename(&self, path: &Path) -> Option<LanguageInfo> {
224 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
225 for re in &self.filename_patterns {
226 if let Some(cap) = re.captures(name) {
227 if let Some(m) = cap.get(1) {
228 if let Some(code) = self.language_codes.get(m.as_str()) {
229 return Some(LanguageInfo {
230 code: code.clone(),
231 source: LanguageSource::Filename,
232 confidence: 0.8,
233 });
234 }
235 }
236 }
237 }
238 }
239 None
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use std::path::Path;
247
248 #[test]
249 fn test_directory_language_detection() {
250 let det = LanguageDetector::new();
251 let p = Path::new("tc/subtitle.srt");
252 let lang = det.get_primary_language(p).unwrap();
253 assert_eq!(lang, "tc");
254 }
255
256 #[test]
257 fn test_filename_language_detection() {
258 let det = LanguageDetector::new();
259 let p = Path::new("subtitle.sc.ass");
260 let lang = det.get_primary_language(p).unwrap();
261 assert_eq!(lang, "sc");
262 }
263
264 #[test]
265 fn test_no_language_detection() {
266 let det = LanguageDetector::new();
267 let p = Path::new("subtitle.ass");
268 assert!(det.get_primary_language(p).is_none());
269 }
270}