1use anyhow::Result;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum Idioma {
17 English,
19 Portugues,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum Mensagem {
33 VpsRegistroVazio,
36 VpsListaTitulo,
38 VpsAdicionada {
40 nome: String,
42 },
43 VpsRemovida {
45 nome: String,
47 },
48 VpsDuplicada {
50 nome: String,
52 },
53 VpsNaoEncontrada {
55 nome: String,
57 },
58 VpsAtivaSelecionada {
60 nome: String,
62 },
63 ConfigCaminhoLabel,
66 ConfigCaminho {
68 caminho: String,
70 },
71 ConfigSemChaves,
73 ErroCarregarConfig,
76 ErroSalvarConfig,
78 ErroConexaoSsh,
80 ErroComandoFalhou,
82 ErroArgumentoInvalido {
84 detalhe: String,
86 },
87 ErroGenerico {
89 detalhe: String,
91 },
92 TunnelAtivo {
95 porta_local: u16,
97 host_remoto: String,
99 porta_remota: u16,
101 vps_nome: String,
103 },
104 TunnelPressioneCtrlC,
106 HealthCheckOk {
109 nome: String,
111 },
112 HealthCheckSemVps,
114 HealthCheckFalhou {
116 nome: String,
118 detalhe: String,
120 },
121 HealthCheckLatencia {
123 nome: String,
125 latencia_ms: u64,
127 },
128 OperacaoCancelada,
130 ScpUploadConcluido {
133 bytes: u64,
135 ms: u64,
137 },
138 ScpDownloadConcluido {
140 bytes: u64,
142 ms: u64,
144 },
145 ScpUploadSomenteArquivo,
147 ScpDownloadLocalNaoDiretorio,
149}
150
151impl Mensagem {
152 pub fn texto(&self, idioma: Idioma) -> String {
156 match idioma {
157 Idioma::English => en(self),
158 Idioma::Portugues => pt(self),
159 }
160 }
161}
162
163pub fn inicializar_idioma(forcar: Option<&str>) -> Result<()> {
167 let idioma = crate::locale::resolver_idioma(forcar);
168 crate::locale::definir_idioma(idioma);
169 Ok(())
170}
171
172#[must_use]
174pub fn idioma_atual() -> Idioma {
175 crate::locale::idioma_atual()
176}
177
178#[must_use]
193pub fn t(msg: Mensagem) -> String {
194 msg.texto(idioma_atual())
195}
196
197fn en(msg: &Mensagem) -> String {
199 match msg {
200 Mensagem::VpsRegistroVazio => "No VPS registered.".to_string(),
201 Mensagem::VpsListaTitulo => "Registered VPS:".to_string(),
202 Mensagem::VpsAdicionada { nome } => format!("VPS '{nome}' added successfully."),
203 Mensagem::VpsRemovida { nome } => format!("VPS '{nome}' removed successfully."),
204 Mensagem::VpsDuplicada { nome } => format!("VPS '{nome}' is already registered."),
205 Mensagem::VpsNaoEncontrada { nome } => format!("VPS '{nome}' not found."),
206 Mensagem::VpsAtivaSelecionada { nome } => format!("Active VPS: '{nome}'."),
207 Mensagem::ConfigCaminhoLabel => "Configuration file:".to_string(),
208 Mensagem::ConfigCaminho { caminho } => caminho.clone(),
209 Mensagem::ConfigSemChaves => "No API keys configured.".to_string(),
210 Mensagem::ErroCarregarConfig => "Failed to load configuration.".to_string(),
211 Mensagem::ErroSalvarConfig => "Failed to save configuration.".to_string(),
212 Mensagem::ErroConexaoSsh => "SSH connection error.".to_string(),
213 Mensagem::ErroComandoFalhou => "Command execution failed.".to_string(),
214 Mensagem::ErroArgumentoInvalido { detalhe } => format!("Invalid argument: {detalhe}"),
215 Mensagem::ErroGenerico { detalhe } => detalhe.clone(),
216 Mensagem::TunnelAtivo {
217 porta_local,
218 host_remoto,
219 porta_remota,
220 vps_nome,
221 } => format!(
222 "SSH tunnel active: localhost:{porta_local} -> {host_remoto}:{porta_remota} via {vps_nome}"
223 ),
224 Mensagem::TunnelPressioneCtrlC => "Press Ctrl+C to terminate.".to_string(),
225 Mensagem::HealthCheckOk { nome } => format!("Health check passed for '{nome}'."),
226 Mensagem::HealthCheckSemVps => {
227 "No active VPS. Use 'ssh-cli connect <NAME>' first.".to_string()
228 }
229 Mensagem::HealthCheckFalhou { nome, detalhe } => {
230 format!("Health check FAILED for '{nome}': {detalhe}")
231 }
232 Mensagem::HealthCheckLatencia { nome, latencia_ms } => {
233 format!("Health check OK for '{nome}' ({latencia_ms}ms)")
234 }
235 Mensagem::OperacaoCancelada => "Operation cancelled by user.".to_string(),
236 Mensagem::ScpUploadConcluido { bytes, ms } => {
237 format!("Upload completed: {bytes} bytes in {ms}ms")
238 }
239 Mensagem::ScpDownloadConcluido { bytes, ms } => {
240 format!("Download completed: {bytes} bytes in {ms}ms")
241 }
242 Mensagem::ScpUploadSomenteArquivo => {
243 "upload only supports regular files (no directories / no -r)".to_string()
244 }
245 Mensagem::ScpDownloadLocalNaoDiretorio => {
246 "download local path must be a file path, not an existing directory".to_string()
247 }
248 }
249}
250
251fn pt(msg: &Mensagem) -> String {
253 match msg {
254 Mensagem::VpsRegistroVazio => "Nenhum VPS cadastrado.".to_string(),
255 Mensagem::VpsListaTitulo => "VPS cadastrados:".to_string(),
256 Mensagem::VpsAdicionada { nome } => format!("VPS '{nome}' adicionada com sucesso."),
257 Mensagem::VpsRemovida { nome } => format!("VPS '{nome}' removida com sucesso."),
258 Mensagem::VpsDuplicada { nome } => format!("VPS '{nome}' já está cadastrada."),
259 Mensagem::VpsNaoEncontrada { nome } => format!("VPS '{nome}' não encontrada."),
260 Mensagem::VpsAtivaSelecionada { nome } => format!("VPS ativa: '{nome}'."),
261 Mensagem::ConfigCaminhoLabel => "Arquivo de configuração:".to_string(),
262 Mensagem::ConfigCaminho { caminho } => caminho.clone(),
263 Mensagem::ConfigSemChaves => "Nenhuma chave de API configurada.".to_string(),
264 Mensagem::ErroCarregarConfig => "Falha ao carregar configuração.".to_string(),
265 Mensagem::ErroSalvarConfig => "Falha ao salvar configuração.".to_string(),
266 Mensagem::ErroConexaoSsh => "Erro de conexão SSH.".to_string(),
267 Mensagem::ErroComandoFalhou => "Falha na execução do comando.".to_string(),
268 Mensagem::ErroArgumentoInvalido { detalhe } => format!("Argumento inválido: {detalhe}"),
269 Mensagem::ErroGenerico { detalhe } => detalhe.clone(),
270 Mensagem::TunnelAtivo {
271 porta_local,
272 host_remoto,
273 porta_remota,
274 vps_nome,
275 } => format!(
276 "Tunnel SSH: localhost:{porta_local} -> {host_remoto}:{porta_remota} via {vps_nome}"
277 ),
278 Mensagem::TunnelPressioneCtrlC => "Pressione Ctrl+C para encerrar.".to_string(),
279 Mensagem::HealthCheckOk { nome } => format!("Health check bem-sucedido para '{nome}'."),
280 Mensagem::HealthCheckSemVps => {
281 "Nenhuma VPS ativa. Use 'ssh-cli connect <NOME>' primeiro.".to_string()
282 }
283 Mensagem::HealthCheckFalhou { nome, detalhe } => {
284 format!("Health check FALHOU para '{nome}': {detalhe}")
285 }
286 Mensagem::HealthCheckLatencia { nome, latencia_ms } => {
287 format!("Health check OK para '{nome}' ({latencia_ms}ms)")
288 }
289 Mensagem::OperacaoCancelada => "Operação cancelada pelo usuário.".to_string(),
290 Mensagem::ScpUploadConcluido { bytes, ms } => {
291 format!("Upload concluído: {bytes} bytes em {ms}ms")
292 }
293 Mensagem::ScpDownloadConcluido { bytes, ms } => {
294 format!("Download concluído: {bytes} bytes em {ms}ms")
295 }
296 Mensagem::ScpUploadSomenteArquivo => {
297 "upload só suporta arquivos regulares (sem diretórios / sem -r)".to_string()
298 }
299 Mensagem::ScpDownloadLocalNaoDiretorio => {
300 "caminho local de download deve ser arquivo, não diretório existente".to_string()
301 }
302 }
303}
304
305#[cfg(test)]
306mod testes {
307 use super::*;
308
309 #[test]
310 fn idioma_enum_e_copy() {
311 let a = Idioma::English;
312 let b = a;
313 assert_eq!(a, b);
314 }
315
316 #[test]
317 fn mensagem_nao_e_copy_mas_e_clone() {
318 let m = Mensagem::VpsAdicionada {
319 nome: "vps-01".to_string(),
320 };
321 let m2 = m.clone();
322 assert_eq!(m, m2);
323 }
324
325 #[test]
326 fn vps_registro_vazio_en() {
327 assert_eq!(
328 Mensagem::VpsRegistroVazio.texto(Idioma::English),
329 "No VPS registered."
330 );
331 }
332
333 #[test]
334 fn vps_registro_vazio_pt() {
335 assert_eq!(
336 Mensagem::VpsRegistroVazio.texto(Idioma::Portugues),
337 "Nenhum VPS cadastrado."
338 );
339 }
340
341 #[test]
342 fn vps_adicionada_inclui_nome_en() {
343 let msg = Mensagem::VpsAdicionada {
344 nome: "prod-01".to_string(),
345 };
346 assert_eq!(
347 msg.texto(Idioma::English),
348 "VPS 'prod-01' added successfully."
349 );
350 }
351
352 #[test]
353 fn vps_adicionada_inclui_nome_pt() {
354 let msg = Mensagem::VpsAdicionada {
355 nome: "prod-01".to_string(),
356 };
357 assert_eq!(
358 msg.texto(Idioma::Portugues),
359 "VPS 'prod-01' adicionada com sucesso."
360 );
361 }
362
363 #[test]
364 fn vps_removida_inclui_nome() {
365 let msg = Mensagem::VpsRemovida {
366 nome: "dev-01".to_string(),
367 };
368 assert!(msg.texto(Idioma::English).contains("dev-01"));
369 assert!(msg.texto(Idioma::Portugues).contains("dev-01"));
370 }
371
372 #[test]
373 fn vps_duplicada_inclui_nome() {
374 let msg = Mensagem::VpsDuplicada {
375 nome: "staging".to_string(),
376 };
377 assert!(msg.texto(Idioma::English).contains("staging"));
378 assert!(msg.texto(Idioma::Portugues).contains("staging"));
379 }
380
381 #[test]
382 fn vps_nao_encontrada_inclui_nome() {
383 let msg = Mensagem::VpsNaoEncontrada {
384 nome: "inexistente".to_string(),
385 };
386 assert!(msg.texto(Idioma::English).contains("inexistente"));
387 assert!(msg.texto(Idioma::Portugues).contains("inexistente"));
388 }
389
390 #[test]
391 fn tunnel_ativo_inclui_todos_os_campos() {
392 let msg = Mensagem::TunnelAtivo {
393 porta_local: 8080,
394 host_remoto: "1.2.3.4".to_string(),
395 porta_remota: 22,
396 vps_nome: "meu-servidor".to_string(),
397 };
398 let en = msg.texto(Idioma::English);
399 assert!(en.contains("8080"));
400 assert!(en.contains("1.2.3.4"));
401 assert!(en.contains("22"));
402 assert!(en.contains("meu-servidor"));
403 }
404
405 #[test]
406 fn erro_argumento_invalido_inclui_detalhe() {
407 let msg = Mensagem::ErroArgumentoInvalido {
408 detalhe: "porta fora do intervalo".to_string(),
409 };
410 assert!(msg
411 .texto(Idioma::English)
412 .contains("porta fora do intervalo"));
413 assert!(msg
414 .texto(Idioma::Portugues)
415 .contains("porta fora do intervalo"));
416 }
417
418 #[test]
419 fn health_check_ok_inclui_nome() {
420 let msg = Mensagem::HealthCheckOk {
421 nome: "prod-01".to_string(),
422 };
423 assert!(msg.texto(Idioma::English).contains("prod-01"));
424 assert!(msg.texto(Idioma::Portugues).contains("prod-01"));
425 }
426
427 #[test]
428 fn todas_variantes_unitarias_en_nao_vazias() {
429 let unitarias = [
430 Mensagem::VpsRegistroVazio,
431 Mensagem::VpsListaTitulo,
432 Mensagem::ConfigCaminhoLabel,
433 Mensagem::ConfigSemChaves,
434 Mensagem::ErroCarregarConfig,
435 Mensagem::ErroSalvarConfig,
436 Mensagem::ErroConexaoSsh,
437 Mensagem::ErroComandoFalhou,
438 Mensagem::TunnelPressioneCtrlC,
439 Mensagem::HealthCheckSemVps,
440 Mensagem::OperacaoCancelada,
441 ];
442 for v in &unitarias {
443 let texto = v.texto(Idioma::English);
444 assert!(!texto.is_empty(), "EN vazia para {:?}", v);
445 }
446 }
447
448 #[test]
449 fn todas_variantes_unitarias_pt_nao_vazias() {
450 let unitarias = [
451 Mensagem::VpsRegistroVazio,
452 Mensagem::VpsListaTitulo,
453 Mensagem::ConfigCaminhoLabel,
454 Mensagem::ConfigSemChaves,
455 Mensagem::ErroCarregarConfig,
456 Mensagem::ErroSalvarConfig,
457 Mensagem::ErroConexaoSsh,
458 Mensagem::ErroComandoFalhou,
459 Mensagem::TunnelPressioneCtrlC,
460 Mensagem::HealthCheckSemVps,
461 Mensagem::OperacaoCancelada,
462 ];
463 for v in &unitarias {
464 let texto = v.texto(Idioma::Portugues);
465 assert!(!texto.is_empty(), "PT vazia para {:?}", v);
466 }
467 }
468
469 #[test]
470 fn traducoes_pt_diferentes_de_en_para_unitarias() {
471 let pares = [
472 (Mensagem::VpsRegistroVazio, Mensagem::VpsRegistroVazio),
473 (Mensagem::ErroConexaoSsh, Mensagem::ErroConexaoSsh),
474 (Mensagem::HealthCheckSemVps, Mensagem::HealthCheckSemVps),
475 (Mensagem::OperacaoCancelada, Mensagem::OperacaoCancelada),
476 ];
477 for (a, b) in &pares {
478 let en = a.texto(Idioma::English);
479 let pt = b.texto(Idioma::Portugues);
480 assert_ne!(en, pt, "EN == PT para {:?}", a);
481 }
482 }
483
484 #[test]
485 fn health_check_falhou_inclui_nome_e_detalhe() {
486 let msg = Mensagem::HealthCheckFalhou {
487 nome: "prod-01".to_string(),
488 detalhe: "timeout".to_string(),
489 };
490 assert!(msg.texto(Idioma::English).contains("prod-01"));
491 assert!(msg.texto(Idioma::English).contains("timeout"));
492 assert!(msg.texto(Idioma::Portugues).contains("prod-01"));
493 assert!(msg.texto(Idioma::Portugues).contains("timeout"));
494 }
495
496 #[test]
497 fn health_check_latencia_inclui_nome_e_ms() {
498 let msg = Mensagem::HealthCheckLatencia {
499 nome: "relay-01".to_string(),
500 latencia_ms: 42,
501 };
502 assert!(msg.texto(Idioma::English).contains("relay-01"));
503 assert!(msg.texto(Idioma::English).contains("42"));
504 assert!(msg.texto(Idioma::Portugues).contains("relay-01"));
505 assert!(msg.texto(Idioma::Portugues).contains("42"));
506 }
507
508 #[test]
509 fn inicializar_idioma_sem_forcar_nao_panic() {
510 let resultado = inicializar_idioma(None);
511 assert!(resultado.is_ok());
512 }
513
514 #[test]
515 fn inicializar_idioma_com_pt_br_funciona() {
516 let resultado = inicializar_idioma(Some("pt-BR"));
517 assert!(resultado.is_ok());
518 }
519
520 #[test]
521 fn idioma_atual_retorna_valor_valido() {
522 let idioma = idioma_atual();
523 assert!(idioma == Idioma::English || idioma == Idioma::Portugues);
524 }
525}