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.
This commit is contained in:
Marco Cadetg
2025-12-24 16:15:22 +01:00
parent a481214c62
commit 092c53fdd0
19 changed files with 2025 additions and 242 deletions

View File

@@ -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

278
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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
<details>
<summary><b>eBPF Enhanced Process Identification (Linux Default)</b></summary>

View File

@@ -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)

View File

@@ -85,6 +85,7 @@ Options:
-l, --log-level <LEVEL> Set the log level (if not provided, no logging will be enabled)
--json-log <FILE> Enable JSON logging of connection events to specified file
-f, --bpf-filter <FILTER> BPF filter expression for packet capture
--lang <LOCALE> 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 <LOCALE>`
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

221
assets/locales/de.yml Normal file
View File

@@ -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

223
assets/locales/en.yml Normal file
View File

@@ -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

220
assets/locales/es.yml Normal file
View File

@@ -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

221
assets/locales/fr.yml Normal file
View File

@@ -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

221
assets/locales/ru.yml Normal file
View File

@@ -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

221
assets/locales/zh.yml Normal file
View File

@@ -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

View File

@@ -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()?;

84
scripts/check-locales.py Executable file
View File

@@ -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())

View File

@@ -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"),
);

View File

@@ -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;

View File

@@ -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<String> = 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::<String>("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

View File

@@ -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<PrivilegeStatus> {
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<PrivilegeStatus> {
}
// 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<PrivilegeStatus> {
|| 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<PrivilegeStatus> {
}
// 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))

346
src/ui.rs
View File

@@ -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::<Vec<_>>())
.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<Line> = 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<Line> = 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<Line> = 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]);