use anyhow::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Language {
English,
Portuguese,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Message {
VpsRegistryEmpty,
VpsListTitle,
VpsAdded {
name: String,
},
VpsRemoved {
name: String,
},
VpsDuplicate {
name: String,
},
VpsNotFound {
name: String,
},
VpsActiveSelected {
name: String,
},
ConfigPathLabel,
ConfigPath {
path: String,
},
ConfigNoKeys,
ErrorLoadConfig,
ErrorSaveConfig,
ErrorSshConnection,
ErrorCommandFailed,
ErrorInvalidArgument {
detail: String,
},
ErrorGeneric {
detail: String,
},
VpsEdited {
name: String,
},
ExportCompleted {
path: String,
},
ImportCompleted,
PrimaryKeyReady {
source: String,
key_file: String,
},
ReencryptCompleted {
hosts: usize,
},
Success {
detail: String,
},
TunnelActive {
local_port: u16,
remote_host: String,
remote_port: u16,
vps_name: String,
},
TunnelPressCtrlC,
HealthCheckOk {
name: String,
},
HealthCheckNoVps,
HealthCheckFailed {
name: String,
detail: String,
},
HealthCheckLatency {
name: String,
latency_ms: u64,
},
OperationCancelled,
ScpUploadCompleted {
bytes: u64,
ms: u64,
},
ScpDownloadCompleted {
bytes: u64,
ms: u64,
},
ScpUploadFileOnly,
ScpDownloadLocalNotDirectory,
}
impl Message {
pub fn text(&self, language: Language) -> String {
match language {
Language::English => en(self),
Language::Portuguese => pt(self),
}
}
}
pub fn initialize_language(force_lang: Option<&str>) -> Result<()> {
let language = crate::locale::resolve_language(force_lang);
crate::locale::set_language(language);
Ok(())
}
#[must_use]
pub fn current_language() -> Language {
crate::locale::current_language()
}
#[must_use]
pub fn t(msg: Message) -> String {
msg.text(current_language())
}
fn en(msg: &Message) -> String {
match msg {
Message::VpsRegistryEmpty => "No VPS registered.".to_string(),
Message::VpsListTitle => "Registered VPS:".to_string(),
Message::VpsAdded { name } => format!("VPS '{name}' added successfully."),
Message::VpsRemoved { name } => format!("VPS '{name}' removed successfully."),
Message::VpsDuplicate { name } => format!("VPS '{name}' is already registered."),
Message::VpsNotFound { name } => format!("VPS '{name}' not found."),
Message::VpsActiveSelected { name } => format!("Active VPS: '{name}'."),
Message::ConfigPathLabel => "Configuration file:".to_string(),
Message::ConfigPath { path } => path.clone(),
Message::ConfigNoKeys => "No API keys configured.".to_string(),
Message::ErrorLoadConfig => "Failed to load configuration.".to_string(),
Message::ErrorSaveConfig => "Failed to save configuration.".to_string(),
Message::ErrorSshConnection => "SSH connection error.".to_string(),
Message::ErrorCommandFailed => "Command execution failed.".to_string(),
Message::ErrorInvalidArgument { detail } => format!("Invalid argument: {detail}"),
Message::ErrorGeneric { detail } => detail.clone(),
Message::VpsEdited { name } => format!("VPS '{name}' edited."),
Message::ExportCompleted { path } => format!("exported to {path}"),
Message::ImportCompleted => "import completed".to_string(),
Message::PrimaryKeyReady { source, key_file } => {
format!("primary-key ready (source={source}; key_file={key_file})")
}
Message::ReencryptCompleted { hosts } => {
format!("re-encrypt completed for {hosts} host(s)")
}
Message::Success { detail } => detail.clone(),
Message::TunnelActive {
local_port,
remote_host,
remote_port,
vps_name,
} => format!(
"SSH tunnel active: localhost:{local_port} -> {remote_host}:{remote_port} via {vps_name}"
),
Message::TunnelPressCtrlC => "Press Ctrl+C to terminate.".to_string(),
Message::HealthCheckOk { name } => format!("Health check passed for '{name}'."),
Message::HealthCheckNoVps => {
"No active VPS. Use 'ssh-cli connect <NAME>' first.".to_string()
}
Message::HealthCheckFailed { name, detail } => {
format!("Health check FAILED for '{name}': {detail}")
}
Message::HealthCheckLatency { name, latency_ms } => {
format!("Health check OK for '{name}' ({latency_ms}ms)")
}
Message::OperationCancelled => "Operation cancelled by user.".to_string(),
Message::ScpUploadCompleted { bytes, ms } => {
format!("Upload completed: {bytes} bytes in {ms}ms")
}
Message::ScpDownloadCompleted { bytes, ms } => {
format!("Download completed: {bytes} bytes in {ms}ms")
}
Message::ScpUploadFileOnly => {
"upload only supports regular files (no directories / no -r)".to_string()
}
Message::ScpDownloadLocalNotDirectory => {
"download local path must be a file path, not an existing directory".to_string()
}
}
}
fn pt(msg: &Message) -> String {
match msg {
Message::VpsRegistryEmpty => "Nenhum VPS cadastrado.".to_string(),
Message::VpsListTitle => "VPS cadastrados:".to_string(),
Message::VpsAdded { name } => format!("VPS '{name}' adicionada com sucesso."),
Message::VpsRemoved { name } => format!("VPS '{name}' removida com sucesso."),
Message::VpsDuplicate { name } => format!("VPS '{name}' já está cadastrada."),
Message::VpsNotFound { name } => format!("VPS '{name}' não encontrada."),
Message::VpsActiveSelected { name } => format!("VPS ativa: '{name}'."),
Message::ConfigPathLabel => "Arquivo de configuração:".to_string(),
Message::ConfigPath { path } => path.clone(),
Message::ConfigNoKeys => "Nenhuma chave de API configurada.".to_string(),
Message::ErrorLoadConfig => "Falha ao carregar configuração.".to_string(),
Message::ErrorSaveConfig => "Falha ao salvar configuração.".to_string(),
Message::ErrorSshConnection => "Erro de conexão SSH.".to_string(),
Message::ErrorCommandFailed => "Falha na execução do comando.".to_string(),
Message::ErrorInvalidArgument { detail } => format!("Argumento inválido: {detail}"),
Message::ErrorGeneric { detail } => detail.clone(),
Message::VpsEdited { name } => format!("VPS '{name}' editada."),
Message::ExportCompleted { path } => format!("exportado para {path}"),
Message::ImportCompleted => "importação concluída".to_string(),
Message::PrimaryKeyReady { source, key_file } => {
format!("primary-key pronta (source={source}; key_file={key_file})")
}
Message::ReencryptCompleted { hosts } => {
format!("re-cifragem concluída para {hosts} host(s)")
}
Message::Success { detail } => detail.clone(),
Message::TunnelActive {
local_port,
remote_host,
remote_port,
vps_name,
} => format!(
"Tunnel SSH: localhost:{local_port} -> {remote_host}:{remote_port} via {vps_name}"
),
Message::TunnelPressCtrlC => "Pressione Ctrl+C para encerrar.".to_string(),
Message::HealthCheckOk { name } => format!("Health check bem-sucedido para '{name}'."),
Message::HealthCheckNoVps => {
"Nenhuma VPS ativa. Use 'ssh-cli connect <NOME>' primeiro.".to_string()
}
Message::HealthCheckFailed { name, detail } => {
format!("Health check FALHOU para '{name}': {detail}")
}
Message::HealthCheckLatency { name, latency_ms } => {
format!("Health check OK para '{name}' ({latency_ms}ms)")
}
Message::OperationCancelled => "Operação cancelada pelo usuário.".to_string(),
Message::ScpUploadCompleted { bytes, ms } => {
format!("Upload concluído: {bytes} bytes em {ms}ms")
}
Message::ScpDownloadCompleted { bytes, ms } => {
format!("Download concluído: {bytes} bytes em {ms}ms")
}
Message::ScpUploadFileOnly => {
"upload só suporta arquivos regulares (sem diretórios / sem -r)".to_string()
}
Message::ScpDownloadLocalNotDirectory => {
"caminho local de download deve ser arquivo, não diretório existente".to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn language_enum_is_copy() {
let a = Language::English;
let b = a;
assert_eq!(a, b);
}
#[test]
fn message_is_not_copy_but_is_clone() {
let m = Message::VpsAdded {
name: "vps-01".to_string(),
};
let m2 = m.clone();
assert_eq!(m, m2);
}
#[test]
fn vps_registry_empty_en() {
assert_eq!(
Message::VpsRegistryEmpty.text(Language::English),
"No VPS registered."
);
}
#[test]
fn vps_registry_empty_pt() {
assert_eq!(
Message::VpsRegistryEmpty.text(Language::Portuguese),
"Nenhum VPS cadastrado."
);
}
#[test]
fn vps_added_includes_name_en() {
let msg = Message::VpsAdded {
name: "prod-01".to_string(),
};
assert_eq!(
msg.text(Language::English),
"VPS 'prod-01' added successfully."
);
}
#[test]
fn vps_added_includes_name_pt() {
let msg = Message::VpsAdded {
name: "prod-01".to_string(),
};
assert_eq!(
msg.text(Language::Portuguese),
"VPS 'prod-01' adicionada com sucesso."
);
}
#[test]
fn vps_removed_includes_name() {
let msg = Message::VpsRemoved {
name: "dev-01".to_string(),
};
assert!(msg.text(Language::English).contains("dev-01"));
assert!(msg.text(Language::Portuguese).contains("dev-01"));
}
#[test]
fn vps_duplicate_includes_name() {
let msg = Message::VpsDuplicate {
name: "staging".to_string(),
};
assert!(msg.text(Language::English).contains("staging"));
assert!(msg.text(Language::Portuguese).contains("staging"));
}
#[test]
fn vps_not_found_includes_name() {
let msg = Message::VpsNotFound {
name: "inexistente".to_string(),
};
assert!(msg.text(Language::English).contains("inexistente"));
assert!(msg.text(Language::Portuguese).contains("inexistente"));
}
#[test]
fn tunnel_active_includes_all_fields() {
let msg = Message::TunnelActive {
local_port: 8080,
remote_host: "1.2.3.4".to_string(),
remote_port: 22,
vps_name: "meu-servidor".to_string(),
};
let en = msg.text(Language::English);
assert!(en.contains("8080"));
assert!(en.contains("1.2.3.4"));
assert!(en.contains("22"));
assert!(en.contains("meu-servidor"));
}
#[test]
fn error_invalid_argument_includes_detail() {
let msg = Message::ErrorInvalidArgument {
detail: "port out of range".to_string(),
};
assert!(msg
.text(Language::English)
.contains("port out of range"));
assert!(msg
.text(Language::Portuguese)
.contains("port out of range"));
}
#[test]
fn health_check_ok_includes_name() {
let msg = Message::HealthCheckOk {
name: "prod-01".to_string(),
};
assert!(msg.text(Language::English).contains("prod-01"));
assert!(msg.text(Language::Portuguese).contains("prod-01"));
}
#[test]
fn all_unit_variants_en_nonempty() {
let unit_variants = [
Message::VpsRegistryEmpty,
Message::VpsListTitle,
Message::ConfigPathLabel,
Message::ConfigNoKeys,
Message::ErrorLoadConfig,
Message::ErrorSaveConfig,
Message::ErrorSshConnection,
Message::ErrorCommandFailed,
Message::TunnelPressCtrlC,
Message::HealthCheckNoVps,
Message::OperationCancelled,
];
for v in &unit_variants {
let text = v.text(Language::English);
assert!(!text.is_empty(), "empty EN for {:?}", v);
}
}
#[test]
fn all_unit_variants_pt_nonempty() {
let unit_variants = [
Message::VpsRegistryEmpty,
Message::VpsListTitle,
Message::ConfigPathLabel,
Message::ConfigNoKeys,
Message::ErrorLoadConfig,
Message::ErrorSaveConfig,
Message::ErrorSshConnection,
Message::ErrorCommandFailed,
Message::TunnelPressCtrlC,
Message::HealthCheckNoVps,
Message::OperationCancelled,
];
for v in &unit_variants {
let text = v.text(Language::Portuguese);
assert!(!text.is_empty(), "empty PT for {:?}", v);
}
}
#[test]
fn pt_translations_differ_from_en_for_units() {
let pairs = [
(Message::VpsRegistryEmpty, Message::VpsRegistryEmpty),
(Message::ErrorSshConnection, Message::ErrorSshConnection),
(Message::HealthCheckNoVps, Message::HealthCheckNoVps),
(Message::OperationCancelled, Message::OperationCancelled),
];
for (a, b) in &pairs {
let en = a.text(Language::English);
let pt = b.text(Language::Portuguese);
assert_ne!(en, pt, "EN == PT for {:?}", a);
}
}
#[test]
fn health_check_failed_includes_name_and_detail() {
let msg = Message::HealthCheckFailed {
name: "prod-01".to_string(),
detail: "timeout".to_string(),
};
assert!(msg.text(Language::English).contains("prod-01"));
assert!(msg.text(Language::English).contains("timeout"));
assert!(msg.text(Language::Portuguese).contains("prod-01"));
assert!(msg.text(Language::Portuguese).contains("timeout"));
}
#[test]
fn health_check_latency_includes_name_and_ms() {
let msg = Message::HealthCheckLatency {
name: "relay-01".to_string(),
latency_ms: 42,
};
assert!(msg.text(Language::English).contains("relay-01"));
assert!(msg.text(Language::English).contains("42"));
assert!(msg.text(Language::Portuguese).contains("relay-01"));
assert!(msg.text(Language::Portuguese).contains("42"));
}
#[test]
fn initialize_language_without_force_no_panic() {
let result = initialize_language(None);
assert!(result.is_ok());
}
#[test]
fn initialize_language_with_pt_br_works() {
let result = initialize_language(Some("pt-BR"));
assert!(result.is_ok());
}
#[test]
fn current_language_returns_valid_value() {
let language = current_language();
assert!(language == Language::English || language == Language::Portuguese);
}
}