#![forbid(unsafe_code)]
use anyhow::Result;
use unic_langid::LanguageIdentifier;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TextDirection {
Ltr,
Rtl,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Language {
English,
Portuguese,
}
impl Language {
pub const AVAILABLE: &'static [Language] = &[Language::English, Language::Portuguese];
#[must_use]
pub const fn bcp47(self) -> &'static str {
match self {
Self::English => "en",
Self::Portuguese => "pt-BR",
}
}
#[must_use]
pub fn language_identifier(self) -> LanguageIdentifier {
self.bcp47()
.parse()
.unwrap_or_else(|_| LanguageIdentifier::default())
}
#[must_use]
pub const fn fallback(self) -> Language {
match self {
Self::English => Self::English,
Self::Portuguese => Self::English,
}
}
#[must_use]
pub const fn direction(self) -> TextDirection {
match self {
Self::English | Self::Portuguese => TextDirection::Ltr,
}
}
#[must_use]
pub const fn script(self) -> &'static str {
match self {
Self::English | Self::Portuguese => "Latn",
}
}
#[must_use]
pub fn from_langid(id: &LanguageIdentifier) -> Option<Language> {
match id.language.as_str() {
"en" => Some(Self::English),
"pt" => Some(Self::Portuguese),
_ => None,
}
}
}
#[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,
SftpUploadCompleted {
bytes: u64,
ms: u64,
},
SftpDownloadCompleted {
bytes: u64,
ms: u64,
},
LocalePreferenceSaved {
lang: String,
path: String,
},
LocalePreferenceCleared,
LocaleStatusTitle,
}
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>,
config_dir_override: Option<&std::path::Path>,
) -> Result<()> {
let resolution =
crate::locale::resolve_language_detailed(force_lang, config_dir_override);
tracing::debug!(
target: "ssh_cli::i18n",
language = resolution.language.bcp47(),
source = resolution.source.as_str(),
"locale resolved"
);
crate::locale::set_language(resolution.language);
Ok(())
}
#[must_use]
pub fn current_language() -> Language {
crate::locale::current_language()
}
#[must_use]
#[allow(clippy::needless_pass_by_value)]
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: {}:{local_port} -> {remote_host}:{remote_port} via {vps_name}",
crate::constants::DEFAULT_TUNNEL_BIND_ADDR
),
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()
}
Message::SftpUploadCompleted { bytes, ms } => {
format!("SFTP upload completed: {bytes} bytes in {ms}ms")
}
Message::SftpDownloadCompleted { bytes, ms } => {
format!("SFTP download completed: {bytes} bytes in {ms}ms")
}
Message::LocalePreferenceSaved { lang, path } => {
format!("language preference saved: {lang} ({path})")
}
Message::LocalePreferenceCleared => "language preference cleared.".to_string(),
Message::LocaleStatusTitle => "Locale status:".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: {}:{local_port} -> {remote_host}:{remote_port} via {vps_name}",
crate::constants::DEFAULT_TUNNEL_BIND_ADDR
),
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()
}
Message::SftpUploadCompleted { bytes, ms } => {
format!("Upload SFTP concluído: {bytes} bytes em {ms}ms")
}
Message::SftpDownloadCompleted { bytes, ms } => {
format!("Download SFTP concluído: {bytes} bytes em {ms}ms")
}
Message::LocalePreferenceSaved { lang, path } => {
format!("preferência de idioma salva: {lang} ({path})")
}
Message::LocalePreferenceCleared => "preferência de idioma removida.".to_string(),
Message::LocaleStatusTitle => "Status do locale:".to_string(),
}
}
#[cfg(test)]
#[path = "i18n_tests.rs"]
mod tests;