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 ConfirmarRemocaoVps {
132 nome: String,
134 },
135 RemocaoCancelada,
137 RemoveExigeYesEmNaoInterativo,
139}
140
141impl Mensagem {
142 pub fn texto(&self, idioma: Idioma) -> String {
146 match idioma {
147 Idioma::English => en(self),
148 Idioma::Portugues => pt(self),
149 }
150 }
151}
152
153pub fn inicializar_idioma(forcar: Option<&str>) -> Result<()> {
157 let idioma = crate::locale::resolver_idioma(forcar);
158 crate::locale::definir_idioma(idioma);
159 Ok(())
160}
161
162#[must_use]
164pub fn idioma_atual() -> Idioma {
165 crate::locale::idioma_atual()
166}
167
168#[must_use]
183pub fn t(msg: Mensagem) -> String {
184 msg.texto(idioma_atual())
185}
186
187fn en(msg: &Mensagem) -> String {
189 match msg {
190 Mensagem::VpsRegistroVazio => "No VPS registered.".to_string(),
191 Mensagem::VpsListaTitulo => "Registered VPS:".to_string(),
192 Mensagem::VpsAdicionada { nome } => format!("VPS '{nome}' added successfully."),
193 Mensagem::VpsRemovida { nome } => format!("VPS '{nome}' removed successfully."),
194 Mensagem::VpsDuplicada { nome } => format!("VPS '{nome}' is already registered."),
195 Mensagem::VpsNaoEncontrada { nome } => format!("VPS '{nome}' not found."),
196 Mensagem::VpsAtivaSelecionada { nome } => format!("Active VPS: '{nome}'."),
197 Mensagem::ConfigCaminhoLabel => "Configuration file:".to_string(),
198 Mensagem::ConfigCaminho { caminho } => caminho.clone(),
199 Mensagem::ConfigSemChaves => "No API keys configured.".to_string(),
200 Mensagem::ErroCarregarConfig => "Failed to load configuration.".to_string(),
201 Mensagem::ErroSalvarConfig => "Failed to save configuration.".to_string(),
202 Mensagem::ErroConexaoSsh => "SSH connection error.".to_string(),
203 Mensagem::ErroComandoFalhou => "Command execution failed.".to_string(),
204 Mensagem::ErroArgumentoInvalido { detalhe } => format!("Invalid argument: {detalhe}"),
205 Mensagem::ErroGenerico { detalhe } => detalhe.clone(),
206 Mensagem::TunnelAtivo {
207 porta_local,
208 host_remoto,
209 porta_remota,
210 vps_nome,
211 } => format!(
212 "SSH tunnel active: localhost:{porta_local} -> {host_remoto}:{porta_remota} via {vps_nome}"
213 ),
214 Mensagem::TunnelPressioneCtrlC => "Press Ctrl+C to terminate.".to_string(),
215 Mensagem::HealthCheckOk { nome } => format!("Health check passed for '{nome}'."),
216 Mensagem::HealthCheckSemVps => {
217 "No active VPS. Use 'ssh-cli connect <NAME>' first.".to_string()
218 }
219 Mensagem::HealthCheckFalhou { nome, detalhe } => {
220 format!("Health check FAILED for '{nome}': {detalhe}")
221 }
222 Mensagem::HealthCheckLatencia { nome, latencia_ms } => {
223 format!("Health check OK for '{nome}' ({latencia_ms}ms)")
224 }
225 Mensagem::OperacaoCancelada => "Operation cancelled by user.".to_string(),
226 Mensagem::ConfirmarRemocaoVps { nome } => format!("Remove VPS '{nome}'? (y/N): "),
227 Mensagem::RemocaoCancelada => "Removal cancelled.".to_string(),
228 Mensagem::RemoveExigeYesEmNaoInterativo => {
229 "Non-interactive mode: use --yes (-y) to confirm removal.".to_string()
230 }
231 }
232}
233
234fn pt(msg: &Mensagem) -> String {
236 match msg {
237 Mensagem::VpsRegistroVazio => "Nenhum VPS cadastrado.".to_string(),
238 Mensagem::VpsListaTitulo => "VPS cadastrados:".to_string(),
239 Mensagem::VpsAdicionada { nome } => format!("VPS '{nome}' adicionada com sucesso."),
240 Mensagem::VpsRemovida { nome } => format!("VPS '{nome}' removida com sucesso."),
241 Mensagem::VpsDuplicada { nome } => format!("VPS '{nome}' já está cadastrada."),
242 Mensagem::VpsNaoEncontrada { nome } => format!("VPS '{nome}' não encontrada."),
243 Mensagem::VpsAtivaSelecionada { nome } => format!("VPS ativa: '{nome}'."),
244 Mensagem::ConfigCaminhoLabel => "Arquivo de configuração:".to_string(),
245 Mensagem::ConfigCaminho { caminho } => caminho.clone(),
246 Mensagem::ConfigSemChaves => "Nenhuma chave de API configurada.".to_string(),
247 Mensagem::ErroCarregarConfig => "Falha ao carregar configuração.".to_string(),
248 Mensagem::ErroSalvarConfig => "Falha ao salvar configuração.".to_string(),
249 Mensagem::ErroConexaoSsh => "Erro de conexão SSH.".to_string(),
250 Mensagem::ErroComandoFalhou => "Falha na execução do comando.".to_string(),
251 Mensagem::ErroArgumentoInvalido { detalhe } => format!("Argumento inválido: {detalhe}"),
252 Mensagem::ErroGenerico { detalhe } => detalhe.clone(),
253 Mensagem::TunnelAtivo {
254 porta_local,
255 host_remoto,
256 porta_remota,
257 vps_nome,
258 } => format!(
259 "Tunnel SSH: localhost:{porta_local} -> {host_remoto}:{porta_remota} via {vps_nome}"
260 ),
261 Mensagem::TunnelPressioneCtrlC => "Pressione Ctrl+C para encerrar.".to_string(),
262 Mensagem::HealthCheckOk { nome } => format!("Health check bem-sucedido para '{nome}'."),
263 Mensagem::HealthCheckSemVps => {
264 "Nenhuma VPS ativa. Use 'ssh-cli connect <NOME>' primeiro.".to_string()
265 }
266 Mensagem::HealthCheckFalhou { nome, detalhe } => {
267 format!("Health check FALHOU para '{nome}': {detalhe}")
268 }
269 Mensagem::HealthCheckLatencia { nome, latencia_ms } => {
270 format!("Health check OK para '{nome}' ({latencia_ms}ms)")
271 }
272 Mensagem::OperacaoCancelada => "Operação cancelada pelo usuário.".to_string(),
273 Mensagem::ConfirmarRemocaoVps { nome } => format!("Remover VPS '{nome}'? (s/N): "),
274 Mensagem::RemocaoCancelada => "Remoção cancelada.".to_string(),
275 Mensagem::RemoveExigeYesEmNaoInterativo => {
276 "Modo não-interativo: use --yes (-y) para confirmar remoção.".to_string()
277 }
278 }
279}
280
281#[cfg(test)]
282mod testes {
283 use super::*;
284
285 #[test]
286 fn idioma_enum_e_copy() {
287 let a = Idioma::English;
288 let b = a;
289 assert_eq!(a, b);
290 }
291
292 #[test]
293 fn mensagem_nao_e_copy_mas_e_clone() {
294 let m = Mensagem::VpsAdicionada {
295 nome: "vps-01".to_string(),
296 };
297 let m2 = m.clone();
298 assert_eq!(m, m2);
299 }
300
301 #[test]
302 fn vps_registro_vazio_en() {
303 assert_eq!(
304 Mensagem::VpsRegistroVazio.texto(Idioma::English),
305 "No VPS registered."
306 );
307 }
308
309 #[test]
310 fn vps_registro_vazio_pt() {
311 assert_eq!(
312 Mensagem::VpsRegistroVazio.texto(Idioma::Portugues),
313 "Nenhum VPS cadastrado."
314 );
315 }
316
317 #[test]
318 fn vps_adicionada_inclui_nome_en() {
319 let msg = Mensagem::VpsAdicionada {
320 nome: "prod-01".to_string(),
321 };
322 assert_eq!(
323 msg.texto(Idioma::English),
324 "VPS 'prod-01' added successfully."
325 );
326 }
327
328 #[test]
329 fn vps_adicionada_inclui_nome_pt() {
330 let msg = Mensagem::VpsAdicionada {
331 nome: "prod-01".to_string(),
332 };
333 assert_eq!(
334 msg.texto(Idioma::Portugues),
335 "VPS 'prod-01' adicionada com sucesso."
336 );
337 }
338
339 #[test]
340 fn vps_removida_inclui_nome() {
341 let msg = Mensagem::VpsRemovida {
342 nome: "dev-01".to_string(),
343 };
344 assert!(msg.texto(Idioma::English).contains("dev-01"));
345 assert!(msg.texto(Idioma::Portugues).contains("dev-01"));
346 }
347
348 #[test]
349 fn vps_duplicada_inclui_nome() {
350 let msg = Mensagem::VpsDuplicada {
351 nome: "staging".to_string(),
352 };
353 assert!(msg.texto(Idioma::English).contains("staging"));
354 assert!(msg.texto(Idioma::Portugues).contains("staging"));
355 }
356
357 #[test]
358 fn vps_nao_encontrada_inclui_nome() {
359 let msg = Mensagem::VpsNaoEncontrada {
360 nome: "inexistente".to_string(),
361 };
362 assert!(msg.texto(Idioma::English).contains("inexistente"));
363 assert!(msg.texto(Idioma::Portugues).contains("inexistente"));
364 }
365
366 #[test]
367 fn tunnel_ativo_inclui_todos_os_campos() {
368 let msg = Mensagem::TunnelAtivo {
369 porta_local: 8080,
370 host_remoto: "1.2.3.4".to_string(),
371 porta_remota: 22,
372 vps_nome: "meu-servidor".to_string(),
373 };
374 let en = msg.texto(Idioma::English);
375 assert!(en.contains("8080"));
376 assert!(en.contains("1.2.3.4"));
377 assert!(en.contains("22"));
378 assert!(en.contains("meu-servidor"));
379 }
380
381 #[test]
382 fn erro_argumento_invalido_inclui_detalhe() {
383 let msg = Mensagem::ErroArgumentoInvalido {
384 detalhe: "porta fora do intervalo".to_string(),
385 };
386 assert!(msg
387 .texto(Idioma::English)
388 .contains("porta fora do intervalo"));
389 assert!(msg
390 .texto(Idioma::Portugues)
391 .contains("porta fora do intervalo"));
392 }
393
394 #[test]
395 fn health_check_ok_inclui_nome() {
396 let msg = Mensagem::HealthCheckOk {
397 nome: "prod-01".to_string(),
398 };
399 assert!(msg.texto(Idioma::English).contains("prod-01"));
400 assert!(msg.texto(Idioma::Portugues).contains("prod-01"));
401 }
402
403 #[test]
404 fn todas_variantes_unitarias_en_nao_vazias() {
405 let unitarias = [
406 Mensagem::VpsRegistroVazio,
407 Mensagem::VpsListaTitulo,
408 Mensagem::ConfigCaminhoLabel,
409 Mensagem::ConfigSemChaves,
410 Mensagem::ErroCarregarConfig,
411 Mensagem::ErroSalvarConfig,
412 Mensagem::ErroConexaoSsh,
413 Mensagem::ErroComandoFalhou,
414 Mensagem::TunnelPressioneCtrlC,
415 Mensagem::HealthCheckSemVps,
416 Mensagem::OperacaoCancelada,
417 ];
418 for v in &unitarias {
419 let texto = v.texto(Idioma::English);
420 assert!(!texto.is_empty(), "EN vazia para {:?}", v);
421 }
422 }
423
424 #[test]
425 fn todas_variantes_unitarias_pt_nao_vazias() {
426 let unitarias = [
427 Mensagem::VpsRegistroVazio,
428 Mensagem::VpsListaTitulo,
429 Mensagem::ConfigCaminhoLabel,
430 Mensagem::ConfigSemChaves,
431 Mensagem::ErroCarregarConfig,
432 Mensagem::ErroSalvarConfig,
433 Mensagem::ErroConexaoSsh,
434 Mensagem::ErroComandoFalhou,
435 Mensagem::TunnelPressioneCtrlC,
436 Mensagem::HealthCheckSemVps,
437 Mensagem::OperacaoCancelada,
438 ];
439 for v in &unitarias {
440 let texto = v.texto(Idioma::Portugues);
441 assert!(!texto.is_empty(), "PT vazia para {:?}", v);
442 }
443 }
444
445 #[test]
446 fn traducoes_pt_diferentes_de_en_para_unitarias() {
447 let pares = [
448 (Mensagem::VpsRegistroVazio, Mensagem::VpsRegistroVazio),
449 (Mensagem::ErroConexaoSsh, Mensagem::ErroConexaoSsh),
450 (Mensagem::HealthCheckSemVps, Mensagem::HealthCheckSemVps),
451 (Mensagem::OperacaoCancelada, Mensagem::OperacaoCancelada),
452 ];
453 for (a, b) in &pares {
454 let en = a.texto(Idioma::English);
455 let pt = b.texto(Idioma::Portugues);
456 assert_ne!(en, pt, "EN == PT para {:?}", a);
457 }
458 }
459
460 #[test]
461 fn health_check_falhou_inclui_nome_e_detalhe() {
462 let msg = Mensagem::HealthCheckFalhou {
463 nome: "prod-01".to_string(),
464 detalhe: "timeout".to_string(),
465 };
466 assert!(msg.texto(Idioma::English).contains("prod-01"));
467 assert!(msg.texto(Idioma::English).contains("timeout"));
468 assert!(msg.texto(Idioma::Portugues).contains("prod-01"));
469 assert!(msg.texto(Idioma::Portugues).contains("timeout"));
470 }
471
472 #[test]
473 fn health_check_latencia_inclui_nome_e_ms() {
474 let msg = Mensagem::HealthCheckLatencia {
475 nome: "relay-01".to_string(),
476 latencia_ms: 42,
477 };
478 assert!(msg.texto(Idioma::English).contains("relay-01"));
479 assert!(msg.texto(Idioma::English).contains("42"));
480 assert!(msg.texto(Idioma::Portugues).contains("relay-01"));
481 assert!(msg.texto(Idioma::Portugues).contains("42"));
482 }
483
484 #[test]
485 fn inicializar_idioma_sem_forcar_nao_panic() {
486 let resultado = inicializar_idioma(None);
487 assert!(resultado.is_ok());
488 }
489
490 #[test]
491 fn inicializar_idioma_com_pt_br_funciona() {
492 let resultado = inicializar_idioma(Some("pt-BR"));
493 assert!(resultado.is_ok());
494 }
495
496 #[test]
497 fn idioma_atual_retorna_valor_valido() {
498 let idioma = idioma_atual();
499 assert!(idioma == Idioma::English || idioma == Idioma::Portugues);
500 }
501}