From 092c53fdd0d30f2e3bc8e273746b402de3298b74 Mon Sep 17 00:00:00 2001 From: Marco Cadetg Date: Wed, 24 Dec 2025 16:15:22 +0100 Subject: [PATCH] feat: add internationalization (i18n) support Add multi-language support using rust-i18n with 6 locales: - English (en) - source of truth - Spanish (es) - German (de) - French (fr) - Chinese (zh) - Russian (ru) Features: - Automatic system locale detection via sys-locale - Manual override via --lang flag or RUSTNET_LANG env var - Translated CLI help text, UI labels, error messages - CI check to ensure all translation keys are consistent The locale is detected before CLI parsing so that --help output is displayed in the user's language. --- .github/workflows/rust.yml | 10 ++ Cargo.lock | 278 ++++++++++++++++++++++++++++- Cargo.toml | 3 + README.md | 1 + ROADMAP.md | 2 +- USAGE.md | 32 ++++ assets/locales/de.yml | 221 +++++++++++++++++++++++ assets/locales/en.yml | 223 ++++++++++++++++++++++++ assets/locales/es.yml | 220 +++++++++++++++++++++++ assets/locales/fr.yml | 221 +++++++++++++++++++++++ assets/locales/ru.yml | 221 +++++++++++++++++++++++ assets/locales/zh.yml | 221 +++++++++++++++++++++++ build.rs | 5 + scripts/check-locales.py | 84 +++++++++ src/cli.rs | 67 ++++--- src/lib.rs | 3 + src/main.rs | 54 +++++- src/network/privileges.rs | 55 +++--- src/ui.rs | 346 ++++++++++++++++++------------------- 19 files changed, 2025 insertions(+), 242 deletions(-) create mode 100644 assets/locales/de.yml create mode 100644 assets/locales/en.yml create mode 100644 assets/locales/es.yml create mode 100644 assets/locales/fr.yml create mode 100644 assets/locales/ru.yml create mode 100644 assets/locales/zh.yml create mode 100755 scripts/check-locales.py diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a6191e8..cafe456 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,6 +9,8 @@ on: - 'Cargo.lock' - 'assets/services' - 'build.rs' + - 'assets/locales/**' + - 'scripts/check-locales.py' - '.github/workflows/rust.yml' pull_request: branches: [ "main" ] @@ -18,6 +20,8 @@ on: - 'Cargo.lock' - 'assets/services' - 'build.rs' + - 'assets/locales/**' + - 'scripts/check-locales.py' - '.github/workflows/rust.yml' workflow_dispatch: @@ -34,7 +38,13 @@ jobs: - uses: actions/checkout@v6 - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y libpcap-dev libelf-dev zlib1g-dev clang llvm pkg-config + - name: Check translation keys + run: python3 scripts/check-locales.py - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose + - name: Security audit + run: | + cargo install cargo-audit + cargo audit diff --git a/Cargo.lock b/Cargo.lock index 1ed79d6..526f119 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,12 +129,27 @@ dependencies = [ "x11rb", ] +[[package]] +name = "arc-swap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base62" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adf9755786e27479693dedd3271691a92b5e242ab139cacb9fb8e7fb5381111" + [[package]] name = "base64" version = "0.22.1" @@ -162,6 +177,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -906,6 +931,36 @@ dependencies = [ "wasi 0.14.7+wasi-0.2.4", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[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 = "half" version = "2.7.0" @@ -1009,6 +1064,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "image" version = "0.25.8" @@ -1076,6 +1147,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" @@ -1344,6 +1424,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "normpath" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -1750,7 +1839,7 @@ dependencies = [ "crossterm 0.28.1", "indoc", "instability", - "itertools", + "itertools 0.13.0", "lru", "paste", "strum", @@ -1824,6 +1913,60 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" +[[package]] +name = "rust-i18n" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332" +dependencies = [ + "globwalk", + "once_cell", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965" +dependencies = [ + "glob", + "once_cell", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "serde_yaml", + "syn", +] + +[[package]] +name = "rust-i18n-support" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19" +dependencies = [ + "arc-swap", + "base62", + "globwalk", + "itertools 0.11.0", + "lazy_static", + "normpath", + "once_cell", + "proc-macro2", + "regex", + "serde", + "serde_json", + "serde_yaml", + "siphasher", + "toml", + "triomphe", +] + [[package]] name = "rustix" version = "0.38.44" @@ -1879,10 +2022,12 @@ dependencies = [ "procfs", "ratatui", "ring", + "rust-i18n", "serde", "serde_json", "simple-logging", "simplelog", + "sys-locale", "windows", "zip", ] @@ -1899,6 +2044,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[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 = "schannel" version = "0.1.28" @@ -1990,6 +2144,28 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2085,6 +2261,12 @@ dependencies = [ "time", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "smallvec" version = "1.15.1" @@ -2101,6 +2283,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2152,6 +2340,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + [[package]] name = "tempfile" version = "3.23.0" @@ -2261,6 +2458,47 @@ dependencies = [ "time-core", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tracing" version = "0.1.41" @@ -2304,6 +2542,17 @@ dependencies = [ "petgraph", ] +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + [[package]] name = "typenum" version = "1.19.0" @@ -2334,7 +2583,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 0.1.14", ] @@ -2351,6 +2600,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -2385,6 +2640,16 @@ dependencies = [ "libc", ] +[[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.1+wasi-snapshot-preview1" @@ -2893,6 +3158,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index 31bab69..357333f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,8 @@ ring = "0.17" aes = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +rust-i18n = "3" +sys-locale = "0.3" [target.'cfg(target_os = "linux")'.dependencies] procfs = "0.18" @@ -74,6 +76,7 @@ anyhow = "1.0" clap = { version = "4.5", features = ["derive"] } clap_complete = "4.5" clap_mangen = "0.2" +rust-i18n = "3" [target.'cfg(windows)'.build-dependencies] http_req = "0.14" diff --git a/README.md b/README.md index aea33a9..6a3dbd4 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ A cross-platform network monitoring tool built with Rust. RustNet provides real- - **Multi-threaded Processing**: Concurrent packet processing for high performance - **Optional Logging**: Detailed logging with configurable log levels (disabled by default) - **Security Sandboxing**: Landlock-based filesystem/network restrictions on Linux 5.13+ (see [SECURITY.md](SECURITY.md)) +- **Multi-language Support**: UI available in English, Spanish, German, French, Chinese, and Russian
eBPF Enhanced Process Identification (Linux Default) diff --git a/ROADMAP.md b/ROADMAP.md index 00e6cd3..8b77034 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -108,7 +108,7 @@ The experimental eBPF support provides efficient process identification but has ### Future Enhancements -- [ ] **Internationalization (i18n)**: Support for multiple languages in the UI +- [x] **Internationalization (i18n)**: Support for multiple languages in the UI (English, Spanish, German, French, Chinese, Russian) - [ ] **Connection History**: Store and display historical connection data - [ ] **Export Functionality**: Export connections to CSV/JSON formats - [ ] **Configuration File**: Support for persistent configuration (filters, UI preferences) diff --git a/USAGE.md b/USAGE.md index 3d96337..9f879f4 100644 --- a/USAGE.md +++ b/USAGE.md @@ -85,6 +85,7 @@ Options: -l, --log-level Set the log level (if not provided, no logging will be enabled) --json-log Enable JSON logging of connection events to specified file -f, --bpf-filter BPF filter expression for packet capture + --lang Override system locale (e.g., en, es, de, fr, zh, ru) --no-sandbox Disable Landlock sandboxing (Linux only) --sandbox-strict Require full sandbox enforcement or exit (Linux only) -h, --help Print help @@ -234,6 +235,37 @@ Enable logging with the specified level. Logging is **disabled by default**. Log files are created in the `logs/` directory with timestamp: `rustnet_YYYY-MM-DD_HH-MM-SS.log` +#### `--lang ` + +Override the system locale for the user interface. RustNet automatically detects your system language, but you can override it with this option. + +**Available locales:** +- `en` - English (default) +- `es` - Spanish (Español) +- `de` - German (Deutsch) +- `fr` - French (Français) +- `zh` - Chinese (中文) +- `ru` - Russian (Русский) + +**Examples:** +```bash +# Use German interface +rustnet --lang de + +# Use Spanish interface +rustnet --lang es +``` + +**Environment variable:** You can also set `RUSTNET_LANG` to override the locale: +```bash +RUSTNET_LANG=fr rustnet +``` + +**Priority order:** +1. `--lang` command-line flag (highest priority) +2. `RUSTNET_LANG` environment variable +3. System locale detection (automatic) + ## Keyboard Controls ### Navigation diff --git a/assets/locales/de.yml b/assets/locales/de.yml new file mode 100644 index 0000000..15fe4ab --- /dev/null +++ b/assets/locales/de.yml @@ -0,0 +1,221 @@ +# ============================================================================= +# RustNet Monitor - German Translations (Deutsche Übersetzungen) +# TODO: Translate strings from English to German +# ============================================================================= + +# Application name +"app.name": "RustNet Monitor" + +# Tab names +"tabs.overview": "Übersicht" +"tabs.details": "Details" +"tabs.interfaces": "Schnittstellen" +"tabs.graph": "Grafik" +"tabs.help": "Hilfe" + +# Table headers +"table.headers.protocol": "Pro" +"table.headers.local_address": "Lokale Adresse" +"table.headers.remote_address": "Remote-Adresse" +"table.headers.state": "Status" +"table.headers.service": "Dienst" +"table.headers.application": "Anwendung / Host" +"table.headers.bandwidth": "↓/↑" +"table.headers.process": "Prozess" +"table.title": "Aktive Verbindungen" +"table.title_sorted": "Aktive Verbindungen (Sortierung: %{column} %{direction})" + +# Sort column display names +"sort.time": "Zeit" +"sort.bandwidth": "Bandbreite Gesamt" +"sort.process": "Prozess" +"sort.local_addr": "Lokale Adr." +"sort.remote_addr": "Remote Adr." +"sort.application": "Anwendung" +"sort.service": "Dienst" +"sort.state": "Status" +"sort.protocol": "Protokoll" + +# Statistics panel +"stats.title": "Statistiken" +"stats.interface": "Schnittstelle: %{name}" +"stats.link_layer": "Verbindungsschicht: %{type}" +"stats.link_layer_tunnel": "Verbindungsschicht: %{type} (Tunnel)" +"stats.process_detection": "Prozesserkennung: %{method}" +"stats.tcp_connections": "TCP-Verbindungen: %{count}" +"stats.udp_connections": "UDP-Verbindungen: %{count}" +"stats.total_connections": "Verbindungen Gesamt: %{count}" +"stats.packets_processed": "Verarbeitete Pakete: %{count}" +"stats.packets_dropped": "Verworfene Pakete: %{count}" + +# Network stats panel +"network_stats.title": "Netzwerk-Stats" + +# Security panel +"security.title": "Sicherheit" +"security.landlock": "Landlock:" +"security.kernel_supported": "[Kernel unterstützt]" +"security.kernel_unsupported": "[Kernel nicht unterstützt]" +"security.no_restrictions": "Keine Einschränkungen aktiv" +"security.cap_dropped": "CAP_NET_RAW entfernt" +"security.fs_restricted": "FS eingeschränkt" +"security.net_blocked": "Netz blockiert" +"security.running_as_root": "Läuft als root (UID 0)" +"security.running_as_uid": "Läuft als UID %{uid}" +"security.running_as_admin": "Läuft als Administrator" +"security.running_as_standard": "Läuft als Standardbenutzer" + +# Interface stats panel +"interface_stats.title": "Interface-Stats (drücke 'i')" +"interface_stats.full_title": "Interface-Statistiken (Drücke 'i' zum Umschalten)" +"interface_stats.no_stats": "Noch keine Interface-Statistiken verfügbar..." +"interface_stats.more": "... %{count} weitere (drücke 'i')" +"interface_stats.err_label": "Err: " +"interface_stats.drop_label": "Drop: " +"interface_stats.headers.interface": "Schnittstelle" +"interface_stats.headers.rx_rate": "RX Rate" +"interface_stats.headers.tx_rate": "TX Rate" +"interface_stats.headers.rx_packets": "RX Pakete" +"interface_stats.headers.tx_packets": "TX Pakete" +"interface_stats.headers.rx_err": "RX Err" +"interface_stats.headers.tx_err": "TX Err" +"interface_stats.headers.rx_drop": "RX Drop" +"interface_stats.headers.tx_drop": "TX Drop" +"interface_stats.headers.collisions": "Kollisionen" + +# Graph tab +"graph.traffic_title": "Datenverkehr über Zeit (60s)" +"graph.connections_title": "Verbindungen" +"graph.connections_label": "%{count} Verbindungen" +"graph.collecting_data": "Sammle Daten..." +"graph.collecting_short": "Sammeln..." +"graph.app_distribution": "Anwendungsverteilung" +"graph.no_connections": "Keine Verbindungen" +"graph.top_processes": "Top Prozesse" +"graph.no_active_processes": "Keine aktiven Prozesse" +"graph.network_health": "Netzwerk-Gesundheit" +"graph.tcp_counters": "TCP-Zähler" +"graph.tcp_states": "TCP-Status" +"graph.no_tcp_connections": "Keine TCP-Verbindungen" +"graph.axis.time": "Zeit" +"graph.axis.rate": "Rate" +"graph.legend.rx": "RX (eingehend)" +"graph.legend.tx": "TX (ausgehend)" + +# Connection details +"details.title": "Verbindungsdetails" +"details.info_title": "Verbindungsinformation" +"details.no_connections": "Keine Verbindungen verfügbar" +"details.traffic_title": "Verkehrsstatistik" +"details.bytes_sent": "Bytes gesendet:" +"details.bytes_received": "Bytes empfangen:" +"details.packets_sent": "Pakete gesendet:" +"details.packets_received": "Pakete empfangen:" +"details.current_rate_in": "Aktuelle Rate (Eingang):" +"details.current_rate_out": "Aktuelle Rate (Ausgang):" +"details.initial_rtt": "Initiale RTT:" + +# Filter input +"filter.title_active": "Filter (↑↓/jk navigieren, Enter bestätigen, Esc abbrechen)" +"filter.title_applied": "Aktiver Filter (Esc zum Löschen)" + +# Status bar messages +"status.quit_confirm": "Drücke 'q' erneut zum Beenden oder eine andere Taste zum Abbrechen" +"status.clear_confirm": "Drücke 'x' erneut um alle Verbindungen zu löschen oder eine andere Taste zum Abbrechen" +"status.default": "'h' Hilfe | Tab/Shift+Tab Tabs wechseln | '/' Filter | 'c' Kopieren | Verbindungen: %{count}" +"status.help_hint": "Drücke 'h' für Hilfe | 'c' zum Kopieren der Adresse | Verbindungen: %{count}" +"status.filter_active": "'h' Hilfe | Tab/Shift+Tab Tabs wechseln | Zeige %{count} gefilterte Verbindungen (Esc zum Löschen)" + +# Loading screen +"loading.title": "RustNet Monitor" +"loading.message": "Lade Netzwerkverbindungen..." +"loading.subtitle": "Dies kann einige Sekunden dauern" + +# Help screen +"help.title": "RustNet Monitor" +"help.subtitle": "Netzwerkverbindungs-Monitor" +"help.keys.quit": "Anwendung beenden (zweimal drücken zum Bestätigen)" +"help.keys.quit_immediate": "Sofort beenden" +"help.keys.switch_tabs": "Zwischen Tabs wechseln" +"help.keys.navigate": "Verbindungen navigieren (mit Umbruch)" +"help.keys.jump_first_last": "Zur ersten/letzten Verbindung springen (vim-Stil)" +"help.keys.page_navigate": "Verbindungen seitenweise navigieren" +"help.keys.copy_address": "Remote-Adresse in Zwischenablage kopieren" +"help.keys.toggle_ports": "Zwischen Dienstnamen und Portnummern wechseln" +"help.keys.toggle_hostnames": "Zwischen Hostnamen und IPs wechseln (mit --resolve-dns)" +"help.keys.cycle_sort": "Sortierspalten durchlaufen (Bandbreite, Prozess, etc.)" +"help.keys.toggle_sort_dir": "Sortierrichtung umschalten (aufsteigend/absteigend)" +"help.keys.view_details": "Verbindungsdetails anzeigen" +"help.keys.return_overview": "Zurück zur Übersicht" +"help.keys.toggle_help": "Diese Hilfe ein-/ausblenden" +"help.keys.toggle_interfaces": "Interface-Statistiken ein-/ausblenden" +"help.keys.filter_mode": "Filtermodus aktivieren (während Eingabe navigieren!)" +"help.keys.clear_connections": "Alle Verbindungen löschen" +"help.sections.tabs": "Tabs:" +"help.sections.overview_desc": "Verbindungsliste mit Mini-Verkehrsgrafik" +"help.sections.details_desc": "Vollständige Details zur ausgewählten Verbindung" +"help.sections.interfaces_desc": "Netzwerk-Interface-Statistiken" +"help.sections.graph_desc": "Verkehrsdiagramme und Protokollverteilung" +"help.sections.help_desc": "Diese Hilfe" +"help.sections.colors": "Verbindungsfarben:" +"help.colors.white": "Weiß" +"help.colors.yellow": "Gelb" +"help.colors.red": "Rot" +"help.sections.colors_white": "Aktive Verbindung (< 75% des Timeouts)" +"help.sections.colors_yellow": "Veraltete Verbindung (75-90% des Timeouts)" +"help.sections.colors_red": "Kritisch - wird bald entfernt (> 90% des Timeouts)" +"help.sections.filter_examples": "Filterbeispiele:" +"help.filter_examples.general": "Suche nach '%{term}' in allen Feldern" +"help.filter_examples.port": "Filter Ports mit '%{term}' (443, 8080, etc.)" +"help.filter_examples.src": "Nach Quell-IP-Präfix filtern" +"help.filter_examples.dst": "Nach Ziel filtern" +"help.filter_examples.sni": "Nach SNI-Hostname filtern" +"help.filter_examples.process": "Nach Prozessname filtern" + +# Error messages + +# Privilege error messages (platform-specific) +"privileges.insufficient": "Unzureichende Berechtigungen für Netzwerk-Paketerfassung." +"privileges.missing": "Fehlend" +"privileges.how_to_fix": "Lösung" +# Linux privileges +"privileges.linux.cap_net_raw_missing": "CAP_NET_RAW-Fähigkeit (erforderlich für Paketerfassung)" +"privileges.linux.run_sudo": "Mit sudo ausführen: sudo rustnet" +"privileges.linux.setcap_modern": "Fähigkeiten setzen (modernes Linux 5.8+, mit eBPF): sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(which rustnet)" +"privileges.linux.setcap_legacy": "Fähigkeiten setzen (ältere Kernel, mit eBPF): sudo setcap 'cap_net_raw,cap_sys_admin=eip' $(which rustnet)" +"privileges.linux.setcap_minimal": "Fähigkeiten setzen (nur Paketerfassung, ohne eBPF): sudo setcap 'cap_net_raw=eip' $(which rustnet)" +"privileges.linux.docker_flags": "Bei Docker diese Flags hinzufügen:\n --cap-add=NET_RAW --cap-add=BPF --cap-add=PERFMON --net=host --pid=host" +# macOS privileges +"privileges.macos.bpf_access_missing": "Zugriff auf BPF-Geräte (/dev/bpf*)" +"privileges.macos.run_sudo": "Mit sudo ausführen: sudo rustnet" +"privileges.macos.chmod_bpf": "BPF-Geräteberechtigungen ändern (temporär):\n sudo chmod o+rw /dev/bpf*" +"privileges.macos.install_wireshark": "BPF-Berechtigungs-Helfer installieren (dauerhaft):\n brew install wireshark && sudo /usr/local/bin/install-bpf" +# FreeBSD privileges +"privileges.freebsd.bpf_access_missing": "Zugriff auf BPF-Geräte (/dev/bpf*)" +"privileges.freebsd.run_sudo": "Mit sudo ausführen: sudo rustnet" +"privileges.freebsd.add_to_bpf_group": "Benutzer zur bpf-Gruppe hinzufügen:\n sudo pw groupmod bpf -m $(whoami)\n Dann abmelden und wieder anmelden" +"privileges.freebsd.chmod_bpf": "BPF-Geräteberechtigungen ändern (temporär):\n sudo chmod o+rw /dev/bpf*" +# Windows privileges +"privileges.windows.admin_missing": "Administratorrechte" +"privileges.windows.run_admin": "Als Administrator ausführen: Rechtsklick auf Terminal und 'Als Administrator ausführen' wählen" +"privileges.windows.npcap_mode": "Bei Npcap: Sicherstellen, dass 'WinPcap API-kompatibler Modus' bei Installation aktiviert wurde" + +# CLI help text +"cli.about": "Plattformübergreifendes Netzwerk-Monitoring-Tool" +"cli.interface_help": "Zu überwachende Netzwerkschnittstelle" +"cli.interface_help_linux": "Zu überwachende Netzwerkschnittstelle (\"any\" für alle Schnittstellen)" +"cli.no_localhost_help": "Localhost-Verbindungen ausfiltern" +"cli.show_localhost_help": "Localhost-Verbindungen anzeigen (überschreibt Standardfilterung)" +"cli.refresh_interval_help": "UI-Aktualisierungsintervall in Millisekunden" +"cli.no_dpi_help": "Deep Packet Inspection deaktivieren" +"cli.log_level_help": "Log-Level setzen (wenn nicht angegeben, kein Logging)" +"cli.json_log_help": "JSON-Logging von Verbindungsereignissen in angegebene Datei aktivieren" +"cli.bpf_help": "BPF-Filterausdruck für Paketerfassung (z.B. \"tcp port 443\", \"dst port 80\")" +"cli.bpf_help_macos": "BPF-Filterausdruck für Paketerfassung (z.B. \"tcp port 443\"). Hinweis: BPF-Filter deaktiviert PKTAP (Prozessinfo fällt auf lsof zurück)" +"cli.resolve_dns_help": "Reverse-DNS-Auflösung für IP-Adressen aktivieren (zeigt Hostnamen statt IPs)" +"cli.show_ptr_lookups_help": "PTR-Lookup-Verbindungen in UI anzeigen (standardmäßig versteckt bei --resolve-dns)" +"cli.lang_help": "System-Locale überschreiben (z.B. en, es, de, fr, zh, ru)" +"cli.no_sandbox_help": "Landlock-Sandboxing deaktivieren" +"cli.sandbox_strict_help": "Vollständige Sandbox-Durchsetzung erfordern oder beenden" + +# Common UI labels diff --git a/assets/locales/en.yml b/assets/locales/en.yml new file mode 100644 index 0000000..a11c887 --- /dev/null +++ b/assets/locales/en.yml @@ -0,0 +1,223 @@ +# ============================================================================= +# RustNet Monitor - English Translations +# ============================================================================= +# This is the source of truth for all translatable strings. +# Variables use %{name} syntax, e.g., "Connections: %{count}" +# ============================================================================= + +# Application name +"app.name": "RustNet Monitor" + +# Tab names +"tabs.overview": "Overview" +"tabs.details": "Details" +"tabs.interfaces": "Interfaces" +"tabs.graph": "Graph" +"tabs.help": "Help" + +# Table headers +"table.headers.protocol": "Pro" +"table.headers.local_address": "Local Address" +"table.headers.remote_address": "Remote Address" +"table.headers.state": "State" +"table.headers.service": "Service" +"table.headers.application": "Application / Host" +"table.headers.bandwidth": "Down/Up" +"table.headers.process": "Process" +"table.title": "Active Connections" +"table.title_sorted": "Active Connections (Sort: %{column} %{direction})" + +# Sort column display names +"sort.time": "Time" +"sort.bandwidth": "Bandwidth Total" +"sort.process": "Process" +"sort.local_addr": "Local Addr" +"sort.remote_addr": "Remote Addr" +"sort.application": "Application" +"sort.service": "Service" +"sort.state": "State" +"sort.protocol": "Protocol" + +# Statistics panel +"stats.title": "Statistics" +"stats.interface": "Interface: %{name}" +"stats.link_layer": "Link Layer: %{type}" +"stats.link_layer_tunnel": "Link Layer: %{type} (Tunnel)" +"stats.process_detection": "Process Detection: %{method}" +"stats.tcp_connections": "TCP Connections: %{count}" +"stats.udp_connections": "UDP Connections: %{count}" +"stats.total_connections": "Total Connections: %{count}" +"stats.packets_processed": "Packets Processed: %{count}" +"stats.packets_dropped": "Packets Dropped: %{count}" + +# Network stats panel +"network_stats.title": "Network Stats" + +# Security panel +"security.title": "Security" +"security.landlock": "Landlock:" +"security.kernel_supported": "[kernel supported]" +"security.kernel_unsupported": "[kernel unsupported]" +"security.no_restrictions": "No restrictions active" +"security.cap_dropped": "CAP_NET_RAW dropped" +"security.fs_restricted": "FS restricted" +"security.net_blocked": "Net blocked" +"security.running_as_root": "Running as root (UID 0)" +"security.running_as_uid": "Running as UID %{uid}" +"security.running_as_admin": "Running as Administrator" +"security.running_as_standard": "Running as standard user" + +# Interface stats panel +"interface_stats.title": "Interface Stats (press 'i')" +"interface_stats.full_title": "Interface Statistics (Press 'i' to toggle)" +"interface_stats.no_stats": "No interface statistics available yet..." +"interface_stats.more": "... %{count} more (press 'i')" +"interface_stats.err_label": "Err: " +"interface_stats.drop_label": "Drop: " +"interface_stats.headers.interface": "Interface" +"interface_stats.headers.rx_rate": "RX Rate" +"interface_stats.headers.tx_rate": "TX Rate" +"interface_stats.headers.rx_packets": "RX Packets" +"interface_stats.headers.tx_packets": "TX Packets" +"interface_stats.headers.rx_err": "RX Err" +"interface_stats.headers.tx_err": "TX Err" +"interface_stats.headers.rx_drop": "RX Drop" +"interface_stats.headers.tx_drop": "TX Drop" +"interface_stats.headers.collisions": "Collisions" + +# Graph tab +"graph.traffic_title": "Traffic Over Time (60s)" +"graph.connections_title": "Connections" +"graph.connections_label": "%{count} connections" +"graph.collecting_data": "Collecting data..." +"graph.collecting_short": "Collecting..." +"graph.app_distribution": "Application Distribution" +"graph.no_connections": "No connections" +"graph.top_processes": "Top Processes" +"graph.no_active_processes": "No active processes" +"graph.network_health": "Network Health" +"graph.tcp_counters": "TCP Counters" +"graph.tcp_states": "TCP States" +"graph.no_tcp_connections": "No TCP connections" +"graph.axis.time": "Time" +"graph.axis.rate": "Rate" +"graph.legend.rx": "RX (incoming)" +"graph.legend.tx": "TX (outgoing)" + +# Connection details +"details.title": "Connection Details" +"details.info_title": "Connection Information" +"details.no_connections": "No connections available" +"details.traffic_title": "Traffic Statistics" +"details.bytes_sent": "Bytes Sent:" +"details.bytes_received": "Bytes Received:" +"details.packets_sent": "Packets Sent:" +"details.packets_received": "Packets Received:" +"details.current_rate_in": "Current Rate (In):" +"details.current_rate_out": "Current Rate (Out):" +"details.initial_rtt": "Initial RTT:" + +# Filter input +"filter.title_active": "Filter (↑↓/jk to navigate, Enter to confirm, Esc to cancel)" +"filter.title_applied": "Active Filter (Press Esc to clear)" + +# Status bar messages +"status.quit_confirm": "Press 'q' again to quit or any other key to cancel" +"status.clear_confirm": "Press 'x' again to clear all connections or any other key to cancel" +"status.default": "'h' help | Tab/Shift+Tab switch tabs | '/' filter | 'c' copy | Connections: %{count}" +"status.help_hint": "Press 'h' for help | 'c' to copy address | Connections: %{count}" +"status.filter_active": "'h' help | Tab/Shift+Tab switch tabs | Showing %{count} filtered connections (Esc to clear)" + +# Loading screen +"loading.title": "RustNet Monitor" +"loading.message": "Loading network connections..." +"loading.subtitle": "This may take a few seconds" + +# Help screen +"help.title": "RustNet Monitor" +"help.subtitle": "Network Connection Monitor" +"help.keys.quit": "Quit application (press twice to confirm)" +"help.keys.quit_immediate": "Quit immediately" +"help.keys.switch_tabs": "Switch between tabs" +"help.keys.navigate": "Navigate connections (wraps around)" +"help.keys.jump_first_last": "Jump to first/last connection (vim-style)" +"help.keys.page_navigate": "Navigate connections by page" +"help.keys.copy_address": "Copy remote address to clipboard" +"help.keys.toggle_ports": "Toggle between service names and port numbers" +"help.keys.toggle_hostnames": "Toggle between hostnames and IP addresses (when --resolve-dns)" +"help.keys.cycle_sort": "Cycle through sort columns (Bandwidth, Process, etc.)" +"help.keys.toggle_sort_dir": "Toggle sort direction (ascending/descending)" +"help.keys.view_details": "View connection details" +"help.keys.return_overview": "Return to overview" +"help.keys.toggle_help": "Toggle this help screen" +"help.keys.toggle_interfaces": "Toggle interface statistics view" +"help.keys.filter_mode": "Enter filter mode (navigate while typing!)" +"help.keys.clear_connections": "Clear all connections" +"help.sections.tabs": "Tabs:" +"help.sections.overview_desc": "Connection list with mini traffic graph" +"help.sections.details_desc": "Full details for selected connection" +"help.sections.interfaces_desc": "Network interface statistics" +"help.sections.graph_desc": "Traffic charts and protocol distribution" +"help.sections.help_desc": "This help screen" +"help.sections.colors": "Connection Colors:" +"help.colors.white": "White" +"help.colors.yellow": "Yellow" +"help.colors.red": "Red" +"help.sections.colors_white": "Active connection (< 75% of timeout)" +"help.sections.colors_yellow": "Stale connection (75-90% of timeout)" +"help.sections.colors_red": "Critical - will be removed soon (> 90% of timeout)" +"help.sections.filter_examples": "Filter Examples:" +"help.filter_examples.general": "Search for '%{term}' in all fields" +"help.filter_examples.port": "Filter ports containing '%{term}' (443, 8080, etc.)" +"help.filter_examples.src": "Filter by source IP prefix" +"help.filter_examples.dst": "Filter by destination" +"help.filter_examples.sni": "Filter by SNI hostname" +"help.filter_examples.process": "Filter by process name" + +# Error messages + +# Privilege error messages (platform-specific) +"privileges.insufficient": "Insufficient privileges for network packet capture." +"privileges.missing": "Missing" +"privileges.how_to_fix": "How to fix" +# Linux privileges +"privileges.linux.cap_net_raw_missing": "CAP_NET_RAW capability (required for packet capture)" +"privileges.linux.run_sudo": "Run with sudo: sudo rustnet" +"privileges.linux.setcap_modern": "Set capabilities (modern Linux 5.8+, with eBPF): sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(which rustnet)" +"privileges.linux.setcap_legacy": "Set capabilities (legacy/older kernels, with eBPF): sudo setcap 'cap_net_raw,cap_sys_admin=eip' $(which rustnet)" +"privileges.linux.setcap_minimal": "Set capabilities (packet capture only, no eBPF): sudo setcap 'cap_net_raw=eip' $(which rustnet)" +"privileges.linux.docker_flags": "If running in Docker, add these flags:\n --cap-add=NET_RAW --cap-add=BPF --cap-add=PERFMON --net=host --pid=host" +# macOS privileges +"privileges.macos.bpf_access_missing": "Access to BPF devices (/dev/bpf*)" +"privileges.macos.run_sudo": "Run with sudo: sudo rustnet" +"privileges.macos.chmod_bpf": "Change BPF device permissions (temporary):\n sudo chmod o+rw /dev/bpf*" +"privileges.macos.install_wireshark": "Install BPF permission helper (persistent):\n brew install wireshark && sudo /usr/local/bin/install-bpf" +# FreeBSD privileges +"privileges.freebsd.bpf_access_missing": "Access to BPF devices (/dev/bpf*)" +"privileges.freebsd.run_sudo": "Run with sudo: sudo rustnet" +"privileges.freebsd.add_to_bpf_group": "Add your user to the bpf group:\n sudo pw groupmod bpf -m $(whoami)\n Then logout and login again" +"privileges.freebsd.chmod_bpf": "Change BPF device permissions (temporary):\n sudo chmod o+rw /dev/bpf*" +# Windows privileges +"privileges.windows.admin_missing": "Administrator privileges" +"privileges.windows.run_admin": "Run as Administrator: Right-click the terminal and select 'Run as Administrator'" +"privileges.windows.npcap_mode": "If using Npcap: Ensure it was installed with 'WinPcap API-compatible Mode' enabled" + +# CLI help text +"cli.about": "Cross-platform network monitoring tool" +"cli.interface_help": "Network interface to monitor" +"cli.interface_help_linux": "Network interface to monitor (use \"any\" to capture all interfaces)" +"cli.no_localhost_help": "Filter out localhost connections" +"cli.show_localhost_help": "Show localhost connections (overrides default filtering)" +"cli.refresh_interval_help": "UI refresh interval in milliseconds" +"cli.no_dpi_help": "Disable deep packet inspection" +"cli.log_level_help": "Set the log level (if not provided, no logging will be enabled)" +"cli.json_log_help": "Enable JSON logging of connection events to specified file" +"cli.bpf_help": "BPF filter expression for packet capture (e.g., \"tcp port 443\", \"dst port 80\")" +"cli.bpf_help_macos": "BPF filter expression for packet capture (e.g., \"tcp port 443\"). Note: Using a BPF filter disables PKTAP (process info falls back to lsof)" +"cli.resolve_dns_help": "Enable reverse DNS resolution for IP addresses (shows hostnames instead of IPs)" +"cli.show_ptr_lookups_help": "Show PTR lookup connections in UI (hidden by default when --resolve-dns is enabled)" +"cli.lang_help": "Override system locale (e.g., en, es, de, fr, zh, ru)" +"cli.no_sandbox_help": "Disable Landlock sandboxing" +"cli.sandbox_strict_help": "Require full sandbox enforcement or exit" + +# Common UI labels diff --git a/assets/locales/es.yml b/assets/locales/es.yml new file mode 100644 index 0000000..20b7824 --- /dev/null +++ b/assets/locales/es.yml @@ -0,0 +1,220 @@ +# ============================================================================= +# RustNet Monitor - Spanish Translations (Traducciones al Español) +# ============================================================================= + +# Application name +"app.name": "RustNet Monitor" + +# Tab names +"tabs.overview": "Vista General" +"tabs.details": "Detalles" +"tabs.interfaces": "Interfaces" +"tabs.graph": "Gráfico" +"tabs.help": "Ayuda" + +# Table headers +"table.headers.protocol": "Pro" +"table.headers.local_address": "Dirección Local" +"table.headers.remote_address": "Dirección Remota" +"table.headers.state": "Estado" +"table.headers.service": "Servicio" +"table.headers.application": "Aplicación / Host" +"table.headers.bandwidth": "↓/↑" +"table.headers.process": "Proceso" +"table.title": "Conexiones Activas" +"table.title_sorted": "Conexiones Activas (Orden: %{column} %{direction})" + +# Sort column display names +"sort.time": "Tiempo" +"sort.bandwidth": "Ancho de Banda Total" +"sort.process": "Proceso" +"sort.local_addr": "Dir. Local" +"sort.remote_addr": "Dir. Remota" +"sort.application": "Aplicación" +"sort.service": "Servicio" +"sort.state": "Estado" +"sort.protocol": "Protocolo" + +# Statistics panel +"stats.title": "Estadísticas" +"stats.interface": "Interfaz: %{name}" +"stats.link_layer": "Capa de Enlace: %{type}" +"stats.link_layer_tunnel": "Capa de Enlace: %{type} (Túnel)" +"stats.process_detection": "Detección de Procesos: %{method}" +"stats.tcp_connections": "Conexiones TCP: %{count}" +"stats.udp_connections": "Conexiones UDP: %{count}" +"stats.total_connections": "Total de Conexiones: %{count}" +"stats.packets_processed": "Paquetes Procesados: %{count}" +"stats.packets_dropped": "Paquetes Descartados: %{count}" + +# Network stats panel +"network_stats.title": "Estadísticas de Red" + +# Security panel +"security.title": "Seguridad" +"security.landlock": "Landlock:" +"security.kernel_supported": "[kernel soportado]" +"security.kernel_unsupported": "[kernel no soportado]" +"security.no_restrictions": "Sin restricciones activas" +"security.cap_dropped": "CAP_NET_RAW eliminado" +"security.fs_restricted": "FS restringido" +"security.net_blocked": "Red bloqueada" +"security.running_as_root": "Ejecutando como root (UID 0)" +"security.running_as_uid": "Ejecutando como UID %{uid}" +"security.running_as_admin": "Ejecutando como Administrador" +"security.running_as_standard": "Ejecutando como usuario estándar" + +# Interface stats panel +"interface_stats.title": "Stats de Interfaz (presiona 'i')" +"interface_stats.full_title": "Estadísticas de Interfaz (Presiona 'i' para alternar)" +"interface_stats.no_stats": "Estadísticas de interfaz aún no disponibles..." +"interface_stats.more": "... %{count} más (presiona 'i')" +"interface_stats.err_label": "Err: " +"interface_stats.drop_label": "Drop: " +"interface_stats.headers.interface": "Interfaz" +"interface_stats.headers.rx_rate": "Tasa RX" +"interface_stats.headers.tx_rate": "Tasa TX" +"interface_stats.headers.rx_packets": "Paquetes RX" +"interface_stats.headers.tx_packets": "Paquetes TX" +"interface_stats.headers.rx_err": "Err RX" +"interface_stats.headers.tx_err": "Err TX" +"interface_stats.headers.rx_drop": "Drop RX" +"interface_stats.headers.tx_drop": "Drop TX" +"interface_stats.headers.collisions": "Colisiones" + +# Graph tab +"graph.traffic_title": "Tráfico en el Tiempo (60s)" +"graph.connections_title": "Conexiones" +"graph.connections_label": "%{count} conexiones" +"graph.collecting_data": "Recopilando datos..." +"graph.collecting_short": "Recopilando..." +"graph.app_distribution": "Distribución de Aplicaciones" +"graph.no_connections": "Sin conexiones" +"graph.top_processes": "Procesos Principales" +"graph.no_active_processes": "Sin procesos activos" +"graph.network_health": "Estado de la Red" +"graph.tcp_counters": "Contadores TCP" +"graph.tcp_states": "Estados TCP" +"graph.no_tcp_connections": "Sin conexiones TCP" +"graph.axis.time": "Tiempo" +"graph.axis.rate": "Tasa" +"graph.legend.rx": "RX (entrante)" +"graph.legend.tx": "TX (saliente)" + +# Connection details +"details.title": "Detalles de Conexión" +"details.info_title": "Información de Conexión" +"details.no_connections": "No hay conexiones disponibles" +"details.traffic_title": "Estadísticas de Tráfico" +"details.bytes_sent": "Bytes Enviados:" +"details.bytes_received": "Bytes Recibidos:" +"details.packets_sent": "Paquetes Enviados:" +"details.packets_received": "Paquetes Recibidos:" +"details.current_rate_in": "Tasa Actual (Entrada):" +"details.current_rate_out": "Tasa Actual (Salida):" +"details.initial_rtt": "RTT Inicial:" + +# Filter input +"filter.title_active": "Filtro (↑↓/jk para navegar, Enter para confirmar, Esc para cancelar)" +"filter.title_applied": "Filtro Activo (Presiona Esc para limpiar)" + +# Status bar messages +"status.quit_confirm": "Presiona 'q' de nuevo para salir o cualquier otra tecla para cancelar" +"status.clear_confirm": "Presiona 'x' de nuevo para borrar todas las conexiones o cualquier otra tecla para cancelar" +"status.default": "'h' ayuda | Tab/Shift+Tab cambiar pestañas | '/' filtrar | 'c' copiar | Conexiones: %{count}" +"status.help_hint": "Presiona 'h' para ayuda | 'c' para copiar dirección | Conexiones: %{count}" +"status.filter_active": "'h' ayuda | Tab/Shift+Tab cambiar pestañas | Mostrando %{count} conexiones filtradas (Esc para limpiar)" + +# Loading screen +"loading.title": "RustNet Monitor" +"loading.message": "Cargando conexiones de red..." +"loading.subtitle": "Esto puede tomar unos segundos" + +# Help screen +"help.title": "RustNet Monitor" +"help.subtitle": "Monitor de Conexiones de Red" +"help.keys.quit": "Salir de la aplicación (presiona dos veces para confirmar)" +"help.keys.quit_immediate": "Salir inmediatamente" +"help.keys.switch_tabs": "Cambiar entre pestañas" +"help.keys.navigate": "Navegar conexiones (con retorno)" +"help.keys.jump_first_last": "Ir a la primera/última conexión (estilo vim)" +"help.keys.page_navigate": "Navegar conexiones por página" +"help.keys.copy_address": "Copiar dirección remota al portapapeles" +"help.keys.toggle_ports": "Alternar entre nombres de servicio y números de puerto" +"help.keys.toggle_hostnames": "Alternar entre hostnames e IPs (con --resolve-dns)" +"help.keys.cycle_sort": "Ciclar columnas de orden (Ancho de Banda, Proceso, etc.)" +"help.keys.toggle_sort_dir": "Alternar dirección de orden (ascendente/descendente)" +"help.keys.view_details": "Ver detalles de conexión" +"help.keys.return_overview": "Volver a vista general" +"help.keys.toggle_help": "Alternar esta pantalla de ayuda" +"help.keys.toggle_interfaces": "Alternar vista de estadísticas de interfaz" +"help.keys.filter_mode": "Entrar en modo filtro (¡navega mientras escribes!)" +"help.keys.clear_connections": "Limpiar todas las conexiones" +"help.sections.tabs": "Pestañas:" +"help.sections.overview_desc": "Lista de conexiones con mini gráfico de tráfico" +"help.sections.details_desc": "Detalles completos de la conexión seleccionada" +"help.sections.interfaces_desc": "Estadísticas de interfaces de red" +"help.sections.graph_desc": "Gráficos de tráfico y distribución de protocolos" +"help.sections.help_desc": "Esta pantalla de ayuda" +"help.sections.colors": "Colores de Conexión:" +"help.colors.white": "Blanco" +"help.colors.yellow": "Amarillo" +"help.colors.red": "Rojo" +"help.sections.colors_white": "Conexión activa (< 75% del timeout)" +"help.sections.colors_yellow": "Conexión obsoleta (75-90% del timeout)" +"help.sections.colors_red": "Crítico - será eliminada pronto (> 90% del timeout)" +"help.sections.filter_examples": "Ejemplos de Filtro:" +"help.filter_examples.general": "Buscar '%{term}' en todos los campos" +"help.filter_examples.port": "Filtrar puertos que contengan '%{term}' (443, 8080, etc.)" +"help.filter_examples.src": "Filtrar por prefijo de IP origen" +"help.filter_examples.dst": "Filtrar por destino" +"help.filter_examples.sni": "Filtrar por hostname SNI" +"help.filter_examples.process": "Filtrar por nombre de proceso" + +# Error messages + +# Privilege error messages (platform-specific) +"privileges.insufficient": "Privilegios insuficientes para captura de paquetes de red." +"privileges.missing": "Faltante" +"privileges.how_to_fix": "Cómo solucionar" +# Linux privileges +"privileges.linux.cap_net_raw_missing": "Capacidad CAP_NET_RAW (requerida para captura de paquetes)" +"privileges.linux.run_sudo": "Ejecutar con sudo: sudo rustnet" +"privileges.linux.setcap_modern": "Establecer capacidades (Linux moderno 5.8+, con eBPF): sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(which rustnet)" +"privileges.linux.setcap_legacy": "Establecer capacidades (kernels antiguos, con eBPF): sudo setcap 'cap_net_raw,cap_sys_admin=eip' $(which rustnet)" +"privileges.linux.setcap_minimal": "Establecer capacidades (solo captura, sin eBPF): sudo setcap 'cap_net_raw=eip' $(which rustnet)" +"privileges.linux.docker_flags": "Si ejecutas en Docker, agrega estos flags:\n --cap-add=NET_RAW --cap-add=BPF --cap-add=PERFMON --net=host --pid=host" +# macOS privileges +"privileges.macos.bpf_access_missing": "Acceso a dispositivos BPF (/dev/bpf*)" +"privileges.macos.run_sudo": "Ejecutar con sudo: sudo rustnet" +"privileges.macos.chmod_bpf": "Cambiar permisos de dispositivo BPF (temporal):\n sudo chmod o+rw /dev/bpf*" +"privileges.macos.install_wireshark": "Instalar ayudante de permisos BPF (persistente):\n brew install wireshark && sudo /usr/local/bin/install-bpf" +# FreeBSD privileges +"privileges.freebsd.bpf_access_missing": "Acceso a dispositivos BPF (/dev/bpf*)" +"privileges.freebsd.run_sudo": "Ejecutar con sudo: sudo rustnet" +"privileges.freebsd.add_to_bpf_group": "Agregar tu usuario al grupo bpf:\n sudo pw groupmod bpf -m $(whoami)\n Luego cierra sesión e inicia de nuevo" +"privileges.freebsd.chmod_bpf": "Cambiar permisos de dispositivo BPF (temporal):\n sudo chmod o+rw /dev/bpf*" +# Windows privileges +"privileges.windows.admin_missing": "Privilegios de Administrador" +"privileges.windows.run_admin": "Ejecutar como Administrador: Clic derecho en la terminal y selecciona 'Ejecutar como Administrador'" +"privileges.windows.npcap_mode": "Si usas Npcap: Asegúrate de que fue instalado con 'Modo compatible con API WinPcap' habilitado" + +# CLI help text +"cli.about": "Herramienta de monitoreo de red multiplataforma" +"cli.interface_help": "Interfaz de red a monitorear" +"cli.interface_help_linux": "Interfaz de red a monitorear (usa \"any\" para capturar todas las interfaces)" +"cli.no_localhost_help": "Filtrar conexiones localhost" +"cli.show_localhost_help": "Mostrar conexiones localhost (sobrescribe el filtrado por defecto)" +"cli.refresh_interval_help": "Intervalo de actualización de UI en milisegundos" +"cli.no_dpi_help": "Deshabilitar inspección profunda de paquetes" +"cli.log_level_help": "Establecer nivel de log (si no se proporciona, no se habilitará logging)" +"cli.json_log_help": "Habilitar logging JSON de eventos de conexión a archivo especificado" +"cli.bpf_help": "Expresión de filtro BPF para captura de paquetes (ej. \"tcp port 443\", \"dst port 80\")" +"cli.bpf_help_macos": "Expresión de filtro BPF para captura de paquetes (ej. \"tcp port 443\"). Nota: Usar un filtro BPF deshabilita PKTAP (info de proceso recurre a lsof)" +"cli.resolve_dns_help": "Habilitar resolución DNS inversa para direcciones IP (muestra hostnames en lugar de IPs)" +"cli.show_ptr_lookups_help": "Mostrar conexiones de búsqueda PTR en UI (ocultas por defecto con --resolve-dns)" +"cli.lang_help": "Sobrescribir locale del sistema (ej. en, es, de, fr, zh, ru)" +"cli.no_sandbox_help": "Deshabilitar sandboxing Landlock" +"cli.sandbox_strict_help": "Requerir aplicación completa de sandbox o salir" + +# Common UI labels diff --git a/assets/locales/fr.yml b/assets/locales/fr.yml new file mode 100644 index 0000000..026ea32 --- /dev/null +++ b/assets/locales/fr.yml @@ -0,0 +1,221 @@ +# ============================================================================= +# RustNet Monitor - French Translations (Traductions Françaises) +# TODO: Translate strings from English to French +# ============================================================================= + +# Application name +"app.name": "RustNet Monitor" + +# Tab names +"tabs.overview": "Aperçu" +"tabs.details": "Détails" +"tabs.interfaces": "Interfaces" +"tabs.graph": "Graphique" +"tabs.help": "Aide" + +# Table headers +"table.headers.protocol": "Pro" +"table.headers.local_address": "Adresse Locale" +"table.headers.remote_address": "Adresse Distante" +"table.headers.state": "État" +"table.headers.service": "Service" +"table.headers.application": "Application / Hôte" +"table.headers.bandwidth": "↓/↑" +"table.headers.process": "Processus" +"table.title": "Connexions Actives" +"table.title_sorted": "Connexions Actives (Tri: %{column} %{direction})" + +# Sort column display names +"sort.time": "Temps" +"sort.bandwidth": "Bande Passante Totale" +"sort.process": "Processus" +"sort.local_addr": "Adr. Locale" +"sort.remote_addr": "Adr. Distante" +"sort.application": "Application" +"sort.service": "Service" +"sort.state": "État" +"sort.protocol": "Protocole" + +# Statistics panel +"stats.title": "Statistiques" +"stats.interface": "Interface: %{name}" +"stats.link_layer": "Couche Liaison: %{type}" +"stats.link_layer_tunnel": "Couche Liaison: %{type} (Tunnel)" +"stats.process_detection": "Détection Processus: %{method}" +"stats.tcp_connections": "Connexions TCP: %{count}" +"stats.udp_connections": "Connexions UDP: %{count}" +"stats.total_connections": "Total Connexions: %{count}" +"stats.packets_processed": "Paquets Traités: %{count}" +"stats.packets_dropped": "Paquets Perdus: %{count}" + +# Network stats panel +"network_stats.title": "Stats Réseau" + +# Security panel +"security.title": "Sécurité" +"security.landlock": "Landlock:" +"security.kernel_supported": "[kernel supporté]" +"security.kernel_unsupported": "[kernel non supporté]" +"security.no_restrictions": "Aucune restriction active" +"security.cap_dropped": "CAP_NET_RAW supprimé" +"security.fs_restricted": "FS restreint" +"security.net_blocked": "Réseau bloqué" +"security.running_as_root": "Exécution en root (UID 0)" +"security.running_as_uid": "Exécution en UID %{uid}" +"security.running_as_admin": "Exécution en Administrateur" +"security.running_as_standard": "Exécution en utilisateur standard" + +# Interface stats panel +"interface_stats.title": "Stats Interface (appuyez 'i')" +"interface_stats.full_title": "Statistiques Interface (Appuyez 'i' pour basculer)" +"interface_stats.no_stats": "Statistiques interface pas encore disponibles..." +"interface_stats.more": "... %{count} de plus (appuyez 'i')" +"interface_stats.err_label": "Err: " +"interface_stats.drop_label": "Drop: " +"interface_stats.headers.interface": "Interface" +"interface_stats.headers.rx_rate": "Débit RX" +"interface_stats.headers.tx_rate": "Débit TX" +"interface_stats.headers.rx_packets": "Paquets RX" +"interface_stats.headers.tx_packets": "Paquets TX" +"interface_stats.headers.rx_err": "Err RX" +"interface_stats.headers.tx_err": "Err TX" +"interface_stats.headers.rx_drop": "Drop RX" +"interface_stats.headers.tx_drop": "Drop TX" +"interface_stats.headers.collisions": "Collisions" + +# Graph tab +"graph.traffic_title": "Trafic sur le Temps (60s)" +"graph.connections_title": "Connexions" +"graph.connections_label": "%{count} connexions" +"graph.collecting_data": "Collecte des données..." +"graph.collecting_short": "Collecte..." +"graph.app_distribution": "Distribution Applications" +"graph.no_connections": "Aucune connexion" +"graph.top_processes": "Top Processus" +"graph.no_active_processes": "Aucun processus actif" +"graph.network_health": "Santé Réseau" +"graph.tcp_counters": "Compteurs TCP" +"graph.tcp_states": "États TCP" +"graph.no_tcp_connections": "Aucune connexion TCP" +"graph.axis.time": "Temps" +"graph.axis.rate": "Débit" +"graph.legend.rx": "RX (entrant)" +"graph.legend.tx": "TX (sortant)" + +# Connection details +"details.title": "Détails Connexion" +"details.info_title": "Information Connexion" +"details.no_connections": "Aucune connexion disponible" +"details.traffic_title": "Statistiques Trafic" +"details.bytes_sent": "Octets Envoyés:" +"details.bytes_received": "Octets Reçus:" +"details.packets_sent": "Paquets Envoyés:" +"details.packets_received": "Paquets Reçus:" +"details.current_rate_in": "Débit Actuel (Entrant):" +"details.current_rate_out": "Débit Actuel (Sortant):" +"details.initial_rtt": "RTT Initial:" + +# Filter input +"filter.title_active": "Filtre (↑↓/jk naviguer, Entrée confirmer, Échap annuler)" +"filter.title_applied": "Filtre Actif (Appuyez Échap pour effacer)" + +# Status bar messages +"status.quit_confirm": "Appuyez 'q' à nouveau pour quitter ou autre touche pour annuler" +"status.clear_confirm": "Appuyez 'x' à nouveau pour effacer toutes les connexions ou autre touche pour annuler" +"status.default": "'h' aide | Tab/Shift+Tab changer onglets | '/' filtrer | 'c' copier | Connexions: %{count}" +"status.help_hint": "Appuyez 'h' pour aide | 'c' pour copier adresse | Connexions: %{count}" +"status.filter_active": "'h' aide | Tab/Shift+Tab changer onglets | Affichage %{count} connexions filtrées (Échap pour effacer)" + +# Loading screen +"loading.title": "RustNet Monitor" +"loading.message": "Chargement des connexions réseau..." +"loading.subtitle": "Cela peut prendre quelques secondes" + +# Help screen +"help.title": "RustNet Monitor" +"help.subtitle": "Moniteur de Connexions Réseau" +"help.keys.quit": "Quitter application (appuyer deux fois pour confirmer)" +"help.keys.quit_immediate": "Quitter immédiatement" +"help.keys.switch_tabs": "Changer entre onglets" +"help.keys.navigate": "Naviguer connexions (avec retour)" +"help.keys.jump_first_last": "Aller à première/dernière connexion (style vim)" +"help.keys.page_navigate": "Naviguer connexions par page" +"help.keys.copy_address": "Copier adresse distante dans presse-papiers" +"help.keys.toggle_ports": "Basculer entre noms de service et numéros de port" +"help.keys.toggle_hostnames": "Basculer entre noms d'hôte et IPs (avec --resolve-dns)" +"help.keys.cycle_sort": "Parcourir colonnes de tri (Bande passante, Processus, etc.)" +"help.keys.toggle_sort_dir": "Basculer direction tri (ascendant/descendant)" +"help.keys.view_details": "Voir détails connexion" +"help.keys.return_overview": "Retour à l'aperçu" +"help.keys.toggle_help": "Basculer cet écran d'aide" +"help.keys.toggle_interfaces": "Basculer vue statistiques interface" +"help.keys.filter_mode": "Entrer mode filtre (naviguer en tapant!)" +"help.keys.clear_connections": "Effacer toutes les connexions" +"help.sections.tabs": "Onglets:" +"help.sections.overview_desc": "Liste connexions avec mini graphique trafic" +"help.sections.details_desc": "Détails complets connexion sélectionnée" +"help.sections.interfaces_desc": "Statistiques interfaces réseau" +"help.sections.graph_desc": "Graphiques trafic et distribution protocoles" +"help.sections.help_desc": "Cet écran d'aide" +"help.sections.colors": "Couleurs Connexion:" +"help.colors.white": "Blanc" +"help.colors.yellow": "Jaune" +"help.colors.red": "Rouge" +"help.sections.colors_white": "Connexion active (< 75% du timeout)" +"help.sections.colors_yellow": "Connexion obsolète (75-90% du timeout)" +"help.sections.colors_red": "Critique - sera supprimée bientôt (> 90% du timeout)" +"help.sections.filter_examples": "Exemples Filtre:" +"help.filter_examples.general": "Chercher '%{term}' dans tous les champs" +"help.filter_examples.port": "Filtrer ports contenant '%{term}' (443, 8080, etc.)" +"help.filter_examples.src": "Filtrer par préfixe IP source" +"help.filter_examples.dst": "Filtrer par destination" +"help.filter_examples.sni": "Filtrer par nom d'hôte SNI" +"help.filter_examples.process": "Filtrer par nom processus" + +# Error messages + +# Privilege error messages (platform-specific) +"privileges.insufficient": "Privilèges insuffisants pour capture paquets réseau." +"privileges.missing": "Manquant" +"privileges.how_to_fix": "Comment résoudre" +# Linux privileges +"privileges.linux.cap_net_raw_missing": "Capacité CAP_NET_RAW (requise pour capture paquets)" +"privileges.linux.run_sudo": "Exécuter avec sudo: sudo rustnet" +"privileges.linux.setcap_modern": "Définir capacités (Linux moderne 5.8+, avec eBPF): sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(which rustnet)" +"privileges.linux.setcap_legacy": "Définir capacités (anciens kernels, avec eBPF): sudo setcap 'cap_net_raw,cap_sys_admin=eip' $(which rustnet)" +"privileges.linux.setcap_minimal": "Définir capacités (capture seule, sans eBPF): sudo setcap 'cap_net_raw=eip' $(which rustnet)" +"privileges.linux.docker_flags": "Si exécution dans Docker, ajouter ces flags:\n --cap-add=NET_RAW --cap-add=BPF --cap-add=PERFMON --net=host --pid=host" +# macOS privileges +"privileges.macos.bpf_access_missing": "Accès aux périphériques BPF (/dev/bpf*)" +"privileges.macos.run_sudo": "Exécuter avec sudo: sudo rustnet" +"privileges.macos.chmod_bpf": "Modifier permissions périphérique BPF (temporaire):\n sudo chmod o+rw /dev/bpf*" +"privileges.macos.install_wireshark": "Installer aide permissions BPF (persistant):\n brew install wireshark && sudo /usr/local/bin/install-bpf" +# FreeBSD privileges +"privileges.freebsd.bpf_access_missing": "Accès aux périphériques BPF (/dev/bpf*)" +"privileges.freebsd.run_sudo": "Exécuter avec sudo: sudo rustnet" +"privileges.freebsd.add_to_bpf_group": "Ajouter utilisateur au groupe bpf:\n sudo pw groupmod bpf -m $(whoami)\n Puis déconnexion et reconnexion" +"privileges.freebsd.chmod_bpf": "Modifier permissions périphérique BPF (temporaire):\n sudo chmod o+rw /dev/bpf*" +# Windows privileges +"privileges.windows.admin_missing": "Privilèges Administrateur" +"privileges.windows.run_admin": "Exécuter en Administrateur: Clic droit sur terminal et sélectionner 'Exécuter en tant qu'Administrateur'" +"privileges.windows.npcap_mode": "Si utilisant Npcap: S'assurer qu'il a été installé avec 'Mode compatible API WinPcap' activé" + +# CLI help text +"cli.about": "Outil surveillance réseau multiplateforme" +"cli.interface_help": "Interface réseau à surveiller" +"cli.interface_help_linux": "Interface réseau à surveiller (utiliser \"any\" pour toutes les interfaces)" +"cli.no_localhost_help": "Filtrer connexions localhost" +"cli.show_localhost_help": "Afficher connexions localhost (remplace filtrage par défaut)" +"cli.refresh_interval_help": "Intervalle actualisation UI en millisecondes" +"cli.no_dpi_help": "Désactiver inspection approfondie paquets" +"cli.log_level_help": "Définir niveau log (si non fourni, pas de logging)" +"cli.json_log_help": "Activer logging JSON événements connexion vers fichier spécifié" +"cli.bpf_help": "Expression filtre BPF pour capture paquets (ex. \"tcp port 443\", \"dst port 80\")" +"cli.bpf_help_macos": "Expression filtre BPF pour capture paquets (ex. \"tcp port 443\"). Note: Filtre BPF désactive PKTAP (info processus revient à lsof)" +"cli.resolve_dns_help": "Activer résolution DNS inverse pour adresses IP (affiche noms d'hôte au lieu IPs)" +"cli.show_ptr_lookups_help": "Afficher connexions recherche PTR dans UI (cachées par défaut avec --resolve-dns)" +"cli.lang_help": "Remplacer locale système (ex. en, es, de, fr, zh, ru)" +"cli.no_sandbox_help": "Désactiver sandboxing Landlock" +"cli.sandbox_strict_help": "Exiger application complète sandbox ou quitter" + +# Common UI labels diff --git a/assets/locales/ru.yml b/assets/locales/ru.yml new file mode 100644 index 0000000..86ad73f --- /dev/null +++ b/assets/locales/ru.yml @@ -0,0 +1,221 @@ +# ============================================================================= +# RustNet Monitor - Russian Translations (Русские переводы) +# TODO: Translate strings from English to Russian +# ============================================================================= + +# Application name +"app.name": "RustNet Monitor" + +# Tab names +"tabs.overview": "Обзор" +"tabs.details": "Детали" +"tabs.interfaces": "Интерфейсы" +"tabs.graph": "График" +"tabs.help": "Справка" + +# Table headers +"table.headers.protocol": "Прот" +"table.headers.local_address": "Локальный адрес" +"table.headers.remote_address": "Удалённый адрес" +"table.headers.state": "Состояние" +"table.headers.service": "Сервис" +"table.headers.application": "Приложение / Хост" +"table.headers.bandwidth": "↓/↑" +"table.headers.process": "Процесс" +"table.title": "Активные подключения" +"table.title_sorted": "Активные подключения (Сортировка: %{column} %{direction})" + +# Sort column display names +"sort.time": "Время" +"sort.bandwidth": "Общая пропускная способность" +"sort.process": "Процесс" +"sort.local_addr": "Лок. адрес" +"sort.remote_addr": "Уд. адрес" +"sort.application": "Приложение" +"sort.service": "Сервис" +"sort.state": "Состояние" +"sort.protocol": "Протокол" + +# Statistics panel +"stats.title": "Статистика" +"stats.interface": "Интерфейс: %{name}" +"stats.link_layer": "Канальный уровень: %{type}" +"stats.link_layer_tunnel": "Канальный уровень: %{type} (Туннель)" +"stats.process_detection": "Обнаружение процессов: %{method}" +"stats.tcp_connections": "TCP подключений: %{count}" +"stats.udp_connections": "UDP подключений: %{count}" +"stats.total_connections": "Всего подключений: %{count}" +"stats.packets_processed": "Обработано пакетов: %{count}" +"stats.packets_dropped": "Отброшено пакетов: %{count}" + +# Network stats panel +"network_stats.title": "Сетевая статистика" + +# Security panel +"security.title": "Безопасность" +"security.landlock": "Landlock:" +"security.kernel_supported": "[ядро поддерживает]" +"security.kernel_unsupported": "[ядро не поддерживает]" +"security.no_restrictions": "Нет активных ограничений" +"security.cap_dropped": "CAP_NET_RAW удалён" +"security.fs_restricted": "ФС ограничена" +"security.net_blocked": "Сеть заблокирована" +"security.running_as_root": "Запущено от root (UID 0)" +"security.running_as_uid": "Запущено от UID %{uid}" +"security.running_as_admin": "Запущено от Администратора" +"security.running_as_standard": "Запущено от обычного пользователя" + +# Interface stats panel +"interface_stats.title": "Статистика интерфейса (нажмите 'i')" +"interface_stats.full_title": "Статистика интерфейса (Нажмите 'i' для переключения)" +"interface_stats.no_stats": "Статистика интерфейса пока недоступна..." +"interface_stats.more": "... ещё %{count} (нажмите 'i')" +"interface_stats.err_label": "Ош: " +"interface_stats.drop_label": "Потери: " +"interface_stats.headers.interface": "Интерфейс" +"interface_stats.headers.rx_rate": "Скорость RX" +"interface_stats.headers.tx_rate": "Скорость TX" +"interface_stats.headers.rx_packets": "Пакеты RX" +"interface_stats.headers.tx_packets": "Пакеты TX" +"interface_stats.headers.rx_err": "Ош RX" +"interface_stats.headers.tx_err": "Ош TX" +"interface_stats.headers.rx_drop": "Потери RX" +"interface_stats.headers.tx_drop": "Потери TX" +"interface_stats.headers.collisions": "Коллизии" + +# Graph tab +"graph.traffic_title": "Трафик за время (60с)" +"graph.connections_title": "Подключения" +"graph.connections_label": "%{count} подключений" +"graph.collecting_data": "Сбор данных..." +"graph.collecting_short": "Сбор..." +"graph.app_distribution": "Распределение приложений" +"graph.no_connections": "Нет подключений" +"graph.top_processes": "Топ процессов" +"graph.no_active_processes": "Нет активных процессов" +"graph.network_health": "Состояние сети" +"graph.tcp_counters": "Счётчики TCP" +"graph.tcp_states": "Состояния TCP" +"graph.no_tcp_connections": "Нет TCP подключений" +"graph.axis.time": "Время" +"graph.axis.rate": "Скорость" +"graph.legend.rx": "RX (входящий)" +"graph.legend.tx": "TX (исходящий)" + +# Connection details +"details.title": "Детали подключения" +"details.info_title": "Информация о подключении" +"details.no_connections": "Нет доступных подключений" +"details.traffic_title": "Статистика трафика" +"details.bytes_sent": "Отправлено байт:" +"details.bytes_received": "Получено байт:" +"details.packets_sent": "Отправлено пакетов:" +"details.packets_received": "Получено пакетов:" +"details.current_rate_in": "Текущая скорость (вход.):" +"details.current_rate_out": "Текущая скорость (исход.):" +"details.initial_rtt": "Начальный RTT:" + +# Filter input +"filter.title_active": "Фильтр (↑↓/jk навигация, Enter подтвердить, Esc отмена)" +"filter.title_applied": "Активный фильтр (Нажмите Esc для очистки)" + +# Status bar messages +"status.quit_confirm": "Нажмите 'q' ещё раз для выхода или другую клавишу для отмены" +"status.clear_confirm": "Нажмите 'x' ещё раз для очистки всех подключений или другую клавишу для отмены" +"status.default": "'h' справка | Tab/Shift+Tab переключение вкладок | '/' фильтр | 'c' копировать | Подключений: %{count}" +"status.help_hint": "Нажмите 'h' для справки | 'c' для копирования адреса | Подключений: %{count}" +"status.filter_active": "'h' справка | Tab/Shift+Tab переключение вкладок | Показано %{count} отфильтрованных подключений (Esc для очистки)" + +# Loading screen +"loading.title": "RustNet Monitor" +"loading.message": "Загрузка сетевых подключений..." +"loading.subtitle": "Это может занять несколько секунд" + +# Help screen +"help.title": "RustNet Monitor" +"help.subtitle": "Монитор сетевых подключений" +"help.keys.quit": "Выйти из приложения (нажмите дважды для подтверждения)" +"help.keys.quit_immediate": "Немедленный выход" +"help.keys.switch_tabs": "Переключение между вкладками" +"help.keys.navigate": "Навигация по подключениям (с циклом)" +"help.keys.jump_first_last": "Перейти к первому/последнему подключению (стиль vim)" +"help.keys.page_navigate": "Постраничная навигация по подключениям" +"help.keys.copy_address": "Копировать удалённый адрес в буфер обмена" +"help.keys.toggle_ports": "Переключение между именами сервисов и номерами портов" +"help.keys.toggle_hostnames": "Переключение между именами хостов и IP (с --resolve-dns)" +"help.keys.cycle_sort": "Цикл по столбцам сортировки (Пропускная способность, Процесс и т.д.)" +"help.keys.toggle_sort_dir": "Переключение направления сортировки (по возрастанию/убыванию)" +"help.keys.view_details": "Просмотр деталей подключения" +"help.keys.return_overview": "Вернуться к обзору" +"help.keys.toggle_help": "Переключить этот экран справки" +"help.keys.toggle_interfaces": "Переключить вид статистики интерфейса" +"help.keys.filter_mode": "Войти в режим фильтра (навигация при вводе!)" +"help.keys.clear_connections": "Очистить все подключения" +"help.sections.tabs": "Вкладки:" +"help.sections.overview_desc": "Список подключений с мини-графиком трафика" +"help.sections.details_desc": "Полные детали выбранного подключения" +"help.sections.interfaces_desc": "Статистика сетевых интерфейсов" +"help.sections.graph_desc": "Графики трафика и распределение протоколов" +"help.sections.help_desc": "Этот экран справки" +"help.sections.colors": "Цвета подключений:" +"help.colors.white": "Белый" +"help.colors.yellow": "Жёлтый" +"help.colors.red": "Красный" +"help.sections.colors_white": "Активное подключение (< 75% таймаута)" +"help.sections.colors_yellow": "Устаревшее подключение (75-90% таймаута)" +"help.sections.colors_red": "Критично - будет удалено скоро (> 90% таймаута)" +"help.sections.filter_examples": "Примеры фильтров:" +"help.filter_examples.general": "Поиск '%{term}' во всех полях" +"help.filter_examples.port": "Фильтр портов, содержащих '%{term}' (443, 8080 и т.д.)" +"help.filter_examples.src": "Фильтр по префиксу исходного IP" +"help.filter_examples.dst": "Фильтр по назначению" +"help.filter_examples.sni": "Фильтр по имени хоста SNI" +"help.filter_examples.process": "Фильтр по имени процесса" + +# Error messages + +# Privilege error messages (platform-specific) +"privileges.insufficient": "Недостаточно привилегий для захвата сетевых пакетов." +"privileges.missing": "Отсутствует" +"privileges.how_to_fix": "Как исправить" +# Linux privileges +"privileges.linux.cap_net_raw_missing": "Возможность CAP_NET_RAW (требуется для захвата пакетов)" +"privileges.linux.run_sudo": "Запустить с sudo: sudo rustnet" +"privileges.linux.setcap_modern": "Установить возможности (современный Linux 5.8+, с eBPF): sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(which rustnet)" +"privileges.linux.setcap_legacy": "Установить возможности (старые ядра, с eBPF): sudo setcap 'cap_net_raw,cap_sys_admin=eip' $(which rustnet)" +"privileges.linux.setcap_minimal": "Установить возможности (только захват, без eBPF): sudo setcap 'cap_net_raw=eip' $(which rustnet)" +"privileges.linux.docker_flags": "При запуске в Docker добавьте эти флаги:\n --cap-add=NET_RAW --cap-add=BPF --cap-add=PERFMON --net=host --pid=host" +# macOS privileges +"privileges.macos.bpf_access_missing": "Доступ к устройствам BPF (/dev/bpf*)" +"privileges.macos.run_sudo": "Запустить с sudo: sudo rustnet" +"privileges.macos.chmod_bpf": "Изменить права устройств BPF (временно):\n sudo chmod o+rw /dev/bpf*" +"privileges.macos.install_wireshark": "Установить помощник BPF прав (постоянно):\n brew install wireshark && sudo /usr/local/bin/install-bpf" +# FreeBSD privileges +"privileges.freebsd.bpf_access_missing": "Доступ к устройствам BPF (/dev/bpf*)" +"privileges.freebsd.run_sudo": "Запустить с sudo: sudo rustnet" +"privileges.freebsd.add_to_bpf_group": "Добавить пользователя в группу bpf:\n sudo pw groupmod bpf -m $(whoami)\n Затем выйти и войти снова" +"privileges.freebsd.chmod_bpf": "Изменить права устройств BPF (временно):\n sudo chmod o+rw /dev/bpf*" +# Windows privileges +"privileges.windows.admin_missing": "Права Администратора" +"privileges.windows.run_admin": "Запустить от Администратора: Правый клик на терминал и выбрать 'Запуск от имени Администратора'" +"privileges.windows.npcap_mode": "При использовании Npcap: Убедитесь что установлен с 'Режимом совместимости WinPcap API'" + +# CLI help text +"cli.about": "Кроссплатформенный инструмент мониторинга сети" +"cli.interface_help": "Сетевой интерфейс для мониторинга" +"cli.interface_help_linux": "Сетевой интерфейс для мониторинга (используйте \"any\" для захвата всех интерфейсов)" +"cli.no_localhost_help": "Фильтровать localhost подключения" +"cli.show_localhost_help": "Показывать localhost подключения (переопределяет фильтрацию по умолчанию)" +"cli.refresh_interval_help": "Интервал обновления UI в миллисекундах" +"cli.no_dpi_help": "Отключить глубокий анализ пакетов" +"cli.log_level_help": "Установить уровень логирования (если не указан, логирование не включается)" +"cli.json_log_help": "Включить JSON логирование событий подключений в указанный файл" +"cli.bpf_help": "Выражение BPF фильтра для захвата пакетов (напр. \"tcp port 443\", \"dst port 80\")" +"cli.bpf_help_macos": "Выражение BPF фильтра для захвата пакетов (напр. \"tcp port 443\"). Примечание: Использование BPF фильтра отключает PKTAP (информация о процессе возвращается к lsof)" +"cli.resolve_dns_help": "Включить обратное DNS разрешение для IP адресов (показывает имена хостов вместо IP)" +"cli.show_ptr_lookups_help": "Показывать PTR lookup подключения в UI (скрыты по умолчанию при --resolve-dns)" +"cli.lang_help": "Переопределить системную локаль (напр. en, es, de, fr, zh, ru)" +"cli.no_sandbox_help": "Отключить песочницу Landlock" +"cli.sandbox_strict_help": "Требовать полное применение песочницы или выход" + +# Common UI labels diff --git a/assets/locales/zh.yml b/assets/locales/zh.yml new file mode 100644 index 0000000..ed94590 --- /dev/null +++ b/assets/locales/zh.yml @@ -0,0 +1,221 @@ +# ============================================================================= +# RustNet Monitor - Chinese Translations (中文翻译) +# TODO: Translate strings from English to Chinese +# ============================================================================= + +# Application name +"app.name": "RustNet Monitor" + +# Tab names +"tabs.overview": "概览" +"tabs.details": "详情" +"tabs.interfaces": "接口" +"tabs.graph": "图表" +"tabs.help": "帮助" + +# Table headers +"table.headers.protocol": "协议" +"table.headers.local_address": "本地地址" +"table.headers.remote_address": "远程地址" +"table.headers.state": "状态" +"table.headers.service": "服务" +"table.headers.application": "应用 / 主机" +"table.headers.bandwidth": "↓/↑" +"table.headers.process": "进程" +"table.title": "活动连接" +"table.title_sorted": "活动连接 (排序: %{column} %{direction})" + +# Sort column display names +"sort.time": "时间" +"sort.bandwidth": "总带宽" +"sort.process": "进程" +"sort.local_addr": "本地地址" +"sort.remote_addr": "远程地址" +"sort.application": "应用" +"sort.service": "服务" +"sort.state": "状态" +"sort.protocol": "协议" + +# Statistics panel +"stats.title": "统计" +"stats.interface": "接口: %{name}" +"stats.link_layer": "链路层: %{type}" +"stats.link_layer_tunnel": "链路层: %{type} (隧道)" +"stats.process_detection": "进程检测: %{method}" +"stats.tcp_connections": "TCP 连接: %{count}" +"stats.udp_connections": "UDP 连接: %{count}" +"stats.total_connections": "总连接数: %{count}" +"stats.packets_processed": "已处理数据包: %{count}" +"stats.packets_dropped": "已丢弃数据包: %{count}" + +# Network stats panel +"network_stats.title": "网络统计" + +# Security panel +"security.title": "安全" +"security.landlock": "Landlock:" +"security.kernel_supported": "[内核支持]" +"security.kernel_unsupported": "[内核不支持]" +"security.no_restrictions": "无活动限制" +"security.cap_dropped": "已删除 CAP_NET_RAW" +"security.fs_restricted": "文件系统受限" +"security.net_blocked": "网络已阻止" +"security.running_as_root": "以 root 运行 (UID 0)" +"security.running_as_uid": "以 UID %{uid} 运行" +"security.running_as_admin": "以管理员身份运行" +"security.running_as_standard": "以标准用户运行" + +# Interface stats panel +"interface_stats.title": "接口统计 (按 'i')" +"interface_stats.full_title": "接口统计 (按 'i' 切换)" +"interface_stats.no_stats": "接口统计尚不可用..." +"interface_stats.more": "... 还有 %{count} 个 (按 'i')" +"interface_stats.err_label": "错误: " +"interface_stats.drop_label": "丢包: " +"interface_stats.headers.interface": "接口" +"interface_stats.headers.rx_rate": "接收速率" +"interface_stats.headers.tx_rate": "发送速率" +"interface_stats.headers.rx_packets": "接收包数" +"interface_stats.headers.tx_packets": "发送包数" +"interface_stats.headers.rx_err": "接收错误" +"interface_stats.headers.tx_err": "发送错误" +"interface_stats.headers.rx_drop": "接收丢包" +"interface_stats.headers.tx_drop": "发送丢包" +"interface_stats.headers.collisions": "碰撞" + +# Graph tab +"graph.traffic_title": "流量趋势 (60秒)" +"graph.connections_title": "连接" +"graph.connections_label": "%{count} 个连接" +"graph.collecting_data": "收集数据中..." +"graph.collecting_short": "收集中..." +"graph.app_distribution": "应用分布" +"graph.no_connections": "无连接" +"graph.top_processes": "热门进程" +"graph.no_active_processes": "无活动进程" +"graph.network_health": "网络健康" +"graph.tcp_counters": "TCP 计数器" +"graph.tcp_states": "TCP 状态" +"graph.no_tcp_connections": "无 TCP 连接" +"graph.axis.time": "时间" +"graph.axis.rate": "速率" +"graph.legend.rx": "RX (入站)" +"graph.legend.tx": "TX (出站)" + +# Connection details +"details.title": "连接详情" +"details.info_title": "连接信息" +"details.no_connections": "无可用连接" +"details.traffic_title": "流量统计" +"details.bytes_sent": "已发送字节:" +"details.bytes_received": "已接收字节:" +"details.packets_sent": "已发送数据包:" +"details.packets_received": "已接收数据包:" +"details.current_rate_in": "当前速率 (入站):" +"details.current_rate_out": "当前速率 (出站):" +"details.initial_rtt": "初始 RTT:" + +# Filter input +"filter.title_active": "过滤器 (↑↓/jk 导航, 回车确认, Esc 取消)" +"filter.title_applied": "已激活过滤器 (按 Esc 清除)" + +# Status bar messages +"status.quit_confirm": "再次按 'q' 退出或按其他键取消" +"status.clear_confirm": "再次按 'x' 清除所有连接或按其他键取消" +"status.default": "'h' 帮助 | Tab/Shift+Tab 切换标签 | '/' 过滤 | 'c' 复制 | 连接数: %{count}" +"status.help_hint": "按 'h' 获取帮助 | 'c' 复制地址 | 连接数: %{count}" +"status.filter_active": "'h' 帮助 | Tab/Shift+Tab 切换标签 | 显示 %{count} 个已过滤连接 (Esc 清除)" + +# Loading screen +"loading.title": "RustNet Monitor" +"loading.message": "正在加载网络连接..." +"loading.subtitle": "这可能需要几秒钟" + +# Help screen +"help.title": "RustNet Monitor" +"help.subtitle": "网络连接监控器" +"help.keys.quit": "退出应用 (按两次确认)" +"help.keys.quit_immediate": "立即退出" +"help.keys.switch_tabs": "切换标签页" +"help.keys.navigate": "导航连接 (循环)" +"help.keys.jump_first_last": "跳转到第一个/最后一个连接 (vim风格)" +"help.keys.page_navigate": "按页导航连接" +"help.keys.copy_address": "复制远程地址到剪贴板" +"help.keys.toggle_ports": "切换服务名称和端口号" +"help.keys.toggle_hostnames": "切换主机名和 IP (使用 --resolve-dns 时)" +"help.keys.cycle_sort": "循环排序列 (带宽, 进程等)" +"help.keys.toggle_sort_dir": "切换排序方向 (升序/降序)" +"help.keys.view_details": "查看连接详情" +"help.keys.return_overview": "返回概览" +"help.keys.toggle_help": "切换此帮助屏幕" +"help.keys.toggle_interfaces": "切换接口统计视图" +"help.keys.filter_mode": "进入过滤模式 (边输入边导航!)" +"help.keys.clear_connections": "清除所有连接" +"help.sections.tabs": "标签页:" +"help.sections.overview_desc": "带有迷你流量图的连接列表" +"help.sections.details_desc": "所选连接的完整详情" +"help.sections.interfaces_desc": "网络接口统计" +"help.sections.graph_desc": "流量图表和协议分布" +"help.sections.help_desc": "此帮助屏幕" +"help.sections.colors": "连接颜色:" +"help.colors.white": "白色" +"help.colors.yellow": "黄色" +"help.colors.red": "红色" +"help.sections.colors_white": "活动连接 (< 75% 超时)" +"help.sections.colors_yellow": "过期连接 (75-90% 超时)" +"help.sections.colors_red": "危急 - 即将删除 (> 90% 超时)" +"help.sections.filter_examples": "过滤器示例:" +"help.filter_examples.general": "在所有字段中搜索 '%{term}'" +"help.filter_examples.port": "过滤包含 '%{term}' 的端口 (443, 8080 等)" +"help.filter_examples.src": "按源 IP 前缀过滤" +"help.filter_examples.dst": "按目标过滤" +"help.filter_examples.sni": "按 SNI 主机名过滤" +"help.filter_examples.process": "按进程名过滤" + +# Error messages + +# Privilege error messages (platform-specific) +"privileges.insufficient": "网络数据包捕获权限不足。" +"privileges.missing": "缺少" +"privileges.how_to_fix": "如何修复" +# Linux privileges +"privileges.linux.cap_net_raw_missing": "CAP_NET_RAW 能力 (数据包捕获必需)" +"privileges.linux.run_sudo": "使用 sudo 运行: sudo rustnet" +"privileges.linux.setcap_modern": "设置能力 (现代 Linux 5.8+, 使用 eBPF): sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(which rustnet)" +"privileges.linux.setcap_legacy": "设置能力 (旧内核, 使用 eBPF): sudo setcap 'cap_net_raw,cap_sys_admin=eip' $(which rustnet)" +"privileges.linux.setcap_minimal": "设置能力 (仅捕获, 无 eBPF): sudo setcap 'cap_net_raw=eip' $(which rustnet)" +"privileges.linux.docker_flags": "如果在 Docker 中运行, 添加这些标志:\n --cap-add=NET_RAW --cap-add=BPF --cap-add=PERFMON --net=host --pid=host" +# macOS privileges +"privileges.macos.bpf_access_missing": "BPF 设备访问权限 (/dev/bpf*)" +"privileges.macos.run_sudo": "使用 sudo 运行: sudo rustnet" +"privileges.macos.chmod_bpf": "更改 BPF 设备权限 (临时):\n sudo chmod o+rw /dev/bpf*" +"privileges.macos.install_wireshark": "安装 BPF 权限助手 (永久):\n brew install wireshark && sudo /usr/local/bin/install-bpf" +# FreeBSD privileges +"privileges.freebsd.bpf_access_missing": "BPF 设备访问权限 (/dev/bpf*)" +"privileges.freebsd.run_sudo": "使用 sudo 运行: sudo rustnet" +"privileges.freebsd.add_to_bpf_group": "将用户添加到 bpf 组:\n sudo pw groupmod bpf -m $(whoami)\n 然后注销并重新登录" +"privileges.freebsd.chmod_bpf": "更改 BPF 设备权限 (临时):\n sudo chmod o+rw /dev/bpf*" +# Windows privileges +"privileges.windows.admin_missing": "管理员权限" +"privileges.windows.run_admin": "以管理员身份运行: 右键点击终端并选择 '以管理员身份运行'" +"privileges.windows.npcap_mode": "如使用 Npcap: 确保安装时启用了 'WinPcap API 兼容模式'" + +# CLI help text +"cli.about": "跨平台网络监控工具" +"cli.interface_help": "要监控的网络接口" +"cli.interface_help_linux": "要监控的网络接口 (使用 \"any\" 捕获所有接口)" +"cli.no_localhost_help": "过滤 localhost 连接" +"cli.show_localhost_help": "显示 localhost 连接 (覆盖默认过滤)" +"cli.refresh_interval_help": "UI 刷新间隔 (毫秒)" +"cli.no_dpi_help": "禁用深度包检测" +"cli.log_level_help": "设置日志级别 (如未提供, 则不启用日志)" +"cli.json_log_help": "启用连接事件 JSON 日志记录到指定文件" +"cli.bpf_help": "用于数据包捕获的 BPF 过滤表达式 (例如 \"tcp port 443\", \"dst port 80\")" +"cli.bpf_help_macos": "用于数据包捕获的 BPF 过滤表达式 (例如 \"tcp port 443\")。注意: 使用 BPF 过滤器会禁用 PKTAP (进程信息回退到 lsof)" +"cli.resolve_dns_help": "启用 IP 地址反向 DNS 解析 (显示主机名而非 IP)" +"cli.show_ptr_lookups_help": "在 UI 中显示 PTR 查询连接 (启用 --resolve-dns 时默认隐藏)" +"cli.lang_help": "覆盖系统语言环境 (例如 en, es, de, fr, zh, ru)" +"cli.no_sandbox_help": "禁用 Landlock 沙箱" +"cli.sandbox_strict_help": "要求完全沙箱执行或退出" + +# Common UI labels diff --git a/build.rs b/build.rs index a1045c7..be9b11c 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,12 @@ use anyhow::Result; use std::{env, fs::File, path::PathBuf}; +// Initialize i18n for CLI help text generation (shell completions and manpages) +rust_i18n::i18n!("assets/locales", fallback = "en"); + fn main() -> Result<()> { + // Set locale for build script (uses English for generated assets) + rust_i18n::set_locale("en"); // Generate shell completions and manpage generate_assets()?; diff --git a/scripts/check-locales.py b/scripts/check-locales.py new file mode 100755 index 0000000..1a21e22 --- /dev/null +++ b/scripts/check-locales.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Check that all translation keys from source code are present in locale files. + +Exit codes: + 0 - All keys present and consistent + 1 - Missing keys or inconsistencies found +""" + +import re +import sys +from pathlib import Path + + +def extract_source_keys(src_dir: Path) -> set[str]: + """Extract t!("key") translation keys from Rust source files.""" + pattern = re.compile(r't!\("([a-z_]+(?:\.[a-z_]+)+)"') + keys = set() + for rust_file in src_dir.rglob("*.rs"): + content = rust_file.read_text() + keys.update(pattern.findall(content)) + return keys + + +def extract_locale_keys(locale_file: Path) -> set[str]: + """Extract keys from a YAML locale file.""" + pattern = re.compile(r'^"([^"]+)":', re.MULTILINE) + content = locale_file.read_text() + return set(pattern.findall(content)) + + +def main() -> int: + root = Path(__file__).parent.parent + src_dir = root / "src" + locales_dir = root / "assets" / "locales" + + source_keys = extract_source_keys(src_dir) + print(f"Source code: {len(source_keys)} unique translation keys") + print("---") + + all_locale_keys = {} + for locale_file in sorted(locales_dir.glob("*.yml")): + locale_keys = extract_locale_keys(locale_file) + all_locale_keys[locale_file.name] = locale_keys + print(f"{locale_file.name}: {len(locale_keys)} keys") + + errors = [] + reference_locale = "en.yml" + reference_keys = all_locale_keys.get(reference_locale, set()) + + # Check for missing keys (in source but not in locale) + missing = source_keys - reference_keys + if missing: + errors.append(f"MISSING: {len(missing)} keys in source but not in locales:") + for key in sorted(missing): + errors.append(f" {key}") + + # Check consistency across locale files + for name, keys in sorted(all_locale_keys.items()): + if name == reference_locale: + continue + missing_in_locale = reference_keys - keys + extra_in_locale = keys - reference_keys + if missing_in_locale: + errors.append(f"{name} missing {len(missing_in_locale)} keys from {reference_locale}:") + for key in sorted(missing_in_locale): + errors.append(f" {key}") + if extra_in_locale: + errors.append(f"{name} has {len(extra_in_locale)} extra keys not in {reference_locale}:") + for key in sorted(extra_in_locale): + errors.append(f" {key}") + + if errors: + print("---", file=sys.stderr) + for error in errors: + print(error, file=sys.stderr) + return 1 + + print("---") + print("All translation keys present and consistent") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/cli.rs b/src/cli.rs index ec73596..dc35909 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,41 +1,53 @@ use clap::{Arg, Command}; +use rust_i18n::t; -#[cfg(target_os = "linux")] -const INTERFACE_HELP: &str = "Network interface to monitor (use \"any\" to capture all interfaces)"; +/// Get the interface help text (platform-specific) +fn interface_help() -> String { + #[cfg(target_os = "linux")] + { + t!("cli.interface_help_linux").to_string() + } + #[cfg(not(target_os = "linux"))] + { + t!("cli.interface_help").to_string() + } +} -#[cfg(not(target_os = "linux"))] -const INTERFACE_HELP: &str = "Network interface to monitor"; - -#[cfg(target_os = "macos")] -const BPF_HELP: &str = "BPF filter expression for packet capture (e.g., \"tcp port 443\"). Note: Using a BPF filter disables PKTAP (process info falls back to lsof)"; - -#[cfg(not(target_os = "macos"))] -const BPF_HELP: &str = - "BPF filter expression for packet capture (e.g., \"tcp port 443\", \"dst port 80\")"; +/// Get the BPF filter help text (platform-specific) +fn bpf_help() -> String { + #[cfg(target_os = "macos")] + { + t!("cli.bpf_help_macos").to_string() + } + #[cfg(not(target_os = "macos"))] + { + t!("cli.bpf_help").to_string() + } +} pub fn build_cli() -> Command { let cmd = Command::new("rustnet") .version(env!("CARGO_PKG_VERSION")) .author("Network Monitor") - .about("Cross-platform network monitoring tool") + .about(t!("cli.about").to_string()) .arg( Arg::new("interface") .short('i') .long("interface") .value_name("INTERFACE") - .help(INTERFACE_HELP) + .help(interface_help()) .required(false), ) .arg( Arg::new("no-localhost") .long("no-localhost") - .help("Filter out localhost connections") + .help(t!("cli.no_localhost_help").to_string()) .action(clap::ArgAction::SetTrue), ) .arg( Arg::new("show-localhost") .long("show-localhost") - .help("Show localhost connections (overrides default filtering)") + .help(t!("cli.show_localhost_help").to_string()) .action(clap::ArgAction::SetTrue), ) .arg( @@ -43,7 +55,7 @@ pub fn build_cli() -> Command { .short('r') .long("refresh-interval") .value_name("MILLISECONDS") - .help("UI refresh interval in milliseconds") + .help(t!("cli.refresh_interval_help").to_string()) .value_parser(clap::value_parser!(u64)) .default_value("1000") .required(false), @@ -51,7 +63,7 @@ pub fn build_cli() -> Command { .arg( Arg::new("no-dpi") .long("no-dpi") - .help("Disable deep packet inspection") + .help(t!("cli.no_dpi_help").to_string()) .action(clap::ArgAction::SetTrue), ) .arg( @@ -59,14 +71,14 @@ pub fn build_cli() -> Command { .short('l') .long("log-level") .value_name("LEVEL") - .help("Set the log level (if not provided, no logging will be enabled)") + .help(t!("cli.log_level_help").to_string()) .required(false), ) .arg( Arg::new("json-log") .long("json-log") .value_name("FILE") - .help("Enable JSON logging of connection events to specified file") + .help(t!("cli.json_log_help").to_string()) .required(false), ) .arg( @@ -74,20 +86,27 @@ pub fn build_cli() -> Command { .short('f') .long("bpf-filter") .value_name("FILTER") - .help(BPF_HELP) + .help(bpf_help()) .required(false), ) .arg( Arg::new("resolve-dns") .long("resolve-dns") - .help("Enable reverse DNS resolution for IP addresses (shows hostnames instead of IPs)") + .help(t!("cli.resolve_dns_help").to_string()) .action(clap::ArgAction::SetTrue), ) .arg( Arg::new("show-ptr-lookups") .long("show-ptr-lookups") - .help("Show PTR lookup connections in UI (hidden by default when --resolve-dns is enabled)") + .help(t!("cli.show_ptr_lookups_help").to_string()) .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("lang") + .long("lang") + .value_name("LOCALE") + .help(t!("cli.lang_help").to_string()) + .required(false), ); #[cfg(target_os = "linux")] @@ -95,13 +114,13 @@ pub fn build_cli() -> Command { .arg( Arg::new("no-sandbox") .long("no-sandbox") - .help("Disable Landlock sandboxing") + .help(t!("cli.no_sandbox_help").to_string()) .action(clap::ArgAction::SetTrue), ) .arg( Arg::new("sandbox-strict") .long("sandbox-strict") - .help("Require full sandbox enforcement or exit") + .help(t!("cli.sandbox_strict_help").to_string()) .action(clap::ArgAction::SetTrue) .conflicts_with("no-sandbox"), ); diff --git a/src/lib.rs b/src/lib.rs index 2cd1d06..98200ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,9 @@ //! //! A cross-platform network monitoring library built with Rust. +// Initialize internationalization with YAML files from assets/locales/ directory +rust_i18n::i18n!("assets/locales", fallback = "en"); + pub mod app; pub mod config; pub mod filter; diff --git a/src/main.rs b/src/main.rs index ccfc9d6..fcb0a9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,20 +8,72 @@ use std::io; use std::path::Path; use std::time::Duration; +// Initialize i18n for the binary crate +rust_i18n::i18n!("assets/locales", fallback = "en"); + mod app; mod cli; mod filter; mod network; mod ui; +/// Detect locale from environment, CLI args, or system settings (before CLI parsing). +/// +/// NOTE: This function manually parses `--lang` from argv BEFORE clap parses arguments. +/// This is intentional: we need the locale set before `build_cli()` is called so that +/// clap's help text (`--help`) can be displayed in the user's language. This duplicates +/// some of clap's parsing logic, but is necessary for proper i18n of CLI help messages. +fn detect_locale_early() -> String { + // Check RUSTNET_LANG environment variable first + if let Ok(lang) = std::env::var("RUSTNET_LANG") { + return lang; + } + + // Check for --lang argument in argv (before full CLI parsing) + // This allows help text to be translated correctly + let args: Vec = std::env::args().collect(); + for i in 0..args.len() { + if args[i] == "--lang" { + if let Some(lang) = args.get(i + 1) { + return lang.clone(); + } + } else if let Some(lang) = args[i].strip_prefix("--lang=") { + return lang.to_string(); + } + } + + // Fall back to system locale detection + sys_locale::get_locale() + .unwrap_or_else(|| String::from("en")) + .split('-') + .next() + .unwrap_or("en") + .to_string() +} + +/// Apply locale override from CLI if provided +fn apply_locale_override(matches: &clap::ArgMatches) { + if let Some(lang) = matches.get_one::("lang") { + rust_i18n::set_locale(lang); + info!("Locale overridden via --lang: {}", lang); + } +} + fn main() -> Result<()> { // Check for required dependencies on Windows #[cfg(target_os = "windows")] check_windows_dependencies()?; - // Parse command line arguments + // Detect and set locale BEFORE building CLI (so help text is translated) + let early_locale = detect_locale_early(); + rust_i18n::set_locale(&early_locale); + + // Parse command line arguments (now with translated help text) let matches = cli::build_cli().get_matches(); + // Apply CLI locale override if provided (--lang flag takes precedence) + apply_locale_override(&matches); + // Check privileges BEFORE initializing TUI (so error messages are visible) check_privileges_early()?; // Set up logging only if log-level was provided diff --git a/src/network/privileges.rs b/src/network/privileges.rs index 5eac548..938d13b 100644 --- a/src/network/privileges.rs +++ b/src/network/privileges.rs @@ -17,6 +17,7 @@ use anyhow::anyhow; ))] use log::warn; use log::{debug, info}; +use rust_i18n::t; /// Privilege check result with detailed information #[derive(Debug, Clone)] @@ -61,10 +62,10 @@ impl PrivilegeStatus { return String::new(); } - let mut msg = String::from("Insufficient privileges for network packet capture.\n\n"); + let mut msg = format!("{}\n\n", t!("privileges.insufficient")); if !self.missing.is_empty() { - msg.push_str("Missing:\n"); + msg.push_str(&format!("{}:\n", t!("privileges.missing"))); for item in &self.missing { msg.push_str(&format!(" • {}\n", item)); } @@ -72,7 +73,7 @@ impl PrivilegeStatus { } if !self.instructions.is_empty() { - msg.push_str("How to fix:\n"); + msg.push_str(&format!("{}:\n", t!("privileges.how_to_fix"))); for (i, instruction) in self.instructions.iter().enumerate() { msg.push_str(&format!(" {}. {}\n", i + 1, instruction)); } @@ -156,25 +157,20 @@ fn check_linux_privileges() -> Result { return Ok(PrivilegeStatus::sufficient()); } else { debug!("CAP_NET_RAW: missing"); - missing.push("CAP_NET_RAW capability (required for packet capture)".to_string()); + missing.push(t!("privileges.linux.cap_net_raw_missing").to_string()); } // Build instructions for gaining privileges let mut instructions = vec![ - "Run with sudo: sudo rustnet".to_string(), - "Set capabilities (modern Linux 5.8+, with eBPF): sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(which rustnet)".to_string(), - "Set capabilities (legacy/older kernels, with eBPF): sudo setcap 'cap_net_raw,cap_sys_admin=eip' $(which rustnet)".to_string(), - "Set capabilities (packet capture only, no eBPF): sudo setcap 'cap_net_raw=eip' $(which rustnet)".to_string(), + t!("privileges.linux.run_sudo").to_string(), + t!("privileges.linux.setcap_modern").to_string(), + t!("privileges.linux.setcap_legacy").to_string(), + t!("privileges.linux.setcap_minimal").to_string(), ]; // Add Docker-specific instructions if it looks like we're in a container if is_running_in_container() { - instructions.push( - "If running in Docker, add these flags:\n \ - --cap-add=NET_RAW --cap-add=BPF --cap-add=PERFMON \ - --net=host --pid=host" - .to_string(), - ); + instructions.push(t!("privileges.linux.docker_flags").to_string()); } Ok(PrivilegeStatus::insufficient(missing, instructions)) @@ -268,16 +264,12 @@ fn check_macos_privileges() -> Result { } // No BPF access - build error message - let missing = vec!["Access to BPF devices (/dev/bpf*)".to_string()]; + let missing = vec![t!("privileges.macos.bpf_access_missing").to_string()]; let instructions = vec![ - "Run with sudo: sudo rustnet".to_string(), - "Change BPF device permissions (temporary):\n \ - sudo chmod o+rw /dev/bpf*" - .to_string(), - "Install BPF permission helper (persistent):\n \ - brew install wireshark && sudo /usr/local/bin/install-bpf" - .to_string(), + t!("privileges.macos.run_sudo").to_string(), + t!("privileges.macos.chmod_bpf").to_string(), + t!("privileges.macos.install_wireshark").to_string(), ]; Ok(PrivilegeStatus::insufficient(missing, instructions)) @@ -331,11 +323,11 @@ fn check_windows_privileges() -> Result { || error_str.contains("denied") || error_str.contains("permission") { - let missing = vec!["Administrator privileges".to_string()]; + let missing = vec![t!("privileges.windows.admin_missing").to_string()]; let instructions = vec![ - "Run as Administrator: Right-click the terminal and select 'Run as Administrator'".to_string(), - "If using Npcap: Ensure it was installed with 'WinPcap API-compatible Mode' enabled".to_string(), + t!("privileges.windows.run_admin").to_string(), + t!("privileges.windows.npcap_mode").to_string(), ]; Ok(PrivilegeStatus::insufficient(missing, instructions)) @@ -395,17 +387,12 @@ fn check_freebsd_privileges() -> Result { } // No BPF access - build error message - let missing = vec!["Access to BPF devices (/dev/bpf*)".to_string()]; + let missing = vec![t!("privileges.freebsd.bpf_access_missing").to_string()]; let instructions = vec![ - "Run with sudo: sudo rustnet".to_string(), - "Add your user to the bpf group:\n \ - sudo pw groupmod bpf -m $(whoami)\n \ - Then logout and login again" - .to_string(), - "Change BPF device permissions (temporary):\n \ - sudo chmod o+rw /dev/bpf*" - .to_string(), + t!("privileges.freebsd.run_sudo").to_string(), + t!("privileges.freebsd.add_to_bpf_group").to_string(), + t!("privileges.freebsd.chmod_bpf").to_string(), ]; Ok(PrivilegeStatus::insufficient(missing, instructions)) diff --git a/src/ui.rs b/src/ui.rs index 69e733a..cc9d1c2 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use anyhow::Result; use ratatui::{ Frame, Terminal as RatatuiTerminal, @@ -10,6 +12,7 @@ use ratatui::{ Tabs, Wrap, }, }; +use rust_i18n::t; use crate::app::{App, AppStats}; use crate::network::dns::DnsResolver; @@ -68,18 +71,21 @@ impl SortColumn { } } - /// Get the display name for the sort column - pub fn display_name(self) -> &'static str { + /// Get the display name for the sort column. + /// + /// Returns `Cow<'static, str>` to avoid allocations when using English locale + /// (the `t!()` macro returns the static string directly in that case). + pub fn display_name(self) -> Cow<'static, str> { match self { - Self::CreatedAt => "Time", - Self::BandwidthTotal => "Bandwidth Total", - Self::Process => "Process", - Self::LocalAddress => "Local Addr", - Self::RemoteAddress => "Remote Addr", - Self::Application => "Application", - Self::Service => "Service", - Self::State => "State", - Self::Protocol => "Protocol", + Self::CreatedAt => t!("sort.time"), + Self::BandwidthTotal => t!("sort.bandwidth"), + Self::Process => t!("sort.process"), + Self::LocalAddress => t!("sort.local_addr"), + Self::RemoteAddress => t!("sort.remote_addr"), + Self::Application => t!("sort.application"), + Self::Service => t!("sort.service"), + Self::State => t!("sort.state"), + Self::Protocol => t!("sort.protocol"), } } } @@ -438,18 +444,18 @@ pub fn draw( /// Draw mode tabs fn draw_tabs(f: &mut Frame, ui_state: &UIState, area: Rect) { let titles = vec![ - Span::styled("Overview", Style::default().fg(Color::Green)), - Span::styled("Details", Style::default().fg(Color::Green)), - Span::styled("Interfaces", Style::default().fg(Color::Green)), - Span::styled("Graph", Style::default().fg(Color::Green)), - Span::styled("Help", Style::default().fg(Color::Green)), + Span::styled(t!("tabs.overview").to_string(), Style::default().fg(Color::Green)), + Span::styled(t!("tabs.details").to_string(), Style::default().fg(Color::Green)), + Span::styled(t!("tabs.interfaces").to_string(), Style::default().fg(Color::Green)), + Span::styled(t!("tabs.graph").to_string(), Style::default().fg(Color::Green)), + Span::styled(t!("tabs.help").to_string(), Style::default().fg(Color::Green)), ]; let tabs = Tabs::new(titles.into_iter().map(Line::from).collect::>()) .block( Block::default() .borders(Borders::ALL) - .title("RustNet Monitor"), + .title(t!("app.name").to_string()), ) .select(ui_state.selected_tab) .style(Style::default()) @@ -529,20 +535,20 @@ fn draw_connections_list( } else { "↓" }; - format!("Down/Up {}", arrow) // "Down/Up ↓" or "Down/Up ↑" + format!("{} {}", t!("table.headers.bandwidth"), arrow) } - _ => "Down/Up".to_string(), // No bandwidth sort active + _ => t!("table.headers.bandwidth").to_string(), }; let header_labels = [ - add_sort_indicator("Pro", &[SortColumn::Protocol]), - add_sort_indicator("Local Address", &[SortColumn::LocalAddress]), - add_sort_indicator("Remote Address", &[SortColumn::RemoteAddress]), - add_sort_indicator("State", &[SortColumn::State]), - add_sort_indicator("Service", &[SortColumn::Service]), - add_sort_indicator("Application / Host", &[SortColumn::Application]), + add_sort_indicator(&t!("table.headers.protocol"), &[SortColumn::Protocol]), + add_sort_indicator(&t!("table.headers.local_address"), &[SortColumn::LocalAddress]), + add_sort_indicator(&t!("table.headers.remote_address"), &[SortColumn::RemoteAddress]), + add_sort_indicator(&t!("table.headers.state"), &[SortColumn::State]), + add_sort_indicator(&t!("table.headers.service"), &[SortColumn::Service]), + add_sort_indicator(&t!("table.headers.application"), &[SortColumn::Application]), bandwidth_label, // Use custom bandwidth label instead of generic indicator - add_sort_indicator("Process", &[SortColumn::Process]), + add_sort_indicator(&t!("table.headers.process"), &[SortColumn::Process]), ]; let header_cells = header_labels.iter().enumerate().map(|(idx, h)| { @@ -724,13 +730,9 @@ fn draw_connections_list( } else { "↓" }; - format!( - "Active Connections (Sort: {} {})", - ui_state.sort_column.display_name(), - direction - ) + t!("table.title_sorted", column = ui_state.sort_column.display_name(), direction = direction).to_string() } else { - "Active Connections".to_string() + t!("table.title").to_string() }; let connections_table = Table::new(rows, &widths) @@ -777,35 +779,27 @@ fn draw_stats_panel( let process_detection_method = app.get_process_detection_method(); let (link_layer_type, is_tunnel) = app.get_link_layer_info(); + let link_layer_display = if is_tunnel { + t!("stats.link_layer_tunnel", "type" = link_layer_type).to_string() + } else { + t!("stats.link_layer", "type" = link_layer_type).to_string() + }; + let conn_stats_text: Vec = vec![ - Line::from(format!("Interface: {}", interface_name)), - Line::from(format!( - "Link Layer: {}{}", - link_layer_type, - if is_tunnel { " (Tunnel)" } else { "" } - )), - Line::from(format!("Process Detection: {}", process_detection_method)), + Line::from(t!("stats.interface", name = interface_name).to_string()), + Line::from(link_layer_display), + Line::from(t!("stats.process_detection", method = process_detection_method).to_string()), Line::from(""), - Line::from(format!("TCP Connections: {}", tcp_count)), - Line::from(format!("UDP Connections: {}", udp_count)), - Line::from(format!("Total Connections: {}", connections.len())), + Line::from(t!("stats.tcp_connections", count = tcp_count).to_string()), + Line::from(t!("stats.udp_connections", count = udp_count).to_string()), + Line::from(t!("stats.total_connections", count = connections.len()).to_string()), Line::from(""), - Line::from(format!( - "Packets Processed: {}", - stats - .packets_processed - .load(std::sync::atomic::Ordering::Relaxed) - )), - Line::from(format!( - "Packets Dropped: {}", - stats - .packets_dropped - .load(std::sync::atomic::Ordering::Relaxed) - )), + Line::from(t!("stats.packets_processed", count = stats.packets_processed.load(std::sync::atomic::Ordering::Relaxed)).to_string()), + Line::from(t!("stats.packets_dropped", count = stats.packets_dropped.load(std::sync::atomic::Ordering::Relaxed)).to_string()), ]; let conn_stats = Paragraph::new(conn_stats_text) - .block(Block::default().borders(Borders::ALL).title("Statistics")) + .block(Block::default().borders(Borders::ALL).title(t!("stats.title").to_string())) .style(Style::default()); f.render_widget(conn_stats, chunks[0]); @@ -861,7 +855,7 @@ fn draw_stats_panel( .block( Block::default() .borders(Borders::ALL) - .title("Network Stats"), + .title(t!("network_stats.title").to_string()), ) .style(Style::default()); f.render_widget(network_stats, chunks[1]); @@ -879,33 +873,33 @@ fn draw_stats_panel( let mut features = Vec::new(); if sandbox_info.cap_dropped { - features.push("CAP_NET_RAW dropped"); + features.push(t!("security.cap_dropped").to_string()); } if sandbox_info.fs_restricted { - features.push("FS restricted"); + features.push(t!("security.fs_restricted").to_string()); } if sandbox_info.net_restricted { - features.push("Net blocked"); + features.push(t!("security.net_blocked").to_string()); } let available_indicator = if sandbox_info.landlock_available { - Span::styled(" [kernel supported]", Style::default().fg(Color::DarkGray)) + Span::styled(format!(" {}", t!("security.kernel_supported")), Style::default().fg(Color::DarkGray)) } else { Span::styled( - " [kernel unsupported]", + format!(" {}", t!("security.kernel_unsupported")), Style::default().fg(Color::DarkGray), ) }; vec![ Line::from(vec![ - Span::raw("Landlock: "), + Span::raw(format!("{} ", t!("security.landlock"))), Span::styled(sandbox_info.status.clone(), status_style), available_indicator, ]), Line::from(Span::styled( if features.is_empty() { - "No restrictions active".to_string() + t!("security.no_restrictions").to_string() } else { features.join(", ") }, @@ -921,12 +915,12 @@ fn draw_stats_panel( let is_root = uid == 0; if is_root { vec![Line::from(Span::styled( - "Running as root (UID 0)", + t!("security.running_as_root").to_string(), Style::default().fg(Color::Yellow), ))] } else { vec![Line::from(Span::styled( - format!("Running as UID {}", uid), + t!("security.running_as_uid", uid = uid).to_string(), Style::default().fg(Color::Green), ))] } @@ -937,19 +931,19 @@ fn draw_stats_panel( let is_elevated = crate::is_admin(); if is_elevated { vec![Line::from(Span::styled( - "Running as Administrator", + t!("security.running_as_admin").to_string(), Style::default().fg(Color::Yellow), ))] } else { vec![Line::from(Span::styled( - "Running as standard user", + t!("security.running_as_standard").to_string(), Style::default().fg(Color::Green), ))] } }; let security_stats = Paragraph::new(security_text) - .block(Block::default().borders(Borders::ALL).title("Security")) + .block(Block::default().borders(Borders::ALL).title(t!("security.title").to_string())) .style(Style::default()); f.render_widget(security_stats, chunks[2]); @@ -963,7 +957,7 @@ fn draw_stats_panel( fn draw_interface_stats_with_graph(f: &mut Frame, app: &App, area: Rect) -> Result<()> { let block = Block::default() .borders(Borders::ALL) - .title("Interface Stats (press 'i')"); + .title(t!("interface_stats.title").to_string()); let inner = block.inner(area); f.render_widget(block, area); @@ -1101,19 +1095,16 @@ fn draw_interface_stats_with_graph(f: &mut Frame, app: &App, area: Rect) -> Resu // Show interface name with errors/drops on single line lines.push(Line::from(vec![ Span::raw(format!("{}: ", stat.interface_name)), - Span::raw("Err: "), + Span::raw(t!("interface_stats.err_label").to_string()), Span::styled(format!("{}", total_errors), error_style), - Span::raw(" Drop: "), + Span::raw(format!(" {}", t!("interface_stats.drop_label"))), Span::styled(format!("{}", total_drops), drop_style), ])); } if filtered_interface_stats.len() > num_to_show { lines.push(Line::from(Span::styled( - format!( - "... {} more (press 'i')", - filtered_interface_stats.len() - num_to_show - ), + t!("interface_stats.more", count = filtered_interface_stats.len() - num_to_show).to_string(), Style::default().fg(Color::Gray), ))); } @@ -1180,10 +1171,10 @@ fn draw_graph_tab(f: &mut Frame, app: &App, connections: &[Connection], area: Re fn draw_traffic_chart(f: &mut Frame, history: &TrafficHistory, area: Rect) { let block = Block::default() .borders(Borders::ALL) - .title("Traffic Over Time (60s)"); + .title(t!("graph.traffic_title").to_string()); if !history.has_enough_data() { - let placeholder = Paragraph::new("Collecting data...") + let placeholder = Paragraph::new(t!("graph.collecting_data").to_string()) .block(block) .style(Style::default().fg(Color::DarkGray)); f.render_widget(placeholder, area); @@ -1219,7 +1210,7 @@ fn draw_traffic_chart(f: &mut Frame, history: &TrafficHistory, area: Rect) { .block(block) .x_axis( Axis::default() - .title("Time") + .title(t!("graph.axis.time").to_string()) .style(Style::default().fg(Color::Gray)) .bounds([-60.0, 0.0]) .labels(vec![ @@ -1230,7 +1221,7 @@ fn draw_traffic_chart(f: &mut Frame, history: &TrafficHistory, area: Rect) { ) .y_axis( Axis::default() - .title("Rate") + .title(t!("graph.axis.rate").to_string()) .style(Style::default().fg(Color::Gray)) .bounds([0.0, max_rate]) .labels(vec![ @@ -1245,14 +1236,14 @@ fn draw_traffic_chart(f: &mut Frame, history: &TrafficHistory, area: Rect) { /// Draw connections count sparkline fn draw_connections_sparkline(f: &mut Frame, history: &TrafficHistory, area: Rect) { - let block = Block::default().borders(Borders::ALL).title("Connections"); + let block = Block::default().borders(Borders::ALL).title(t!("graph.connections_title").to_string()); let inner = block.inner(area); f.render_widget(block, area); if !history.has_enough_data() { let placeholder = - Paragraph::new("Collecting...").style(Style::default().fg(Color::DarkGray)); + Paragraph::new(t!("graph.collecting_short").to_string()).style(Style::default().fg(Color::DarkGray)); f.render_widget(placeholder, inner); return; } @@ -1273,7 +1264,7 @@ fn draw_connections_sparkline(f: &mut Frame, history: &TrafficHistory, area: Rec // Current connection count label let current_count = conn_data.last().copied().unwrap_or(0); - let label = Paragraph::new(format!("{} connections", current_count)) + let label = Paragraph::new(t!("graph.connections_label", count = current_count).to_string()) .style(Style::default().fg(Color::White)); f.render_widget(label, chunks[1]); } @@ -1282,7 +1273,7 @@ fn draw_connections_sparkline(f: &mut Frame, history: &TrafficHistory, area: Rec fn draw_app_distribution(f: &mut Frame, connections: &[Connection], area: Rect) { let block = Block::default() .borders(Borders::ALL) - .title("Application Distribution"); + .title(t!("graph.app_distribution").to_string()); let inner = block.inner(area); f.render_widget(block, area); @@ -1322,7 +1313,7 @@ fn draw_app_distribution(f: &mut Frame, connections: &[Connection], area: Rect) if lines.is_empty() { lines.push(Line::from(Span::styled( - "No connections", + t!("graph.no_connections").to_string(), Style::default().fg(Color::DarkGray), ))); } @@ -1337,7 +1328,7 @@ fn draw_top_processes(f: &mut Frame, connections: &[Connection], area: Rect) { let block = Block::default() .borders(Borders::ALL) - .title("Top Processes"); + .title(t!("graph.top_processes").to_string()); let inner = block.inner(area); f.render_widget(block, area); @@ -1379,7 +1370,7 @@ fn draw_top_processes(f: &mut Frame, connections: &[Connection], area: Rect) { if rows.is_empty() { let placeholder = - Paragraph::new("No active processes").style(Style::default().fg(Color::DarkGray)); + Paragraph::new(t!("graph.no_active_processes").to_string()).style(Style::default().fg(Color::DarkGray)); f.render_widget(placeholder, inner); return; } @@ -1403,9 +1394,9 @@ fn draw_top_processes(f: &mut Frame, connections: &[Connection], area: Rect) { fn draw_traffic_legend(f: &mut Frame, area: Rect) { let legend = Paragraph::new(Line::from(vec![ Span::styled("▬", Style::default().fg(Color::Green)), - Span::raw(" RX (incoming) "), + Span::raw(format!(" {} ", t!("graph.legend.rx"))), Span::styled("▬", Style::default().fg(Color::Blue)), - Span::raw(" TX (outgoing)"), + Span::raw(format!(" {}", t!("graph.legend.tx"))), ])) .style(Style::default().fg(Color::DarkGray)); @@ -1416,14 +1407,14 @@ fn draw_traffic_legend(f: &mut Frame, area: Rect) { fn draw_health_chart(f: &mut Frame, history: &TrafficHistory, area: Rect) { let block = Block::default() .borders(Borders::ALL) - .title("Network Health"); + .title(t!("graph.network_health").to_string()); let inner = block.inner(area); f.render_widget(block, area); if !history.has_enough_data() { let placeholder = - Paragraph::new("Collecting data...").style(Style::default().fg(Color::DarkGray)); + Paragraph::new(t!("graph.collecting_data").to_string()).style(Style::default().fg(Color::DarkGray)); f.render_widget(placeholder, inner); return; } @@ -1539,7 +1530,7 @@ fn draw_tcp_counters(f: &mut Frame, app: &App, area: Rect) { let out_of_order = stats.total_tcp_out_of_order.load(Ordering::Relaxed); let fast_retransmits = stats.total_tcp_fast_retransmits.load(Ordering::Relaxed); - let block = Block::default().borders(Borders::ALL).title("TCP Counters"); + let block = Block::default().borders(Borders::ALL).title(t!("graph.tcp_counters").to_string()); let inner = block.inner(area); f.render_widget(block, area); @@ -1647,12 +1638,12 @@ fn draw_tcp_states(f: &mut Frame, connections: &[Connection], area: Rect) { .filter_map(|&name| state_counts.get(name).map(|&count| (name, count))) .collect(); - let block = Block::default().borders(Borders::ALL).title("TCP States"); + let block = Block::default().borders(Borders::ALL).title(t!("graph.tcp_states").to_string()); let inner = block.inner(area); f.render_widget(block, area); if states.is_empty() { - let text = Paragraph::new("No TCP connections").style(Style::default().fg(Color::DarkGray)); + let text = Paragraph::new(t!("graph.no_tcp_connections").to_string()).style(Style::default().fg(Color::DarkGray)); f.render_widget(text, inner); return; } @@ -1705,11 +1696,11 @@ fn draw_connection_details( dns_resolver: Option<&DnsResolver>, ) -> Result<()> { if connections.is_empty() { - let text = Paragraph::new("No connections available") + let text = Paragraph::new(t!("details.no_connections").to_string()) .block( Block::default() .borders(Borders::ALL) - .title("Connection Details"), + .title(t!("details.title").to_string()), ) .style(Style::default().fg(Color::Red)) .alignment(ratatui::layout::Alignment::Center); @@ -1976,7 +1967,7 @@ fn draw_connection_details( Color::Red }; details_text.push(Line::from(vec![ - Span::styled("Initial RTT: ", Style::default().fg(Color::Yellow)), + Span::styled(format!("{} ", t!("details.initial_rtt")), Style::default().fg(Color::Yellow)), Span::styled(format!("{:.1}ms", rtt_ms), Style::default().fg(rtt_color)), ])); } @@ -1985,7 +1976,7 @@ fn draw_connection_details( .block( Block::default() .borders(Borders::ALL) - .title("Connection Information"), + .title(t!("details.info_title").to_string()), ) .style(Style::default()) .wrap(Wrap { trim: true }); @@ -1995,27 +1986,27 @@ fn draw_connection_details( // Traffic details let traffic_text: Vec = vec![ Line::from(vec![ - Span::styled("Bytes Sent: ", Style::default().fg(Color::Yellow)), + Span::styled(format!("{} ", t!("details.bytes_sent")), Style::default().fg(Color::Yellow)), Span::raw(format_bytes(conn.bytes_sent)), ]), Line::from(vec![ - Span::styled("Bytes Received: ", Style::default().fg(Color::Yellow)), + Span::styled(format!("{} ", t!("details.bytes_received")), Style::default().fg(Color::Yellow)), Span::raw(format_bytes(conn.bytes_received)), ]), Line::from(vec![ - Span::styled("Packets Sent: ", Style::default().fg(Color::Yellow)), + Span::styled(format!("{} ", t!("details.packets_sent")), Style::default().fg(Color::Yellow)), Span::raw(conn.packets_sent.to_string()), ]), Line::from(vec![ - Span::styled("Packets Received: ", Style::default().fg(Color::Yellow)), + Span::styled(format!("{} ", t!("details.packets_received")), Style::default().fg(Color::Yellow)), Span::raw(conn.packets_received.to_string()), ]), Line::from(vec![ - Span::styled("Current Rate (In): ", Style::default().fg(Color::Yellow)), + Span::styled(format!("{} ", t!("details.current_rate_in")), Style::default().fg(Color::Yellow)), Span::raw(format_rate(conn.current_incoming_rate_bps)), ]), Line::from(vec![ - Span::styled("Current Rate (Out): ", Style::default().fg(Color::Yellow)), + Span::styled(format!("{} ", t!("details.current_rate_out")), Style::default().fg(Color::Yellow)), Span::raw(format_rate(conn.current_outgoing_rate_bps)), ]), ]; @@ -2024,7 +2015,7 @@ fn draw_connection_details( .block( Block::default() .borders(Borders::ALL) - .title("Traffic Statistics"), + .title(t!("details.traffic_title").to_string()), ) .style(Style::default()) .wrap(Wrap { trim: true }); @@ -2039,21 +2030,21 @@ fn draw_help(f: &mut Frame, area: Rect) -> Result<()> { let help_text: Vec = vec![ Line::from(vec![ Span::styled( - "RustNet Monitor ", + format!("{} ", t!("help.title")), Style::default() .fg(Color::Green) .add_modifier(Modifier::BOLD), ), - Span::raw("- Network Connection Monitor"), + Span::raw(format!("- {}", t!("help.subtitle"))), ]), Line::from(""), Line::from(vec![ Span::styled("q ", Style::default().fg(Color::Yellow)), - Span::raw("Quit application (press twice to confirm)"), + Span::raw(t!("help.keys.quit").to_string()), ]), Line::from(vec![ Span::styled("Ctrl+C ", Style::default().fg(Color::Yellow)), - Span::raw("Quit immediately"), + Span::raw(t!("help.keys.quit_immediate").to_string()), ]), Line::from(vec![ Span::styled("x ", Style::default().fg(Color::Yellow)), @@ -2061,142 +2052,146 @@ fn draw_help(f: &mut Frame, area: Rect) -> Result<()> { ]), Line::from(vec![ Span::styled("Tab ", Style::default().fg(Color::Yellow)), - Span::raw("Switch between tabs"), + Span::raw(t!("help.keys.switch_tabs").to_string()), ]), Line::from(vec![ Span::styled("↑/k, ↓/j ", Style::default().fg(Color::Yellow)), - Span::raw("Navigate connections (wraps around)"), + Span::raw(t!("help.keys.navigate").to_string()), ]), Line::from(vec![ Span::styled("g, G ", Style::default().fg(Color::Yellow)), - Span::raw("Jump to first/last connection (vim-style)"), + Span::raw(t!("help.keys.jump_first_last").to_string()), ]), Line::from(vec![ Span::styled("Page Up/Down ", Style::default().fg(Color::Yellow)), - Span::raw("Navigate connections by page"), + Span::raw(t!("help.keys.page_navigate").to_string()), ]), Line::from(vec![ Span::styled("c ", Style::default().fg(Color::Yellow)), - Span::raw("Copy remote address to clipboard"), + Span::raw(t!("help.keys.copy_address").to_string()), ]), Line::from(vec![ Span::styled("p ", Style::default().fg(Color::Yellow)), - Span::raw("Toggle between service names and port numbers"), + Span::raw(t!("help.keys.toggle_ports").to_string()), ]), Line::from(vec![ Span::styled("d ", Style::default().fg(Color::Yellow)), - Span::raw("Toggle between hostnames and IP addresses (when --resolve-dns)"), + Span::raw(t!("help.keys.toggle_hostnames").to_string()), ]), Line::from(vec![ Span::styled("s ", Style::default().fg(Color::Yellow)), - Span::raw("Cycle through sort columns (Bandwidth, Process, etc.)"), + Span::raw(t!("help.keys.cycle_sort").to_string()), ]), Line::from(vec![ Span::styled("S ", Style::default().fg(Color::Yellow)), - Span::raw("Toggle sort direction (ascending/descending)"), + Span::raw(t!("help.keys.toggle_sort_dir").to_string()), ]), Line::from(vec![ Span::styled("Enter ", Style::default().fg(Color::Yellow)), - Span::raw("View connection details"), + Span::raw(t!("help.keys.view_details").to_string()), ]), Line::from(vec![ Span::styled("Esc ", Style::default().fg(Color::Yellow)), - Span::raw("Return to overview"), + Span::raw(t!("help.keys.return_overview").to_string()), ]), Line::from(vec![ Span::styled("h ", Style::default().fg(Color::Yellow)), - Span::raw("Toggle this help screen"), + Span::raw(t!("help.keys.toggle_help").to_string()), ]), Line::from(vec![ Span::styled("i ", Style::default().fg(Color::Yellow)), - Span::raw("Toggle interface statistics view"), + Span::raw(t!("help.keys.toggle_interfaces").to_string()), ]), Line::from(vec![ Span::styled("/ ", Style::default().fg(Color::Yellow)), - Span::raw("Enter filter mode (navigate while typing!)"), + Span::raw(t!("help.keys.filter_mode").to_string()), + ]), + Line::from(vec![ + Span::styled("x ", Style::default().fg(Color::Yellow)), + Span::raw(t!("help.keys.clear_connections").to_string()), ]), Line::from(""), Line::from(vec![Span::styled( - "Tabs:", + t!("help.sections.tabs").to_string(), Style::default() .fg(Color::Cyan) .add_modifier(Modifier::BOLD), )]), Line::from(vec![ - Span::styled(" Overview ", Style::default().fg(Color::Green)), - Span::raw("Connection list with mini traffic graph"), + Span::styled(format!(" {} ", t!("tabs.overview")), Style::default().fg(Color::Green)), + Span::raw(t!("help.sections.overview_desc").to_string()), ]), Line::from(vec![ - Span::styled(" Details ", Style::default().fg(Color::Green)), - Span::raw("Full details for selected connection"), + Span::styled(format!(" {} ", t!("tabs.details")), Style::default().fg(Color::Green)), + Span::raw(t!("help.sections.details_desc").to_string()), ]), Line::from(vec![ - Span::styled(" Interfaces ", Style::default().fg(Color::Green)), - Span::raw("Network interface statistics"), + Span::styled(format!(" {} ", t!("tabs.interfaces")), Style::default().fg(Color::Green)), + Span::raw(t!("help.sections.interfaces_desc").to_string()), ]), Line::from(vec![ - Span::styled(" Graph ", Style::default().fg(Color::Green)), - Span::raw("Traffic charts and protocol distribution"), + Span::styled(format!(" {} ", t!("tabs.graph")), Style::default().fg(Color::Green)), + Span::raw(t!("help.sections.graph_desc").to_string()), ]), Line::from(vec![ - Span::styled(" Help ", Style::default().fg(Color::Green)), - Span::raw("This help screen"), + Span::styled(format!(" {} ", t!("tabs.help")), Style::default().fg(Color::Green)), + Span::raw(t!("help.sections.help_desc").to_string()), ]), Line::from(""), Line::from(vec![Span::styled( - "Connection Colors:", + t!("help.sections.colors").to_string(), Style::default() .fg(Color::Cyan) .add_modifier(Modifier::BOLD), )]), Line::from(vec![ - Span::styled(" White ", Style::default()), - Span::raw("Active connection (< 75% of timeout)"), + Span::styled(format!(" {} ", t!("help.colors.white")), Style::default()), + Span::raw(t!("help.sections.colors_white").to_string()), ]), Line::from(vec![ - Span::styled(" Yellow ", Style::default().fg(Color::Yellow)), - Span::raw("Stale connection (75-90% of timeout)"), + Span::styled(format!(" {} ", t!("help.colors.yellow")), Style::default().fg(Color::Yellow)), + Span::raw(t!("help.sections.colors_yellow").to_string()), ]), Line::from(vec![ - Span::styled(" Red ", Style::default().fg(Color::Red)), - Span::raw("Critical - will be removed soon (> 90% of timeout)"), + Span::styled(format!(" {} ", t!("help.colors.red")), Style::default().fg(Color::Red)), + Span::raw(t!("help.sections.colors_red").to_string()), ]), Line::from(""), Line::from(vec![Span::styled( - "Filter Examples:", + t!("help.sections.filter_examples").to_string(), Style::default() .fg(Color::Cyan) .add_modifier(Modifier::BOLD), )]), Line::from(vec![ Span::styled(" /google ", Style::default().fg(Color::Green)), - Span::raw("Search for 'google' in all fields"), + Span::raw(t!("help.filter_examples.general", term = "google").to_string()), ]), Line::from(vec![ Span::styled(" /port:44 ", Style::default().fg(Color::Green)), - Span::raw("Filter ports containing '44' (443, 8080, etc.)"), + Span::raw(t!("help.filter_examples.port", term = "44").to_string()), ]), Line::from(vec![ Span::styled(" /src:192.168 ", Style::default().fg(Color::Green)), - Span::raw("Filter by source IP prefix"), + Span::raw(t!("help.filter_examples.src").to_string()), ]), Line::from(vec![ Span::styled(" /dst:github.com ", Style::default().fg(Color::Green)), - Span::raw("Filter by destination"), + Span::raw(t!("help.filter_examples.dst").to_string()), ]), Line::from(vec![ Span::styled(" /sni:example.com ", Style::default().fg(Color::Green)), - Span::raw("Filter by SNI hostname"), + Span::raw(t!("help.filter_examples.sni").to_string()), ]), Line::from(vec![ Span::styled(" /process:firefox ", Style::default().fg(Color::Green)), - Span::raw("Filter by process name"), + Span::raw(t!("help.filter_examples.process").to_string()), ]), Line::from(""), ]; let help = Paragraph::new(help_text) - .block(Block::default().borders(Borders::ALL).title("Help")) + .block(Block::default().borders(Borders::ALL).title(t!("tabs.help").to_string())) .style(Style::default()) .wrap(Wrap { trim: true }) .alignment(ratatui::layout::Alignment::Left); @@ -2226,11 +2221,11 @@ fn draw_interface_stats(f: &mut Frame, app: &crate::app::App, area: Rect) -> Res } if stats.is_empty() { - let empty_msg = Paragraph::new("No interface statistics available yet...") + let empty_msg = Paragraph::new(t!("interface_stats.no_stats").to_string()) .block( Block::default() .borders(Borders::ALL) - .title(" Interface Statistics "), + .title(format!(" {} ", t!("tabs.interfaces"))), ) .style(Style::default().fg(Color::Gray)) .alignment(ratatui::layout::Alignment::Center); @@ -2301,16 +2296,16 @@ fn draw_interface_stats(f: &mut Frame, app: &crate::app::App, area: Rect) -> Res ) .header( Row::new(vec![ - "Interface", - "RX Rate", - "TX Rate", - "RX Packets", - "TX Packets", - "RX Err", - "TX Err", - "RX Drop", - "TX Drop", - "Collisions", + t!("interface_stats.headers.interface").to_string(), + t!("interface_stats.headers.rx_rate").to_string(), + t!("interface_stats.headers.tx_rate").to_string(), + t!("interface_stats.headers.rx_packets").to_string(), + t!("interface_stats.headers.tx_packets").to_string(), + t!("interface_stats.headers.rx_err").to_string(), + t!("interface_stats.headers.tx_err").to_string(), + t!("interface_stats.headers.rx_drop").to_string(), + t!("interface_stats.headers.tx_drop").to_string(), + t!("interface_stats.headers.collisions").to_string(), ]) .style( Style::default() @@ -2321,7 +2316,7 @@ fn draw_interface_stats(f: &mut Frame, app: &crate::app::App, area: Rect) -> Res .block( Block::default() .borders(Borders::ALL) - .title(" Interface Statistics (Press 'i' to toggle) "), + .title(format!(" {} ", t!("interface_stats.full_title"))), ) .style(Style::default()); @@ -2333,9 +2328,9 @@ fn draw_interface_stats(f: &mut Frame, app: &crate::app::App, area: Rect) -> Res /// Draw filter input area fn draw_filter_input(f: &mut Frame, ui_state: &UIState, area: Rect) { let title = if ui_state.filter_mode { - "Filter (↑↓/jk to navigate, Enter to confirm, Esc to cancel)" + t!("filter.title_active").to_string() } else { - "Active Filter (Press Esc to clear)" + t!("filter.title_applied").to_string() }; let input_text = if ui_state.filter_mode { @@ -2366,29 +2361,20 @@ fn draw_filter_input(f: &mut Frame, ui_state: &UIState, area: Rect) { /// Draw status bar fn draw_status_bar(f: &mut Frame, ui_state: &UIState, connection_count: usize, area: Rect) { let status = if ui_state.quit_confirmation { - " Press 'q' again to quit or any other key to cancel ".to_string() + format!(" {} ", t!("status.quit_confirm")) } else if ui_state.clear_confirmation { - " Press 'x' again to clear all connections or any other key to cancel ".to_string() + format!(" {} ", t!("status.clear_confirm")) } else if let Some((ref msg, ref time)) = ui_state.clipboard_message { // Show clipboard message for 3 seconds if time.elapsed().as_secs() < 3 { format!(" {} ", msg) } else { - format!( - " Press 'h' for help | 'c' to copy address | Connections: {} ", - connection_count - ) + format!(" {} ", t!("status.help_hint", count = connection_count)) } } else if !ui_state.filter_query.is_empty() { - format!( - " 'h' help | Tab/Shift+Tab switch tabs | Showing {} filtered connections (Esc to clear) ", - connection_count - ) + format!(" {} ", t!("status.filter_active", count = connection_count)) } else { - format!( - " 'h' help | Tab/Shift+Tab switch tabs | '/' filter | 'c' copy | Connections: {} ", - connection_count - ) + format!(" {} ", t!("status.default", count = connection_count)) }; let style = if ui_state.quit_confirmation || ui_state.clear_confirmation { @@ -2430,11 +2416,11 @@ fn draw_loading_screen(f: &mut Frame) { Line::from(""), Line::from(vec![ Span::styled("⣾ ", Style::default().fg(Color::Yellow)), - Span::styled("Loading network connections...", Style::default()), + Span::styled(t!("loading.message").to_string(), Style::default()), ]), Line::from(""), Line::from(vec![Span::styled( - "This may take a few seconds", + t!("loading.subtitle").to_string(), Style::default().fg(Color::DarkGray), )]), ]; @@ -2444,7 +2430,7 @@ fn draw_loading_screen(f: &mut Frame) { .block( Block::default() .borders(Borders::ALL) - .title("RustNet Monitor"), + .title(t!("loading.title").to_string()), ); f.render_widget(loading_paragraph, chunks[1]);