pub mod en;
pub mod ru;
pub mod zh;
use std::sync::atomic::{AtomicU8, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Lang {
En = 0,
Ru = 1,
Zh = 2,
}
impl Lang {
pub fn next(self) -> Self {
match self {
Self::En => Self::Ru,
Self::Ru => Self::Zh,
Self::Zh => Self::En,
}
}
pub fn label(self) -> &'static str {
match self {
Self::En => "EN",
Self::Ru => "RU",
Self::Zh => "ZH",
}
}
fn from_u8(v: u8) -> Self {
match v {
1 => Self::Ru,
2 => Self::Zh,
_ => Self::En,
}
}
}
static LANG: AtomicU8 = AtomicU8::new(0);
pub fn set_lang(lang: Lang) {
LANG.store(lang as u8, Ordering::Relaxed);
}
pub fn lang() -> Lang {
Lang::from_u8(LANG.load(Ordering::Relaxed))
}
pub fn strings() -> &'static Strings {
match lang() {
Lang::En => &en::STRINGS,
Lang::Ru => &ru::STRINGS,
Lang::Zh => &zh::STRINGS,
}
}
pub fn detect_locale() -> Lang {
if let Ok(val) = std::env::var("PRT_LANG") {
return parse_lang(&val);
}
if let Some(locale) = sys_locale::get_locale() {
let lower = locale.to_lowercase();
if lower.starts_with("ru") {
return Lang::Ru;
}
if lower.starts_with("zh") {
return Lang::Zh;
}
}
Lang::En
}
pub fn parse_lang(s: &str) -> Lang {
match s.to_lowercase().as_str() {
"ru" | "russian" => Lang::Ru,
"zh" | "cn" | "chinese" => Lang::Zh,
_ => Lang::En,
}
}
pub struct Strings {
pub app_name: &'static str,
pub connections: &'static str,
pub no_root_warning: &'static str,
pub sudo_ok: &'static str,
pub filter_label: &'static str,
pub search_mode: &'static str,
pub tab_tree: &'static str,
pub tab_network: &'static str,
pub tab_connection: &'static str,
pub no_selected_process: &'static str,
pub view_chart: &'static str,
pub view_topology: &'static str,
pub view_process: &'static str,
pub view_namespaces: &'static str,
pub process_not_found: &'static str,
pub iface_address: &'static str,
pub iface_interface: &'static str,
pub iface_protocol: &'static str,
pub iface_bind: &'static str,
pub iface_localhost_only: &'static str,
pub iface_all_interfaces: &'static str,
pub iface_specific: &'static str,
pub iface_loopback: &'static str,
pub iface_all: &'static str,
pub conn_local: &'static str,
pub conn_remote: &'static str,
pub conn_state: &'static str,
pub conn_process: &'static str,
pub conn_cmdline: &'static str,
pub help_text: &'static str,
pub kill_cancel: &'static str,
pub copied: &'static str,
pub refreshed: &'static str,
pub clipboard_unavailable: &'static str,
pub scan_error: &'static str,
pub cancelled: &'static str,
pub lang_switched: &'static str,
pub sudo_prompt_title: &'static str,
pub sudo_password_label: &'static str,
pub sudo_confirm_hint: &'static str,
pub sudo_failed: &'static str,
pub sudo_wrong_password: &'static str,
pub sudo_elevated: &'static str,
pub hint_help: &'static str,
pub hint_search: &'static str,
pub hint_kill: &'static str,
pub hint_sudo: &'static str,
pub hint_quit: &'static str,
pub hint_lang: &'static str,
pub hint_back: &'static str,
pub hint_details: &'static str,
pub hint_views: &'static str,
pub hint_sort: &'static str,
pub hint_copy: &'static str,
pub hint_block: &'static str,
pub hint_trace: &'static str,
pub hint_navigate: &'static str,
pub hint_tabs: &'static str,
pub forward_prompt_title: &'static str,
pub forward_host_label: &'static str,
pub forward_confirm_hint: &'static str,
pub hint_forward: &'static str,
pub help_title: &'static str,
}
impl Strings {
pub fn fmt_connections(&self, n: usize) -> String {
format!("{n} {}", self.connections)
}
pub fn fmt_kill_confirm(&self, name: &str, pid: u32) -> String {
match lang() {
Lang::En => format!("Kill {name} (pid {pid})?"),
Lang::Ru => format!("Завершить {name} (pid {pid})?"),
Lang::Zh => format!("终止 {name} (pid {pid})?"),
}
}
pub fn fmt_kill_sent(&self, sig: &str, name: &str, pid: u32) -> String {
match lang() {
Lang::En => format!("sent {sig} to {name} (pid {pid})"),
Lang::Ru => format!("отправлен {sig} → {name} (pid {pid})"),
Lang::Zh => format!("已发送 {sig} → {name} (pid {pid})"),
}
}
pub fn fmt_kill_failed(&self, err: &str) -> String {
match lang() {
Lang::En => format!("kill failed: {err}"),
Lang::Ru => format!("ошибка завершения: {err}"),
Lang::Zh => format!("终止失败: {err}"),
}
}
pub fn fmt_scan_error(&self, err: &str) -> String {
format!("{}: {err}", self.scan_error)
}
pub fn fmt_all_ports(&self, n: usize) -> String {
match lang() {
Lang::En => format!("--- All ports of process ({n}) ---"),
Lang::Ru => format!("--- Все порты процесса ({n}) ---"),
Lang::Zh => format!("--- 进程所有端口 ({n}) ---"),
}
}
pub fn fmt_sudo_error(&self, err: &str) -> String {
match lang() {
Lang::En => format!("sudo: {err}"),
Lang::Ru => format!("sudo ошибка: {err}"),
Lang::Zh => format!("sudo 错误: {err}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lang_next_cycles_all() {
let cases = [
(Lang::En, Lang::Ru),
(Lang::Ru, Lang::Zh),
(Lang::Zh, Lang::En),
];
for (from, expected) in cases {
assert_eq!(from.next(), expected, "{:?}.next()", from);
}
}
#[test]
fn lang_next_full_cycle() {
let start = Lang::En;
let after_3 = start.next().next().next();
assert_eq!(after_3, start);
}
#[test]
fn lang_label() {
let cases = [(Lang::En, "EN"), (Lang::Ru, "RU"), (Lang::Zh, "ZH")];
for (lang, expected) in cases {
assert_eq!(lang.label(), expected);
}
}
#[test]
fn lang_from_u8_table() {
let cases = [
(0, Lang::En),
(1, Lang::Ru),
(2, Lang::Zh),
(99, Lang::En),
(255, Lang::En),
];
for (val, expected) in cases {
assert_eq!(Lang::from_u8(val), expected, "from_u8({val})");
}
}
#[test]
fn parse_lang_table() {
let cases = [
("en", Lang::En),
("ru", Lang::Ru),
("russian", Lang::Ru),
("zh", Lang::Zh),
("cn", Lang::Zh),
("chinese", Lang::Zh),
("EN", Lang::En),
("RU", Lang::Ru),
("ZH", Lang::Zh),
("unknown", Lang::En),
("", Lang::En),
("fr", Lang::En),
];
for (input, expected) in cases {
assert_eq!(parse_lang(input), expected, "parse_lang({input:?})");
}
}
#[test]
fn set_and_get_lang() {
set_lang(Lang::Ru);
assert_eq!(lang(), Lang::Ru);
set_lang(Lang::Zh);
assert_eq!(lang(), Lang::Zh);
set_lang(Lang::En);
assert_eq!(lang(), Lang::En);
}
#[test]
fn strings_returns_correct_lang() {
set_lang(Lang::En);
assert_eq!(strings().app_name, "PRT");
set_lang(Lang::Ru);
assert_eq!(strings().app_name, "PRT");
assert_eq!(strings().hint_quit, "выход");
set_lang(Lang::En);
assert_eq!(strings().hint_quit, "quit");
}
#[test]
fn strings_all_languages_have_non_empty_fields() {
for l in [Lang::En, Lang::Ru, Lang::Zh] {
set_lang(l);
let s = strings();
assert!(!s.app_name.is_empty(), "{:?} app_name empty", l);
assert!(!s.connections.is_empty(), "{:?} connections empty", l);
assert!(!s.help_text.is_empty(), "{:?} help_text empty", l);
assert!(!s.hint_help.is_empty(), "{:?} hint_help empty", l);
assert!(!s.hint_lang.is_empty(), "{:?} hint_lang empty", l);
assert!(!s.lang_switched.is_empty(), "{:?} lang_switched empty", l);
}
set_lang(Lang::En); }
#[test]
fn fmt_connections_contains_count() {
set_lang(Lang::En);
let s = strings();
assert!(s.fmt_connections(42).contains("42"));
}
#[test]
fn fmt_kill_confirm_contains_name_and_pid() {
for l in [Lang::En, Lang::Ru, Lang::Zh] {
set_lang(l);
let s = strings();
let msg = s.fmt_kill_confirm("nginx", 1234);
assert!(msg.contains("nginx"), "{:?}: {msg}", l);
assert!(msg.contains("1234"), "{:?}: {msg}", l);
}
set_lang(Lang::En);
}
}