mirror of
https://github.com/domcyrus/rustnet.git
synced 2026-01-04 13:00:04 -06:00
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:
10
.github/workflows/rust.yml
vendored
10
.github/workflows/rust.yml
vendored
@@ -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
278
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
32
USAGE.md
32
USAGE.md
@@ -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
221
assets/locales/de.yml
Normal 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
223
assets/locales/en.yml
Normal 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
220
assets/locales/es.yml
Normal 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
221
assets/locales/fr.yml
Normal 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
221
assets/locales/ru.yml
Normal 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
221
assets/locales/zh.yml
Normal 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
|
||||
5
build.rs
5
build.rs
@@ -1,7 +1,12 @@
|
||||
use anyhow::Result;
|
||||
use std::{env, fs::File, path::PathBuf};
|
||||
|
||||
// Initialize i18n for CLI help text generation (shell completions and manpages)
|
||||
rust_i18n::i18n!("assets/locales", fallback = "en");
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Set locale for build script (uses English for generated assets)
|
||||
rust_i18n::set_locale("en");
|
||||
// Generate shell completions and manpage
|
||||
generate_assets()?;
|
||||
|
||||
|
||||
84
scripts/check-locales.py
Executable file
84
scripts/check-locales.py
Executable 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())
|
||||
67
src/cli.rs
67
src/cli.rs
@@ -1,41 +1,53 @@
|
||||
use clap::{Arg, Command};
|
||||
use rust_i18n::t;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const INTERFACE_HELP: &str = "Network interface to monitor (use \"any\" to capture all interfaces)";
|
||||
/// Get the interface help text (platform-specific)
|
||||
fn interface_help() -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
t!("cli.interface_help_linux").to_string()
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
t!("cli.interface_help").to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
const INTERFACE_HELP: &str = "Network interface to monitor";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const BPF_HELP: &str = "BPF filter expression for packet capture (e.g., \"tcp port 443\"). Note: Using a BPF filter disables PKTAP (process info falls back to lsof)";
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
const BPF_HELP: &str =
|
||||
"BPF filter expression for packet capture (e.g., \"tcp port 443\", \"dst port 80\")";
|
||||
/// Get the BPF filter help text (platform-specific)
|
||||
fn bpf_help() -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
t!("cli.bpf_help_macos").to_string()
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
t!("cli.bpf_help").to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_cli() -> Command {
|
||||
let cmd = Command::new("rustnet")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.author("Network Monitor")
|
||||
.about("Cross-platform network monitoring tool")
|
||||
.about(t!("cli.about").to_string())
|
||||
.arg(
|
||||
Arg::new("interface")
|
||||
.short('i')
|
||||
.long("interface")
|
||||
.value_name("INTERFACE")
|
||||
.help(INTERFACE_HELP)
|
||||
.help(interface_help())
|
||||
.required(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-localhost")
|
||||
.long("no-localhost")
|
||||
.help("Filter out localhost connections")
|
||||
.help(t!("cli.no_localhost_help").to_string())
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("show-localhost")
|
||||
.long("show-localhost")
|
||||
.help("Show localhost connections (overrides default filtering)")
|
||||
.help(t!("cli.show_localhost_help").to_string())
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
@@ -43,7 +55,7 @@ pub fn build_cli() -> Command {
|
||||
.short('r')
|
||||
.long("refresh-interval")
|
||||
.value_name("MILLISECONDS")
|
||||
.help("UI refresh interval in milliseconds")
|
||||
.help(t!("cli.refresh_interval_help").to_string())
|
||||
.value_parser(clap::value_parser!(u64))
|
||||
.default_value("1000")
|
||||
.required(false),
|
||||
@@ -51,7 +63,7 @@ pub fn build_cli() -> Command {
|
||||
.arg(
|
||||
Arg::new("no-dpi")
|
||||
.long("no-dpi")
|
||||
.help("Disable deep packet inspection")
|
||||
.help(t!("cli.no_dpi_help").to_string())
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
@@ -59,14 +71,14 @@ pub fn build_cli() -> Command {
|
||||
.short('l')
|
||||
.long("log-level")
|
||||
.value_name("LEVEL")
|
||||
.help("Set the log level (if not provided, no logging will be enabled)")
|
||||
.help(t!("cli.log_level_help").to_string())
|
||||
.required(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("json-log")
|
||||
.long("json-log")
|
||||
.value_name("FILE")
|
||||
.help("Enable JSON logging of connection events to specified file")
|
||||
.help(t!("cli.json_log_help").to_string())
|
||||
.required(false),
|
||||
)
|
||||
.arg(
|
||||
@@ -74,20 +86,27 @@ pub fn build_cli() -> Command {
|
||||
.short('f')
|
||||
.long("bpf-filter")
|
||||
.value_name("FILTER")
|
||||
.help(BPF_HELP)
|
||||
.help(bpf_help())
|
||||
.required(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("resolve-dns")
|
||||
.long("resolve-dns")
|
||||
.help("Enable reverse DNS resolution for IP addresses (shows hostnames instead of IPs)")
|
||||
.help(t!("cli.resolve_dns_help").to_string())
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("show-ptr-lookups")
|
||||
.long("show-ptr-lookups")
|
||||
.help("Show PTR lookup connections in UI (hidden by default when --resolve-dns is enabled)")
|
||||
.help(t!("cli.show_ptr_lookups_help").to_string())
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("lang")
|
||||
.long("lang")
|
||||
.value_name("LOCALE")
|
||||
.help(t!("cli.lang_help").to_string())
|
||||
.required(false),
|
||||
);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -95,13 +114,13 @@ pub fn build_cli() -> Command {
|
||||
.arg(
|
||||
Arg::new("no-sandbox")
|
||||
.long("no-sandbox")
|
||||
.help("Disable Landlock sandboxing")
|
||||
.help(t!("cli.no_sandbox_help").to_string())
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("sandbox-strict")
|
||||
.long("sandbox-strict")
|
||||
.help("Require full sandbox enforcement or exit")
|
||||
.help(t!("cli.sandbox_strict_help").to_string())
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
.conflicts_with("no-sandbox"),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
54
src/main.rs
54
src/main.rs
@@ -8,20 +8,72 @@ use std::io;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
// Initialize i18n for the binary crate
|
||||
rust_i18n::i18n!("assets/locales", fallback = "en");
|
||||
|
||||
mod app;
|
||||
mod cli;
|
||||
mod filter;
|
||||
mod network;
|
||||
mod ui;
|
||||
|
||||
/// Detect locale from environment, CLI args, or system settings (before CLI parsing).
|
||||
///
|
||||
/// NOTE: This function manually parses `--lang` from argv BEFORE clap parses arguments.
|
||||
/// This is intentional: we need the locale set before `build_cli()` is called so that
|
||||
/// clap's help text (`--help`) can be displayed in the user's language. This duplicates
|
||||
/// some of clap's parsing logic, but is necessary for proper i18n of CLI help messages.
|
||||
fn detect_locale_early() -> String {
|
||||
// Check RUSTNET_LANG environment variable first
|
||||
if let Ok(lang) = std::env::var("RUSTNET_LANG") {
|
||||
return lang;
|
||||
}
|
||||
|
||||
// Check for --lang argument in argv (before full CLI parsing)
|
||||
// This allows help text to be translated correctly
|
||||
let args: Vec<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
|
||||
|
||||
@@ -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
346
src/ui.rs
@@ -1,3 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use anyhow::Result;
|
||||
use ratatui::{
|
||||
Frame, Terminal as RatatuiTerminal,
|
||||
@@ -10,6 +12,7 @@ use ratatui::{
|
||||
Tabs, Wrap,
|
||||
},
|
||||
};
|
||||
use rust_i18n::t;
|
||||
|
||||
use crate::app::{App, AppStats};
|
||||
use crate::network::dns::DnsResolver;
|
||||
@@ -68,18 +71,21 @@ impl SortColumn {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the display name for the sort column
|
||||
pub fn display_name(self) -> &'static str {
|
||||
/// Get the display name for the sort column.
|
||||
///
|
||||
/// Returns `Cow<'static, str>` to avoid allocations when using English locale
|
||||
/// (the `t!()` macro returns the static string directly in that case).
|
||||
pub fn display_name(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
Self::CreatedAt => "Time",
|
||||
Self::BandwidthTotal => "Bandwidth Total",
|
||||
Self::Process => "Process",
|
||||
Self::LocalAddress => "Local Addr",
|
||||
Self::RemoteAddress => "Remote Addr",
|
||||
Self::Application => "Application",
|
||||
Self::Service => "Service",
|
||||
Self::State => "State",
|
||||
Self::Protocol => "Protocol",
|
||||
Self::CreatedAt => t!("sort.time"),
|
||||
Self::BandwidthTotal => t!("sort.bandwidth"),
|
||||
Self::Process => t!("sort.process"),
|
||||
Self::LocalAddress => t!("sort.local_addr"),
|
||||
Self::RemoteAddress => t!("sort.remote_addr"),
|
||||
Self::Application => t!("sort.application"),
|
||||
Self::Service => t!("sort.service"),
|
||||
Self::State => t!("sort.state"),
|
||||
Self::Protocol => t!("sort.protocol"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -438,18 +444,18 @@ pub fn draw(
|
||||
/// Draw mode tabs
|
||||
fn draw_tabs(f: &mut Frame, ui_state: &UIState, area: Rect) {
|
||||
let titles = vec![
|
||||
Span::styled("Overview", Style::default().fg(Color::Green)),
|
||||
Span::styled("Details", Style::default().fg(Color::Green)),
|
||||
Span::styled("Interfaces", Style::default().fg(Color::Green)),
|
||||
Span::styled("Graph", Style::default().fg(Color::Green)),
|
||||
Span::styled("Help", Style::default().fg(Color::Green)),
|
||||
Span::styled(t!("tabs.overview").to_string(), Style::default().fg(Color::Green)),
|
||||
Span::styled(t!("tabs.details").to_string(), Style::default().fg(Color::Green)),
|
||||
Span::styled(t!("tabs.interfaces").to_string(), Style::default().fg(Color::Green)),
|
||||
Span::styled(t!("tabs.graph").to_string(), Style::default().fg(Color::Green)),
|
||||
Span::styled(t!("tabs.help").to_string(), Style::default().fg(Color::Green)),
|
||||
];
|
||||
|
||||
let tabs = Tabs::new(titles.into_iter().map(Line::from).collect::<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]);
|
||||
|
||||
Reference in New Issue
Block a user