diff --git a/Cargo.lock b/Cargo.lock index c0701150..05e49e67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,12 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arrayvec" version = "0.7.6" @@ -152,18 +158,40 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base62" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f879ef8fc74665ed7f0e6127cb106315888fc2744f68e14b74f83edbb2a08992" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -432,7 +460,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.6.0", "crossterm_winapi", "mio", "parking_lot", @@ -636,6 +664,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -726,6 +760,36 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -900,6 +964,22 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.7", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -978,6 +1058,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1024,6 +1113,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libyml" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64804cc6a5042d4f05379909ba25b503ec04e2c082151d62122d5dcaa274b961" + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1166,13 +1261,22 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cfg-if", "cfg_aliases", "libc", "memoffset", ] +[[package]] +name = "normpath" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1387,12 +1491,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cassowary", "compact_str", "crossterm", "instability", - "itertools", + "itertools 0.13.0", "lru", "paste", "strum", @@ -1408,7 +1512,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -1471,6 +1575,60 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" +[[package]] +name = "rust-i18n" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039f57d22229db401af3458ca939300178e99e88b938573cea12b7c2b0f09724" +dependencies = [ + "globwalk", + "once_cell", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde5c022360a2e54477882843d56b6f9bcb4bc62f504b651a2f497f0028d174f" +dependencies = [ + "glob", + "once_cell", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "serde_yml", + "syn 2.0.75", +] + +[[package]] +name = "rust-i18n-support" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75d2844d36f62b5d6b66f9cf8f8cbdbbbdcdb5fd37a473a9cc2fb45fdcf485d2" +dependencies = [ + "arc-swap", + "base62", + "globwalk", + "itertools 0.11.0", + "lazy_static", + "normpath", + "once_cell", + "proc-macro2", + "regex", + "serde", + "serde_json", + "serde_yml", + "siphasher", + "toml 0.7.8", + "triomphe", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1483,7 +1641,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -1502,6 +1660,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1592,6 +1759,23 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serde_yml" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e76bab63c3fd98d27c17f9cbce177f64a91f5e69ac04cafe04e1bb25d1dc3c" +dependencies = [ + "indexmap 2.4.0", + "itoa", + "libyml", + "log", + "memchr", + "ryu", + "serde", + "serde_json", + "tempfile", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1643,6 +1827,12 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -1668,6 +1858,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -1724,6 +1920,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "terminal_size" version = "0.3.0" @@ -1891,6 +2100,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + [[package]] name = "toml" version = "0.8.19" @@ -1900,7 +2121,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.22.20", ] [[package]] @@ -1912,6 +2133,19 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.4.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.22.20" @@ -1922,7 +2156,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.18", ] [[package]] @@ -1931,7 +2165,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "itertools", + "itertools 0.13.0", "trippy", ] @@ -2020,6 +2254,17 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "triomphe" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + [[package]] name = "trippy" version = "0.12.0-dev" @@ -2038,12 +2283,12 @@ version = "0.12.0-dev" dependencies = [ "anyhow", "arrayvec", - "bitflags", + "bitflags 2.6.0", "derive_more", "hex-literal", "indexmap 2.4.0", "ipnetwork", - "itertools", + "itertools 0.13.0", "mockall", "nix", "parking_lot", @@ -2073,7 +2318,7 @@ dependencies = [ "crossbeam", "dns-lookup", "hickory-resolver", - "itertools", + "itertools 0.13.0", "parking_lot", "thiserror", ] @@ -2084,7 +2329,7 @@ version = "0.12.0-dev" dependencies = [ "anyhow", "hex-literal", - "itertools", + "itertools 0.13.0", "thiserror", ] @@ -2117,11 +2362,12 @@ dependencies = [ "etcetera", "humantime", "insta", - "itertools", + "itertools 0.13.0", "maxminddb", "petgraph", "pretty_assertions", "ratatui", + "rust-i18n", "serde", "serde_json", "serde_with", @@ -2129,7 +2375,7 @@ dependencies = [ "strum", "test-case", "thiserror", - "toml", + "toml 0.8.19", "tracing", "tracing-chrome", "tracing-subscriber", @@ -2193,7 +2439,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ - "itertools", + "itertools 0.13.0", "unicode-segmentation", "unicode-width", ] @@ -2239,6 +2485,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2322,6 +2578,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2485,6 +2750,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.6.18" diff --git a/Cargo.toml b/Cargo.toml index c717c85f..88c69fd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ petgraph = "0.6.5" pretty_assertions = "1.4.0" rand = "0.8.5" ratatui = "0.28.1" +rust-i18n = "3.1.2" serde = { version = "1.0.201", default-features = false } serde_json = { version = "1.0.117", default-features = false } serde_with = "3.9.0" diff --git a/crates/trippy-core/src/config.rs b/crates/trippy-core/src/config.rs index 8e73adb1..0dc8347b 100644 --- a/crates/trippy-core/src/config.rs +++ b/crates/trippy-core/src/config.rs @@ -88,15 +88,6 @@ impl PrivilegeMode { } } -impl Display for PrivilegeMode { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::Privileged => write!(f, "privileged"), - Self::Unprivileged => write!(f, "unprivileged"), - } - } -} - /// The ICMP extension parsing mode. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum IcmpExtensionParseMode { diff --git a/crates/trippy-tui/Cargo.toml b/crates/trippy-tui/Cargo.toml index 147f7048..78d20d31 100644 --- a/crates/trippy-tui/Cargo.toml +++ b/crates/trippy-tui/Cargo.toml @@ -42,6 +42,7 @@ itertools.workspace = true maxminddb.workspace = true petgraph.workspace = true ratatui.workspace = true +rust-i18n.workspace = true serde = { workspace = true, default-features = false, features = [ "derive" ] } serde_json.workspace = true serde_with.workspace = true diff --git a/crates/trippy-tui/src/frontend/render/bsod.rs b/crates/trippy-tui/src/frontend/render/bsod.rs index 0907b8ad..58312660 100644 --- a/crates/trippy-tui/src/frontend/render/bsod.rs +++ b/crates/trippy-tui/src/frontend/render/bsod.rs @@ -3,6 +3,7 @@ use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, BorderType, Borders, Paragraph}; use ratatui::Frame; +use rust_i18n::t; /// Render a blue screen of death. pub fn render(f: &mut Frame<'_>, rect: Rect, error: &str) { @@ -10,7 +11,7 @@ pub fn render(f: &mut Frame<'_>, rect: Rect, error: &str) { .constraints([Constraint::Percentage(35), Constraint::Percentage(65)].as_ref()) .split(rect); let block = Block::default() - .title("Hops") + .title(t!("title_hops").to_string()) .borders(Borders::ALL) .border_type(BorderType::Rounded) .style(Style::default().bg(Color::Blue)); diff --git a/crates/trippy-tui/src/frontend/render/flows.rs b/crates/trippy-tui/src/frontend/render/flows.rs index ec66b053..a202f847 100644 --- a/crates/trippy-tui/src/frontend/render/flows.rs +++ b/crates/trippy-tui/src/frontend/render/flows.rs @@ -4,6 +4,7 @@ use ratatui::style::{Modifier, Style}; use ratatui::text::Line; use ratatui::widgets::{Bar, BarChart, BarGroup, Block, BorderType, Borders}; use ratatui::Frame; +use rust_i18n::t; /// Render the flows. pub fn render(f: &mut Frame<'_>, rect: Rect, app: &TuiApp) { @@ -35,7 +36,7 @@ pub fn render(f: &mut Frame<'_>, rect: Rect, app: &TuiApp) { }) .collect(); let block = Block::default() - .title("Flows") + .title(t!("title_flows").to_string()) .title_alignment(Alignment::Left) .borders(Borders::ALL) .border_type(BorderType::Rounded) diff --git a/crates/trippy-tui/src/frontend/render/header.rs b/crates/trippy-tui/src/frontend/render/header.rs index a48e8d19..a29b47b8 100644 --- a/crates/trippy-tui/src/frontend/render/header.rs +++ b/crates/trippy-tui/src/frontend/render/header.rs @@ -6,16 +6,18 @@ use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, BorderType, Borders, Paragraph}; use ratatui::Frame; +use rust_i18n::t; +use std::borrow::Cow; use std::net::IpAddr; use std::time::Duration; -use trippy_core::{Hop, PortDirection, Protocol}; +use trippy_core::{Hop, PortDirection, PrivilegeMode, Protocol}; use trippy_dns::{ResolveMethod, Resolver}; /// Render the title, config, target, clock and keyboard controls. -#[allow(clippy::too_many_lines)] +#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { let header_block = Block::default() - .title(format!(" Trippy v{} ", clap::crate_version!())) + .title(format!(" {} v{} ", t!("trippy"), clap::crate_version!())) .title_alignment(Alignment::Center) .borders(Borders::ALL) .border_type(BorderType::Rounded) @@ -27,6 +29,7 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { ); let now = chrono::Local::now().to_rfc3339_opts(SecondsFormat::Secs, true); let clock_span = Line::from(Span::raw(now)); + // TODO let help_span = Line::from(vec![ Span::styled("h", Style::default().add_modifier(Modifier::BOLD)), Span::raw("elp "), @@ -42,80 +45,98 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { .alignment(Alignment::Right); let protocol = match app.tracer_config().data.protocol() { Protocol::Icmp => format!( - "icmp({}, {})", - render_target_family(app.tracer_config().data.target_addr()), - app.tracer_config().data.privilege_mode() + "{}({}, {})", + t!("icmp"), + fmt_target_family(app.tracer_config().data.target_addr()), + fmt_privilege_mode(app.tracer_config().data.privilege_mode()) ), Protocol::Udp => format!( - "udp({}, {}, {})", - render_target_family(app.tracer_config().data.target_addr()), + "{}({}, {}, {})", + t!("udp"), + fmt_target_family(app.tracer_config().data.target_addr()), app.tracer_config().data.multipath_strategy(), - app.tracer_config().data.privilege_mode() + fmt_privilege_mode(app.tracer_config().data.privilege_mode()) ), Protocol::Tcp => format!( - "tcp({}, {})", - render_target_family(app.tracer_config().data.target_addr()), - app.tracer_config().data.privilege_mode() + "{}({}, {})", + t!("tcp"), + fmt_target_family(app.tracer_config().data.target_addr()), + fmt_privilege_mode(app.tracer_config().data.privilege_mode()) ), }; let details = if app.show_hop_details { - String::from("on") + String::from(t!("on")) } else { - String::from("off") + String::from(t!("off")) }; let as_info = match app.resolver.config().resolve_method { - ResolveMethod::System => String::from("n/a"), + ResolveMethod::System => String::from(t!("na")), ResolveMethod::Resolv | ResolveMethod::Google | ResolveMethod::Cloudflare => { if app.tui_config.lookup_as_info { - String::from("on") + String::from(t!("on")) } else { - String::from("off") + String::from(t!("off")) } } }; let max_hosts = app .tui_config .max_addrs - .map_or_else(|| String::from("auto"), |m| m.to_string()); + .map_or_else(|| String::from(t!("auto")), |m| m.to_string()); let privacy = if app.hide_private_hops && app.tui_config.privacy_max_ttl > 0 { - "on" + t!("on") } else { - "off" + t!("off") }; let source = render_source(app); let dest = render_destination(app); let target = format!("{source} -> {dest}"); + let hop_count = app.tracer_data().hops_for_flow(app.selected_flow).len(); let discovered = if app.selected_tracer_data.max_flows() > 1 { let plural_flows = if app.tracer_data().flows().len() > 1 { - "flows" + t!("flows") } else { - "flow" + t!("flow") }; + let flow_count = app.tracer_data().flows().len(); format!( - ", discovered {} hops and {} unique {}", - app.tracer_data().hops_for_flow(app.selected_flow).len(), - app.tracer_data().flows().len(), - plural_flows + ", {}", + t!("discovered_flows", + "hop_count" => hop_count, + "flow_count" => flow_count, + "plural_flows" => plural_flows + ) ) } else { - format!( - ", discovered {} hops", - app.tracer_data().hops_for_flow(app.selected_flow).len() - ) + format!(", {}", t!("discovered", "hop_count" => hop_count)) }; let left_line = vec![ Line::from(vec![ - Span::styled("Target: ", Style::default().add_modifier(Modifier::BOLD)), + Span::styled( + format!("{}: ", t!("target")), + Style::default().add_modifier(Modifier::BOLD), + ), Span::raw(target), ]), Line::from(vec![ - Span::styled("Config: ", Style::default().add_modifier(Modifier::BOLD)), + Span::styled( + format!("{}: ", t!("config")), + Style::default().add_modifier(Modifier::BOLD), + ), Span::raw(format!( - "protocol={protocol} as-info={as_info} details={details} max-hosts={max_hosts}, privacy={privacy}" + "{}={protocol} {}={as_info} {}={details} {}={max_hosts}, {}={privacy}", + t!("protocol"), + t!("as-info"), + t!("details"), + t!("max-hosts"), + t!("privacy") )), ]), Line::from(vec![ - Span::styled("Status: ", Style::default().add_modifier(Modifier::BOLD)), + Span::styled( + format!("{}: ", t!("status")), + Style::default().add_modifier(Modifier::BOLD), + ), Span::raw(render_status(app)), Span::raw(discovered), ]), @@ -129,7 +150,14 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { f.render_widget(left, rect); } -const fn render_target_family(target: IpAddr) -> &'static str { +fn fmt_privilege_mode(privilege_mode: PrivilegeMode) -> Cow<'static, str> { + match privilege_mode { + PrivilegeMode::Privileged => t!("privileged"), + PrivilegeMode::Unprivileged => t!("unprivileged"), + } +} + +const fn fmt_target_family(target: IpAddr) -> &'static str { match target { IpAddr::V4(_) => "v4", IpAddr::V6(_) => "v6", @@ -152,7 +180,7 @@ fn render_source(app: &TuiApp) -> String { } } } else { - String::from("unknown") + String::from(t!("unknown")) } } @@ -193,18 +221,19 @@ fn render_status(app: &TuiApp) -> String { } else { 0_f64 }; + // TODO format!(" [{failure_count} of {total_probes} ({failure_rate:.1}%) probes failed❗]") } else { String::new() }; if app.selected_tracer_data.error().is_some() { - String::from("Failed") + String::from(t!("status_failed")) } else if let Some(start) = app.frozen_start { let frozen = format_duration(Duration::from_secs( start.elapsed().unwrap_or_default().as_secs(), )); - format!("Frozen ({frozen}){failures}") + format!("{} ({frozen}){failures}", t!("status_frozen")) } else { - format!("Running{failures}") + format!("{}{failures}", t!("status_running")) } } diff --git a/crates/trippy-tui/src/frontend/render/help.rs b/crates/trippy-tui/src/frontend/render/help.rs index 57766341..be638170 100644 --- a/crates/trippy-tui/src/frontend/render/help.rs +++ b/crates/trippy-tui/src/frontend/render/help.rs @@ -5,6 +5,7 @@ use ratatui::style::Style; use ratatui::text::Line; use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph}; use ratatui::Frame; +use rust_i18n::t; /// Render help dialog. pub fn render(f: &mut Frame<'_>, app: &TuiApp) { @@ -33,7 +34,7 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp) { r#" Copyright 2022 Trippy Contributors "#.to_string(), ]; let block = Block::default() - .title(" Help ") + .title(format!(" {} ", t!("title_help"))) .title_alignment(Alignment::Center) .borders(Borders::ALL) .style(Style::default().bg(app.tui_config.theme.help_dialog_bg)) diff --git a/crates/trippy-tui/src/frontend/render/histogram.rs b/crates/trippy-tui/src/frontend/render/histogram.rs index 8651275d..d85d2fb3 100644 --- a/crates/trippy-tui/src/frontend/render/histogram.rs +++ b/crates/trippy-tui/src/frontend/render/histogram.rs @@ -3,6 +3,7 @@ use ratatui::layout::Rect; use ratatui::style::{Modifier, Style}; use ratatui::widgets::{BarChart, Block, BorderType, Borders}; use ratatui::Frame; +use rust_i18n::t; use std::collections::BTreeMap; use std::time::Duration; @@ -14,7 +15,7 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { let barchart = BarChart::default() .block( Block::default() - .title(format!("Frequency #{}", selected_hop.ttl())) + .title(format!("{} #{}", t!("title_frequency"), selected_hop.ttl())) .style( Style::default() .bg(app.tui_config.theme.bg) diff --git a/crates/trippy-tui/src/frontend/render/history.rs b/crates/trippy-tui/src/frontend/render/history.rs index d1e0f30d..8a33b0c4 100644 --- a/crates/trippy-tui/src/frontend/render/history.rs +++ b/crates/trippy-tui/src/frontend/render/history.rs @@ -4,6 +4,7 @@ use ratatui::layout::Rect; use ratatui::style::Style; use ratatui::widgets::{Block, BorderType, Borders}; use ratatui::Frame; +use rust_i18n::t; /// Render the ping history for the final hop which is typically the target. pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { @@ -23,7 +24,7 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { let history = Sparkline::default() .block( Block::default() - .title(format!("Samples #{}", selected_hop.ttl())) + .title(format!("{} #{}", t!("title_samples"), selected_hop.ttl())) .style( Style::default() .bg(app.tui_config.theme.bg) diff --git a/crates/trippy-tui/src/frontend/render/splash.rs b/crates/trippy-tui/src/frontend/render/splash.rs index f9315fb0..da4524bc 100644 --- a/crates/trippy-tui/src/frontend/render/splash.rs +++ b/crates/trippy-tui/src/frontend/render/splash.rs @@ -4,6 +4,8 @@ use ratatui::style::Style; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, BorderType, Borders, Paragraph}; use ratatui::Frame; +use rust_i18n::t; +use std::borrow::Cow; /// Render the splash screen. /// @@ -13,7 +15,7 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { .constraints([Constraint::Percentage(35), Constraint::Percentage(65)].as_ref()) .split(rect); let block = Block::default() - .title("Hops") + .title(t!("title_hops").to_string()) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.tui_config.theme.border)) @@ -23,14 +25,14 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { .fg(app.tui_config.theme.text), ); #[allow(clippy::needless_raw_string_hashes)] - let splash = vec![ - r#" _____ _ "#, - r#"|_ _| _(_)_ __ _ __ _ _ "#, - r#" | || '_| | '_ \ '_ \ || |"#, - r#" |_||_| |_| .__/ .__/\_, |"#, - r#" |_| |_| |__/ "#, - "", - "Awaiting data...", + let splash: Vec> = vec![ + r#" _____ _ "#.into(), + r#"|_ _| _(_)_ __ _ __ _ _ "#.into(), + r#" | || '_| | '_ \ '_ \ || |"#.into(), + r#" |_||_| |_| .__/ .__/\_, |"#.into(), + r#" |_| |_| |__/ "#.into(), + "".into(), + t!("awaiting_data"), ]; let line: Vec<_> = splash .into_iter() diff --git a/crates/trippy-tui/src/frontend/render/table.rs b/crates/trippy-tui/src/frontend/render/table.rs index 83daa106..d3385181 100644 --- a/crates/trippy-tui/src/frontend/render/table.rs +++ b/crates/trippy-tui/src/frontend/render/table.rs @@ -9,6 +9,7 @@ use ratatui::layout::Rect; use ratatui::style::{Modifier, Style}; use ratatui::widgets::{Block, BorderType, Borders, Cell, Row, Table}; use ratatui::Frame; +use rust_i18n::t; use std::net::IpAddr; use std::rc::Rc; use trippy_core::{Extension, Extensions, IcmpPacketType, MplsLabelStackMember, UnknownExtension}; @@ -63,7 +64,7 @@ pub fn render(f: &mut Frame<'_>, app: &mut TuiApp, rect: Rect) { .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.tui_config.theme.border)) - .title("Hops"), + .title(t!("title_hops").to_string()), ) .style( Style::default() diff --git a/crates/trippy-tui/src/frontend/render/tabs.rs b/crates/trippy-tui/src/frontend/render/tabs.rs index 8e15941b..d07395ab 100644 --- a/crates/trippy-tui/src/frontend/render/tabs.rs +++ b/crates/trippy-tui/src/frontend/render/tabs.rs @@ -4,11 +4,12 @@ use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, BorderType, Borders, Tabs}; use ratatui::Frame; +use rust_i18n::t; /// Render the tabs, one per trace. pub fn render(f: &mut Frame<'_>, rect: Rect, app: &TuiApp) { let tabs_block = Block::default() - .title("Traces") + .title(t!("title_traces").to_string()) .title_alignment(Alignment::Left) .borders(Borders::ALL) .border_type(BorderType::Rounded) diff --git a/crates/trippy-tui/src/frontend/render/world.rs b/crates/trippy-tui/src/frontend/render/world.rs index 304e8315..88829fb7 100644 --- a/crates/trippy-tui/src/frontend/render/world.rs +++ b/crates/trippy-tui/src/frontend/render/world.rs @@ -7,6 +7,7 @@ use ratatui::text::Span; use ratatui::widgets::canvas::{Canvas, Circle, Context, Map, MapResolution, Rectangle}; use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph}; use ratatui::Frame; +use rust_i18n::t; use std::collections::HashMap; use trippy_core::Hop; @@ -32,7 +33,7 @@ fn render_map_canvas(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, entries: &[Map .background_color(app.tui_config.theme.bg) .block( Block::default() - .title("Map") + .title(t!("title_map").to_string()) .borders(Borders::ALL) .border_style(Style::default().fg(app.tui_config.theme.border)) .style( diff --git a/crates/trippy-tui/src/lib.rs b/crates/trippy-tui/src/lib.rs index d1475796..e27c80df 100644 --- a/crates/trippy-tui/src/lib.rs +++ b/crates/trippy-tui/src/lib.rs @@ -19,8 +19,14 @@ mod print; mod report; mod util; +// TODO where to do this? +rust_i18n::i18n!("../../locales"); + /// Run the Trippy application. pub fn trippy() -> anyhow::Result<()> { + // TODO how to get locale? Need user override? + rust_i18n::set_locale("zh-CN"); + let args = Args::parse(); let privilege = Privilege::acquire_privileges()?; let pid = u16::try_from(process::id() % u32::from(u16::MAX))?; diff --git a/locales/app.yml b/locales/app.yml new file mode 100644 index 00000000..02bc39b8 --- /dev/null +++ b/locales/app.yml @@ -0,0 +1,99 @@ +_version: 2 +trippy: + en: trippy +auto: + en: auto + zh-CN: 自动 +on: + en: on + zh-CN: 开 +off: + en: off + zh-CN: 关 +flow: + en: flow + zh-CN: 流量 +flows: + en: flows + zh-CN: 流量 +target: + en: Target + zh-CN: 目标 +config: + en: Config + zh-CN: 配置 +status: + en: Status + zh-CN: 状态 +protocol: + en: protocol + zh-CN: 协议 +as-info: + en: as-info + zh-CN: AS-信息 +details: + en: details + zh-CN: 详情 +max-hosts: + en: max-hosts + zh-CN: 最大主机数 +privacy: + en: privacy + zh-CN: 隐私 +privileged: + en: privileged + zh-CN: 特权 +unprivileged: + en: unprivileged + zh-CN: 非特权 +na: + en: n/a + zh-CN: 无 +discovered: + en: discovered %{hop_count} hops + zh-CN: 发现 %{hop_count} 跳 +discovered_flows: + en: discovered %{hop_count} hops and %{flow_count} unique %{plural_flows} + zh-CN: 发现 %{hop_count} 跳和 %{flow_count} 个唯一 %{plural_flows} +unknown: + en: unknown + zh-CN: 未知 +icmp: + en: icmp +udp: + en: udp +tcp: + en: tcp +status_failed: + en: Failed + zh-CN: 失败 +status_running: + en: Running + zh-CN: 运行中 +status_frozen: + en: Frozen + zh-CN: 冻结 +awaiting_data: + en: Awaiting data... + zh-CN: 等待数据... +title_hops: + en: Hops + zh-CN: 跳 +title_frequency: + en: Frequency + zh-CN: 频率 +title_samples: + en: Samples + zh-CN: 样本 +title_traces: + en: Traces + zh-CN: 跟踪 +title_flows: + en: Flows + zh-CN: 流量 +title_map: + en: Map + zh-CN: 地图 +title_help: + en: Help + zh-CN: 帮助 \ No newline at end of file