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