tl_cli/cli/commands/
translate.rs1use anyhow::{Result, bail};
2use futures_util::StreamExt;
3use std::io::{self, Write};
4
5use super::load_config;
6use crate::cache::CacheManager;
7use crate::config::{ResolveOptions, resolve_config};
8use crate::fs::atomic_write;
9use crate::input::InputReader;
10use crate::translation::{TranslationClient, TranslationRequest};
11use crate::ui::Spinner;
12
13pub struct TranslateOptions {
15 pub file: Option<String>,
17 pub to: Option<String>,
19 pub provider: Option<String>,
21 pub model: Option<String>,
23 pub style: Option<String>,
25 pub no_cache: bool,
27 pub write: bool,
29}
30
31pub async fn run_translate(options: TranslateOptions) -> Result<()> {
36 if options.write && options.file.is_none() {
38 bail!("--write requires a file argument (cannot write to stdin)");
39 }
40
41 let (_manager, config_file) = load_config()?;
42 let resolve_options = ResolveOptions {
43 to: options.to.clone(),
44 provider: options.provider.clone(),
45 model: options.model.clone(),
46 style: options.style.clone(),
47 };
48 let resolved = resolve_config(&resolve_options, &config_file)?;
49
50 let source_text = InputReader::read(options.file.as_deref())?;
51
52 if source_text.is_empty() {
53 bail!("Input is empty");
54 }
55
56 let cache_manager = CacheManager::new()?;
57
58 let request = TranslationRequest {
61 source_text,
62 target_language: resolved.target_language,
63 model: resolved.model,
64 endpoint: resolved.endpoint.clone(),
65 style: resolved.style_prompt,
66 };
67
68 let client = TranslationClient::new(resolved.endpoint, resolved.api_key);
70
71 if !options.no_cache
72 && let Some(cached) = cache_manager.get(&request)?
73 {
74 if options.write {
75 if let Some(ref file_path) = options.file {
76 atomic_write(file_path, &cached)?;
77 }
78 } else {
79 print!("{cached}");
80 io::stdout().flush()?;
81 }
82 return Ok(());
83 }
84
85 let spinner_msg = if options.write {
86 format!(
87 "Translating {}...",
88 options.file.as_deref().unwrap_or("file")
89 )
90 } else {
91 "Translating...".to_string()
92 };
93 let spinner = Spinner::new(&spinner_msg);
94
95 let mut stream = client.translate_stream(&request).await?;
96 let mut full_response = String::new();
97 let mut spinner_active = true;
98
99 while let Some(chunk_result) = stream.next().await {
100 let chunk = chunk_result?;
101
102 if spinner_active && !options.write {
105 spinner.stop();
106 spinner_active = false;
107 }
108
109 if !options.write {
110 print!("{chunk}");
111 io::stdout().flush()?;
112 }
113 full_response.push_str(&chunk);
114 }
115
116 if spinner_active {
117 spinner.stop();
118 }
119
120 if !options.write && !full_response.is_empty() {
121 println!();
122 }
123
124 if !options.no_cache && !full_response.is_empty() {
125 cache_manager.put(&request, &full_response)?;
126 }
127
128 if options.write
130 && !full_response.is_empty()
131 && let Some(ref file_path) = options.file
132 {
133 atomic_write(file_path, &full_response)?;
134 }
135
136 Ok(())
137}