feat(i18n): add multi-language support (ja/ko/vi/ru/zh-TW) with system auto-detection

- Extend AppLanguage to 7 locales: zh-CN, zh-TW, en, ja, ko, vi, ru
- Auto-detect system language via navigator.languages; fallback to English
- Add BCP47 locale mapping for date formatting and sorting
- Create 50 translation JSON files (5 languages × 10 namespaces, 1137 keys each)
- Update language selector with native language names
- Extend locale key parity test to cover all 7 languages (70 assertions)
- Add VNC/RDP desktop, serial terminal, AI agent, port forwarding, and SFTP features
- Add credential encryption, host metrics, session backup crypto, and zmodem bridge
- Update Rust backend: desktop services, host metrics, SSH/SFTP enhancements
This commit is contained in:
Logan.Li committed 2026-08-12 00:07:48 +08:00
1 parent fea6d8db04
commit b0d0e3a5f3
301 files changed
+28002 -2705

No files matched your search

+79 -12
View File
@@ -25,6 +25,11 @@ use model::{descriptor, ConnectionGuard, TunnelCounters, TunnelRuntime};
use relay::relay_channel;
use socks5::read_target;
enum IdClaim {
Fresh(String),
Existing(ForwardStatus),
}
/// Owns forwarding listeners independently from persisted forwarding rules.
/// A rule may exist without a runtime; a runtime always has a cancellable id.
#[derive(Default)]
@@ -43,7 +48,10 @@ impl TunnelManager {
remote_port: u16,
) -> Result<ForwardStatus, String> {
validate_target(&remote_host, remote_port)?;
let forward_id = self.available_id(forward_id)?;
let forward_id = match self.claim_id(forward_id, connection_id, "local")? {
IdClaim::Existing(status) => return Ok(status),
IdClaim::Fresh(id) => id,
};
let listener = TcpListener::bind(("127.0.0.1", local_port))
.map_err(|error| format!("监听本地端口失败: {error}"))?;
listener
@@ -119,7 +127,10 @@ impl TunnelManager {
config: SshConnectRequest,
local_port: u16,
) -> Result<ForwardStatus, String> {
let forward_id = self.available_id(forward_id)?;
let forward_id = match self.claim_id(forward_id, connection_id, "dynamic")? {
IdClaim::Existing(status) => return Ok(status),
IdClaim::Fresh(id) => id,
};
let listener = TcpListener::bind(("127.0.0.1", local_port))
.map_err(|error| format!("监听 SOCKS5 端口失败: {error}"))?;
listener
@@ -216,7 +227,10 @@ impl TunnelManager {
local_port: u16,
) -> Result<ForwardStatus, String> {
validate_target(&local_host, local_port)?;
let forward_id = self.available_id(forward_id)?;
let forward_id = match self.claim_id(forward_id, connection_id, "remote")? {
IdClaim::Existing(status) => return Ok(status),
IdClaim::Fresh(id) => id,
};
let session = establish_session(&config)?;
let (mut listener, bound_port) = session
.channel_forward_listen(remote_port, None, None)
@@ -322,14 +336,36 @@ impl TunnelManager {
Ok(statuses)
}
fn available_id(&self, requested: Option<String>) -> Result<String, String> {
/// Claim a forward id for start.
/// - Same connection + kind already running → return existing status (idempotent).
/// - Same id owned by another connection (or different kind) → stop stale runtime and reuse.
/// Session-scoped rules keep a stable id across reconnects; orphans must not block start.
fn claim_id(
&self,
requested: Option<String>,
connection_id: &str,
kind: &str,
) -> Result<IdClaim, String> {
let id = requested
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| format!("forward-{}", Uuid::new_v4()));
if self.runtimes.lock().map_err(lock_error)?.contains_key(&id) {
return Err(format!("端口转发已在运行: {id}"));
let stale = {
let mut runtimes = self.runtimes.lock().map_err(lock_error)?;
match runtimes.get(&id) {
Some(runtime)
if runtime.descriptor.connection_id == connection_id
&& runtime.descriptor.kind == kind =>
{
return Ok(IdClaim::Existing(runtime.snapshot()));
}
Some(_) => runtimes.remove(&id),
None => None,
}
};
if let Some(runtime) = stale {
runtime.stop();
}
Ok(id)
Ok(IdClaim::Fresh(id))
}
fn register(&self, id: String, runtime: TunnelRuntime) -> Result<ForwardStatus, String> {
@@ -408,9 +444,9 @@ mod tests {
}
#[test]
fn duplicate_runtime_id_is_rejected() {
fn duplicate_runtime_id_is_idempotent_for_same_connection() {
let manager = TunnelManager::default();
manager
let first = manager
.start_dynamic(
Some("same-id".to_string()),
"connection-test",
@@ -418,15 +454,46 @@ mod tests {
0,
)
.unwrap();
let error = manager
let second = manager
.start_dynamic(
Some("same-id".to_string()),
"connection-test",
unreachable_config(),
0,
)
.unwrap_err();
assert!(error.contains("已在运行"));
.unwrap();
assert_eq!(first.forward_id, second.forward_id);
assert_eq!(first.bound_port, second.bound_port);
assert_eq!(manager.list(Some("connection-test")).unwrap().len(), 1);
manager.stop_connection("connection-test").unwrap();
}
#[test]
fn stale_runtime_id_from_other_connection_is_reclaimed() {
let manager = TunnelManager::default();
let first = manager
.start_dynamic(
Some("shared-rule".to_string()),
"connection-old",
unreachable_config(),
0,
)
.unwrap();
let second = manager
.start_dynamic(
Some("shared-rule".to_string()),
"connection-new",
unreachable_config(),
0,
)
.unwrap();
assert_eq!(second.forward_id, "shared-rule");
assert_eq!(second.connection_id, "connection-new");
assert!(manager.list(Some("connection-old")).unwrap().is_empty());
assert_eq!(manager.list(Some("connection-new")).unwrap().len(), 1);
// Old listener was stopped; new bind may reuse the port or pick another (port 0).
assert_ne!(second.bound_port, 0);
let _ = first;
manager.stop_connection("connection-new").unwrap();
}
}