diff --git a/gpui-app/locales/en.yml b/gpui-app/locales/en.yml index 0b2a1e9..1dc2275 100644 --- a/gpui-app/locales/en.yml +++ b/gpui-app/locales/en.yml @@ -4,14 +4,18 @@ "aliyun.auth_token_mode": "Group token" "aliyun.auth_triple_mode": "Per-device secret" "aliyun.build_failed": "Failed to build connection: {e}" +"aliyun.default_preset_name": "Alibaba Cloud device" "aliyun.device_id": "Device ID" +"aliyun.device_id_required": "Device ID cannot be empty" "aliyun.device_secret": "DeviceSecret (per-device)" "aliyun.device_secret_ph": "Triple: DeviceSecret" "aliyun.generate_connect": "Generate & connect" "aliyun.group": "Group (optional)" "aliyun.group_id": "Group ID" "aliyun.help": "Token mode (Alibaba Cloud MQTT): endpoint {实例ID}.mqtt.aliyuncs.com, clientId={GroupId}@@@{设备ID}, password is the HMAC-SHA1 signature. Per-device secret (IoT Platform): endpoint {ProductKey}.iot-as-mqtt.{地域}.aliyuncs.com, signed per the official triple spec." +"aliyun.hmac_init_failed": "HMAC init failed: {e}" "aliyun.instance_id": "Instance ID" +"aliyun.instance_id_required": "Instance ID cannot be empty" "aliyun.preset_name": "Preset name" "aliyun.product_key": "ProductKey (per-device)" "aliyun.product_key_ph": "Triple: ProductKey" @@ -20,6 +24,9 @@ "aliyun.save_preset": "Save preset" "aliyun.saved_presets": "Saved presets" "aliyun.title": "Alibaba Cloud IoT" +"aliyun.token_required_fields": "Group ID, AccessKey ID and AccessKey Secret cannot be empty" +"aliyun.triple_requires_device_name": "Per-device secret mode requires DeviceName" +"aliyun.triple_requires_pk_ds": "Per-device secret mode requires ProductKey and DeviceSecret" "color.blue": "Blue" "color.cyan": "Cyan" "color.green": "Green" @@ -57,6 +64,43 @@ "encode.hex_failed": "Hex decode failed: {e}" "encode.hex_invalid": "Hex decode failed: non-hex character present" "encode.hex_odd": "Hex length must be even (two chars per byte)" +"engine.cancelled": "Connection cancelled" +"engine.connack_rejected": "Connection rejected: {code}" +"engine.connack_rejected_code": "Connection rejected: {code} (code {n})" +"engine.connect_failed": "Connect failed: {e}" +"engine.connect_ok": "Connected (session_present: {sp})" +"engine.connecting": "Connecting to {host}:{port} ({protocol}, {transport})" +"engine.connection_error": "Connection error: {e}" +"engine.connection_not_found": "Connection not found" +"engine.disconnect_received": "DISCONNECT received: {code} (code {n})" +"engine.disconnected_by_server": "Disconnected by server" +"engine.disconnected_by_server_active": "Server closed the connection" +"engine.disconnected_by_server_code": "Disconnected by server: {code}" +"engine.events_dropped": "UI cannot keep up: dropped {n} high-frequency events (messages/logs) so far" +"engine.internal_task_failed": "Internal task failed: {e}" +"engine.puback": "Publish acknowledged (packet {n})" +"engine.puback_reason": "Publish acknowledged (packet {n}, reason {reason})" +"engine.pubcomp": "QoS2 publish complete (packet {n})" +"engine.publish_failed": "Publish failed: {e}" +"engine.publish_no_connection": "Publish failed: connection not found" +"engine.publish_outgoing": "Message sent (packet {n})" +"engine.publish_sent": "Published to {topic} (QoS {qos}, retain={retain})" +"engine.pubrec": "QoS2 publish received (packet {n})" +"engine.received_message": "Received: {topic} ({n} bytes)" +"engine.received_message_qos": "Received: {topic} ({n} bytes, QoS {qos})" +"engine.reconnect_give_up": "Reconnect failed {attempts} times, reached the max of {max}, giving up: {e}" +"engine.reconnect_retry": "Connection error, retrying in {delay}s (attempt {attempts}): {e}" +"engine.suback": "Subscribe confirmed (packet {n})" +"engine.suback_codes": "Subscribe confirmed (packet {n}): {codes}" +"engine.subscribe_rejected": "Subscribe rejected: {e}" +"engine.subscribe_request_failed": "Subscribe request failed: {e}" +"engine.subscribe_topic": "Subscribing: {topic} (QoS {qos})" +"engine.test_timeout": "Connection test timed out ({secs}s) — check the host and port" +"engine.unsuback": "Unsubscribe confirmed" +"engine.unsuback_packet": "Unsubscribe confirmed (packet {n})" +"engine.unsubscribe_failed": "Unsubscribe failed: {e}" +"engine.unsubscribe_ok": "Unsubscribed: {topic}" +"engine.user_disconnect": "Disconnected by user request" "form.auth_section": "Auth & Keep-alive" "form.auto_connect": "Connect on app start" "form.auto_reconnect": "Auto reconnect on disconnect" @@ -312,6 +356,19 @@ "titlebar.switch_light": "Switch to light theme" "titlebar.unpin": "Unpin window" "titlebar.variables": "Global Variables" +"tls.ca_invalid_cert": "Invalid certificate in CA file {p}: {e}" +"tls.ca_no_certs": "No certificate found in CA file {p}" +"tls.ca_parse_failed": "Failed to parse CA file {p}: {e}" +"tls.ca_read_failed": "Failed to read CA file {p}: {e}" +"tls.cert_key_pair_required": "Client certificate and client key must be configured together" +"tls.client_auth_failed": "Failed to load client certificate/key: {e}" +"tls.client_cert_no_certs": "No certificate found in client certificate file {p}" +"tls.client_cert_parse_failed": "Failed to parse client certificate file {p}: {e}" +"tls.client_cert_read_failed": "Failed to read client certificate file {p}: {e}" +"tls.client_key_parse_failed": "Failed to parse client key file {p}: {e}" +"tls.client_key_read_failed": "Failed to read client key file {p}: {e}" +"tls.init_failed": "TLS init failed: {e}" +"tls.native_roots_failed": "Failed to load system root certificates: {detail}" "update.apply": "Update to v{v} now" "update.check": "Check for updates" "update.check_hint": "Check GitHub Releases for a new version" diff --git a/gpui-app/locales/zh-CN.yml b/gpui-app/locales/zh-CN.yml index 51a09d8..d447106 100644 --- a/gpui-app/locales/zh-CN.yml +++ b/gpui-app/locales/zh-CN.yml @@ -4,14 +4,18 @@ "aliyun.auth_token_mode": "Group 令牌 (Token)" "aliyun.auth_triple_mode": "一机一密" "aliyun.build_failed": "无法生成连接: {e}" +"aliyun.default_preset_name": "阿里云设备" "aliyun.device_id": "设备 ID" +"aliyun.device_id_required": "Device ID 不能为空" "aliyun.device_secret": "DeviceSecret(一机一密)" "aliyun.device_secret_ph": "一机一密:DeviceSecret" "aliyun.generate_connect": "生成并连接" "aliyun.group": "分组(可选)" "aliyun.group_id": "Group ID" "aliyun.help": "Token 模式(云消息队列 MQTT 版):接入点 {实例ID}.mqtt.aliyuncs.com,clientId={GroupId}@@@{设备ID},密码为 HMAC-SHA1 签名;一机一密(物联网平台):接入点 {ProductKey}.iot-as-mqtt.{地域}.aliyuncs.com,按官方三元组规范签名" +"aliyun.hmac_init_failed": "HMAC 初始化失败: {e}" "aliyun.instance_id": "实例 ID" +"aliyun.instance_id_required": "Instance ID 不能为空" "aliyun.preset_name": "预设名称" "aliyun.product_key": "ProductKey(一机一密)" "aliyun.product_key_ph": "一机一密:ProductKey" @@ -20,6 +24,9 @@ "aliyun.save_preset": "保存预设" "aliyun.saved_presets": "已保存预设" "aliyun.title": "阿里云 IoT 设备" +"aliyun.token_required_fields": "Group ID、AccessKey ID 与 AccessKey Secret 不能为空" +"aliyun.triple_requires_device_name": "一机一密需要 DeviceName(设备名)" +"aliyun.triple_requires_pk_ds": "一机一密需要 ProductKey 与 DeviceSecret (设备密钥)" "color.blue": "蓝" "color.cyan": "青" "color.green": "绿" @@ -57,6 +64,43 @@ "encode.hex_failed": "Hex 解码失败: {e}" "encode.hex_invalid": "Hex 解码失败: 含非十六进制字符" "encode.hex_odd": "Hex 长度必须为偶数(每两个字符一个字节)" +"engine.cancelled": "连接已取消" +"engine.connack_rejected": "连接被拒绝: {code}" +"engine.connack_rejected_code": "连接被拒绝: {code} (code {n})" +"engine.connect_failed": "连接失败: {e}" +"engine.connect_ok": "连接成功 (session_present: {sp})" +"engine.connecting": "正在连接 {host}:{port} ({protocol}, {transport})" +"engine.connection_error": "连接错误: {e}" +"engine.connection_not_found": "连接不存在" +"engine.disconnect_received": "收到断开连接: {code} (code {n})" +"engine.disconnected_by_server": "服务端断开" +"engine.disconnected_by_server_active": "服务端主动断开" +"engine.disconnected_by_server_code": "服务端断开: {code}" +"engine.events_dropped": "UI 处理不过来,已累计丢弃 {n} 条高频事件(消息/日志)" +"engine.internal_task_failed": "内部任务失败: {e}" +"engine.puback": "发布确认 (packet {n})" +"engine.puback_reason": "发布确认 (packet {n}, reason {reason})" +"engine.pubcomp": "QoS2 发布完成 (packet {n})" +"engine.publish_failed": "发布失败: {e}" +"engine.publish_no_connection": "发布失败: 连接不存在" +"engine.publish_outgoing": "消息已发出 (packet {n})" +"engine.publish_sent": "发布消息到 {topic} (QoS {qos}, retain={retain})" +"engine.pubrec": "QoS2 发布收到 (packet {n})" +"engine.received_message": "收到消息: {topic} ({n} 字节)" +"engine.received_message_qos": "收到消息: {topic} ({n} 字节, QoS {qos})" +"engine.reconnect_give_up": "已重连 {attempts} 次仍失败,达到最大重连次数 {max},停止重连: {e}" +"engine.reconnect_retry": "连接错误,{delay} 秒后重连(第 {attempts} 次): {e}" +"engine.suback": "订阅确认 (packet {n})" +"engine.suback_codes": "订阅确认 (packet {n}): {codes}" +"engine.subscribe_rejected": "订阅被拒绝: {e}" +"engine.subscribe_request_failed": "订阅请求失败: {e}" +"engine.subscribe_topic": "订阅主题: {topic} (QoS {qos})" +"engine.test_timeout": "连接测试超时({secs} 秒),请检查主机地址与端口" +"engine.unsuback": "取消订阅确认" +"engine.unsuback_packet": "取消订阅确认 (packet {n})" +"engine.unsubscribe_failed": "取消订阅失败: {e}" +"engine.unsubscribe_ok": "取消订阅: {topic}" +"engine.user_disconnect": "用户请求断开连接" "form.auth_section": "认证与心跳" "form.auto_connect": "启动时自动连接" "form.auto_reconnect": "断线自动重连" @@ -312,6 +356,19 @@ "titlebar.switch_light": "切换为浅色" "titlebar.unpin": "取消窗口置顶" "titlebar.variables": "全局变量" +"tls.ca_invalid_cert": "CA 文件 {p} 中存在无效证书: {e}" +"tls.ca_no_certs": "CA 文件 {p} 中没有找到任何证书" +"tls.ca_parse_failed": "解析 CA 文件 {p} 失败: {e}" +"tls.ca_read_failed": "读取 CA 文件 {p} 失败: {e}" +"tls.cert_key_pair_required": "客户端证书与客户端密钥必须同时配置" +"tls.client_auth_failed": "加载客户端证书/私钥失败: {e}" +"tls.client_cert_no_certs": "客户端证书文件 {p} 中没有找到证书" +"tls.client_cert_parse_failed": "解析客户端证书文件 {p} 失败: {e}" +"tls.client_cert_read_failed": "读取客户端证书文件 {p} 失败: {e}" +"tls.client_key_parse_failed": "解析客户端私钥文件 {p} 失败: {e}" +"tls.client_key_read_failed": "读取客户端私钥文件 {p} 失败: {e}" +"tls.init_failed": "TLS 初始化失败: {e}" +"tls.native_roots_failed": "加载系统根证书失败: {detail}" "update.apply": "立即更新到 v{v}" "update.check": "检查更新" "update.check_hint": "检查 GitHub Releases 上的新版本" diff --git a/gpui-app/src/aliyun.rs b/gpui-app/src/aliyun.rs index 066a04a..4f38b0f 100644 --- a/gpui-app/src/aliyun.rs +++ b/gpui-app/src/aliyun.rs @@ -19,6 +19,7 @@ use sha1::Sha1; use sha2::Sha256; use crate::model::{ConnectionConfig, ProtocolVersion, TransportKind}; +use crate::ui::i18n; type HmacSha1 = Hmac; type HmacSha256 = Hmac; @@ -38,8 +39,8 @@ impl AliyunAuthMode { pub fn label(self) -> &'static str { match self { - Self::Token => "Group 令牌 (Token)", - Self::DeviceCredential => "一机一密", + Self::Token => i18n::t("aliyun.auth_token_mode"), + Self::DeviceCredential => i18n::t("aliyun.auth_triple_mode"), } } } @@ -83,7 +84,7 @@ impl AliyunPreset { pub fn new() -> Self { Self { id: uuid::Uuid::new_v4().to_string(), - name: "阿里云设备".into(), + name: i18n::t("aliyun.default_preset_name").into(), group: String::new(), instance_id: String::new(), region: String::new(), @@ -99,16 +100,16 @@ impl AliyunPreset { /// HMAC-SHA1(secret, client_id) 的 Base64 编码(Token 模式签名)。 pub fn signature(secret: &str, client_id: &str) -> Result { - let mut mac = - HmacSha1::new_from_slice(secret.as_bytes()).map_err(|e| format!("HMAC 初始化失败: {e}"))?; + let mut mac = HmacSha1::new_from_slice(secret.as_bytes()) + .map_err(|e| i18n::tf("aliyun.hmac_init_failed", &[("e", &e)]))?; mac.update(client_id.as_bytes()); Ok(base64::engine::general_purpose::STANDARD.encode(mac.finalize().into_bytes())) } /// HMAC-SHA256(secret, msg) 的小写 hex(一机一密签名)。 fn hmac_sha256_hex(secret: &str, msg: &str) -> Result { - let mut mac = - HmacSha256::new_from_slice(secret.as_bytes()).map_err(|e| format!("HMAC 初始化失败: {e}"))?; + let mut mac = HmacSha256::new_from_slice(secret.as_bytes()) + .map_err(|e| i18n::tf("aliyun.hmac_init_failed", &[("e", &e)]))?; mac.update(msg.as_bytes()); Ok(mac .finalize() @@ -152,15 +153,15 @@ impl AliyunPreset { || self.access_key_id.is_empty() || self.access_key_secret.is_empty() { - return Err("Group ID、AccessKey ID 与 AccessKey Secret 不能为空".into()); + return Err(i18n::t("aliyun.token_required_fields").into()); } // 空 instance_id 会拼出 ".mqtt.aliyuncs.com" 这样的非法接入点,DNS 报错难懂 if self.instance_id.trim().is_empty() { - return Err("Instance ID 不能为空".into()); + return Err(i18n::t("aliyun.instance_id_required").into()); } // 空 DeviceName 会拼出 "group@@@" 这样的非法 clientId,服务端报错难懂 if self.device_id.trim().is_empty() { - return Err("Device ID 不能为空".into()); + return Err(i18n::t("aliyun.device_id_required").into()); } conn.host = self.host(); let client_id = format!("{}@@@{}", self.group_id, self.device_id); @@ -172,11 +173,11 @@ impl AliyunPreset { } AliyunAuthMode::DeviceCredential => { if self.product_key.is_empty() || self.device_secret.is_empty() { - return Err("一机一密需要 ProductKey 与 DeviceSecret (设备密钥)".into()); + return Err(i18n::t("aliyun.triple_requires_pk_ds").into()); } // 空 DeviceName 会拼出 "pk.|securemode=..." 与 "&pk",服务端报错难懂 if self.device_id.trim().is_empty() { - return Err("一机一密需要 DeviceName(设备名)".into()); + return Err(i18n::t("aliyun.triple_requires_device_name").into()); } conn.host = self.host(); // 物联网平台「一机一密」直连三元组(见模块文档),DeviceName 复用 device_id diff --git a/gpui-app/src/mqtt/mod.rs b/gpui-app/src/mqtt/mod.rs index 49bc9ba..c16a4c4 100644 --- a/gpui-app/src/mqtt/mod.rs +++ b/gpui-app/src/mqtt/mod.rs @@ -20,7 +20,8 @@ use tokio::sync::watch; use crate::model::{ ConnectionConfig, ConnectionStatus, LogEntry, LogLevel, MqttRecord, PublishParams, SubscribeOptions, Subscription, -};// ─── 引擎事件 ──────────────────────────────────────────────────────────────── +}; +use crate::ui::i18n;// ─── 引擎事件 ──────────────────────────────────────────────────────────────── mod tls; @@ -151,9 +152,7 @@ impl MqttEngine { connection_id: String::new(), level: LogLevel::Warn, event: "events_dropped".into(), - message: format!( - "UI 处理不过来,已累计丢弃 {dropped} 条高频事件(消息/日志)" - ), + message: i18n::tf("engine.events_dropped", &[("n", &dropped)]), details: None, })); } @@ -221,12 +220,14 @@ impl MqttEngine { &id, LogLevel::Info, "connect", - format!( - "正在连接 {}:{} ({}, {})", - cfg.host, - cfg.port, - cfg.protocol.label(), - cfg.transport.label() + i18n::tf( + "engine.connecting", + &[ + ("host", &cfg.host), + ("port", &cfg.port), + ("protocol", &cfg.protocol.label()), + ("transport", &cfg.transport.label()), + ], ), None, ); @@ -259,7 +260,13 @@ impl MqttEngine { reason_code: None, session_present: None, }); - engine.log(id, LogLevel::Error, "connect_error", format!("连接失败: {e}"), None); + engine.log( + id, + LogLevel::Error, + "connect_error", + i18n::tf("engine.connect_failed", &[("e", &e)]), + None, + ); }; let (client, eventloop) = match build { Ok(Ok(pair)) => pair, @@ -268,7 +275,12 @@ impl MqttEngine { return; } Err(j) => { - build_error(&engine, &id, generation, format!("内部任务失败: {j}")); + build_error( + &engine, + &id, + generation, + i18n::tf("engine.internal_task_failed", &[("e", &j)]), + ); return; } }; @@ -335,7 +347,13 @@ impl MqttEngine { connected.store(false, Ordering::SeqCst); }); if user_initiated { - self.log(id, LogLevel::Info, "disconnect", "用户请求断开连接".into(), None); + self.log( + id, + LogLevel::Info, + "disconnect", + i18n::t("engine.user_disconnect").into(), + None, + ); self.emit(EngineEvent::Status { connection_id: id.to_string(), status: ConnectionStatus::Disconnected, @@ -357,7 +375,7 @@ impl MqttEngine { &connection_id, LogLevel::Error, "publish_error", - "发布失败: 连接不存在".into(), + i18n::t("engine.publish_no_connection").into(), None, ); return; @@ -405,9 +423,13 @@ impl MqttEngine { &connection_id, LogLevel::Info, "publish_sent", - format!( - "发布消息到 {} (QoS {}, retain={})", - params.topic, params.qos, params.retain + i18n::tf( + "engine.publish_sent", + &[ + ("topic", ¶ms.topic), + ("qos", ¶ms.qos), + ("retain", ¶ms.retain), + ], ), None, ); @@ -429,7 +451,7 @@ impl MqttEngine { &connection_id, LogLevel::Error, "publish_error", - format!("发布失败: {e}"), + i18n::tf("engine.publish_failed", &[("e", &e)]), None, ); } @@ -460,7 +482,7 @@ impl MqttEngine { topic: sub.topic, qos: sub.qos, ok: false, - error: Some("连接不存在".into()), + error: Some(i18n::t("engine.connection_not_found").into()), }); return; }; @@ -468,7 +490,7 @@ impl MqttEngine { &connection_id, LogLevel::Info, "subscribe", - format!("订阅主题: {} (QoS {})", sub.topic, sub.qos), + i18n::tf("engine.subscribe_topic", &[("topic", &sub.topic), ("qos", &sub.qos)]), None, ); let result = match &client { @@ -493,9 +515,9 @@ impl MqttEngine { .await { Ok(notice) => notice.wait_completion_async().await.map_err(|e| { - format!("订阅被拒绝: {e}") + i18n::tf("engine.subscribe_rejected", &[("e", &e)]) }), - Err(e) => Err(format!("订阅请求失败: {e}")), + Err(e) => Err(i18n::tf("engine.subscribe_request_failed", &[("e", &e)])), } } ClientKind::V4(client) => { @@ -504,9 +526,9 @@ impl MqttEngine { .await { Ok(notice) => notice.wait_completion_async().await.map_err(|e| { - format!("订阅被拒绝: {e}") + i18n::tf("engine.subscribe_rejected", &[("e", &e)]) }), - Err(e) => Err(format!("订阅请求失败: {e}")), + Err(e) => Err(i18n::tf("engine.subscribe_request_failed", &[("e", &e)])), } } }; @@ -555,7 +577,7 @@ impl MqttEngine { &connection_id, LogLevel::Info, "unsubscribe", - format!("取消订阅: {topic}"), + i18n::tf("engine.unsubscribe_ok", &[("topic", &topic)]), None, ); } @@ -564,7 +586,7 @@ impl MqttEngine { &connection_id, LogLevel::Error, "unsubscribe_error", - format!("取消订阅失败: {e}"), + i18n::tf("engine.unsubscribe_failed", &[("e", &e)]), None, ); } @@ -618,9 +640,7 @@ impl MqttEngine { .await; let reply = match result { Ok(inner) => inner, - Err(_) => Err(format!( - "连接测试超时({secs} 秒),请检查主机地址与端口" - )), + Err(_) => Err(i18n::tf("engine.test_timeout", &[("secs", &secs)])), }; let _ = tx.try_send(reply); }); diff --git a/gpui-app/src/mqtt/tls.rs b/gpui-app/src/mqtt/tls.rs index a4faa33..441b92d 100644 --- a/gpui-app/src/mqtt/tls.rs +++ b/gpui-app/src/mqtt/tls.rs @@ -5,10 +5,8 @@ use std::sync::Arc; use rumqttc::TlsConfiguration; use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer}; -use crate::model::{ - ConnectionConfig, - SslConfig, -};// ─── 引擎事件 ──────────────────────────────────────────────────────────────── +use crate::model::{ConnectionConfig, SslConfig}; +use crate::ui::i18n;// ─── 引擎事件 ──────────────────────────────────────────────────────────────── @@ -28,7 +26,7 @@ pub(super) fn resolve_tls_config(cfg: &ConnectionConfig) -> Result Result, String> { // 只填证书或只填私钥必然握手失败,提前给出明确错误 if ssl.client_cert_file.is_empty() != ssl.client_key_file.is_empty() { - return Err("客户端证书与客户端密钥必须同时配置".into()); + return Err(i18n::t("tls.cert_key_pair_required").into()); } let provider = ensure_crypto_provider(); let builder = rustls::ClientConfig::builder(); @@ -48,7 +46,7 @@ pub(super) fn build_rustls_config(ssl: &SslConfig) -> Result>() .join("; "); - return Err(format!("加载系统根证书失败: {detail}")); + return Err(i18n::tf("tls.native_roots_failed", &[("detail", &detail)])); } // 个别系统证书加载失败不阻断连接:自定义 CA 与其余根证书仍参与校验 for cert in native.certs { @@ -56,17 +54,19 @@ pub(super) fn build_rustls_config(ssl: &SslConfig) -> Result Result, _>>() - .map_err(|e| format!("解析客户端证书文件 {} 失败: {e}", ssl.client_cert_file))?; + .map_err(|e| { + i18n::tf("tls.client_cert_parse_failed", &[("p", &ssl.client_cert_file), ("e", &e)]) + })?; if certs.is_empty() { - return Err(format!( - "客户端证书文件 {} 中没有找到证书", - ssl.client_cert_file + return Err(i18n::tf( + "tls.client_cert_no_certs", + &[("p", &ssl.client_cert_file)], )); } - let key = PrivateKeyDer::from_pem_slice(&key_pem) - .map_err(|e| format!("解析客户端私钥文件 {} 失败: {e}", ssl.client_key_file))?; + let key = PrivateKeyDer::from_pem_slice(&key_pem).map_err(|e| { + i18n::tf("tls.client_key_parse_failed", &[("p", &ssl.client_key_file), ("e", &e)]) + })?; builder .with_client_auth_cert(certs, key) - .map_err(|e| format!("加载客户端证书/私钥失败: {e}"))? + .map_err(|e| i18n::tf("tls.client_auth_failed", &[("e", &e)]))? }; Ok(Arc::new(config)) } diff --git a/gpui-app/src/mqtt/v4.rs b/gpui-app/src/mqtt/v4.rs index 72997f6..1f946d2 100644 --- a/gpui-app/src/mqtt/v4.rs +++ b/gpui-app/src/mqtt/v4.rs @@ -16,6 +16,7 @@ use crate::model::{ use super::*; +use crate::ui::i18n; pub(super) fn build_v4( cfg: &ConnectionConfig, @@ -54,7 +55,7 @@ pub(super) fn build_v4( None => { opts.set_transport( V4Transport::try_tls_with_default_config() - .map_err(|e| format!("TLS 初始化失败: {e:?}"))?, + .map_err(|e| i18n::tf("tls.init_failed", &[("e", &format!("{e:?}"))]))?, ); } }; @@ -107,7 +108,7 @@ pub(super) async fn test_handshake(cfg_in: &ConnectionConfig) -> Result<(), Stri let owned = cfg.clone(); let (client, mut eventloop) = tokio::task::spawn_blocking(move || build_v5(&owned)) .await - .map_err(|e| format!("内部任务失败: {e}"))??; + .map_err(|e| i18n::tf("engine.internal_task_failed", &[("e", &e)]))??; loop { match eventloop.poll().await { Ok(V5Event::Incoming(V5Packet::ConnAck(ack))) => { @@ -123,12 +124,18 @@ pub(super) async fn test_handshake(cfg_in: &ConnectionConfig) -> Result<(), Stri } return Ok(()); } - return Err(format!("连接被拒绝: {:?}", ack.code)); + return Err(i18n::tf( + "engine.connack_rejected", + &[("code", &format!("{:?}", ack.code))], + )); } Ok(V5Event::Incoming(V5Packet::Disconnect(d))) => { - return Err(format!("服务端断开: {:?}", d.reason_code)); + return Err(i18n::tf( + "engine.disconnected_by_server_code", + &[("code", &format!("{:?}", d.reason_code))], + )); } - Err(e) => return Err(format!("连接失败: {e}")), + Err(e) => return Err(i18n::tf("engine.connect_failed", &[("e", &e)])), _ => {} } } @@ -137,7 +144,7 @@ pub(super) async fn test_handshake(cfg_in: &ConnectionConfig) -> Result<(), Stri let owned = cfg.clone(); let (client, mut eventloop) = tokio::task::spawn_blocking(move || build_v4(&owned)) .await - .map_err(|e| format!("内部任务失败: {e}"))??; + .map_err(|e| i18n::tf("engine.internal_task_failed", &[("e", &e)]))??; loop { match eventloop.poll().await { Ok(rv4::Event::Incoming(rv4::Packet::ConnAck(ack))) => { @@ -151,12 +158,15 @@ pub(super) async fn test_handshake(cfg_in: &ConnectionConfig) -> Result<(), Stri } return Ok(()); } - return Err(format!("连接被拒绝: {:?}", ack.code)); + return Err(i18n::tf( + "engine.connack_rejected", + &[("code", &format!("{:?}", ack.code))], + )); } Ok(rv4::Event::Incoming(rv4::Packet::Disconnect)) => { - return Err("服务端主动断开".into()); + return Err(i18n::t("engine.disconnected_by_server_active").into()); } - Err(e) => return Err(format!("连接失败: {e}")), + Err(e) => return Err(i18n::tf("engine.connect_failed", &[("e", &e)])), _ => {} } } @@ -213,7 +223,7 @@ pub(super) fn spawn_v4_loop( }; engine.log( &id, LogLevel::Info, "publish_received", - format!("收到消息: {} ({} 字节)", record.topic, p.payload.len()), None); + i18n::tf("engine.received_message", &[("topic", &record.topic), ("n", &p.payload.len())]), None); engine.emit(EngineEvent::Message(record)); } Ok(rv4::Event::Incoming(rv4::Packet::ConnAck(ack))) => { @@ -224,7 +234,7 @@ pub(super) fn spawn_v4_loop( // 「连续失败次数」,不能把历史错误累计进来 reconnect_attempts = 0; engine.log(&id, LogLevel::Info, "connack", - format!("连接成功 (session_present: {})", ack.session_present), None); + i18n::tf("engine.connect_ok", &[("sp", &ack.session_present)]), None); engine.emit(EngineEvent::Status { connection_id: id.clone(), status: ConnectionStatus::Connected, @@ -234,7 +244,7 @@ pub(super) fn spawn_v4_loop( }); } else { engine.log(&id, LogLevel::Error, "connack", - format!("连接被拒绝: {:?}", ack.code), None); + i18n::tf("engine.connack_rejected", &[("code", &format!("{:?}", ack.code))]), None); engine.emit(EngineEvent::Status { connection_id: id.clone(), status: ConnectionStatus::Error, @@ -251,7 +261,7 @@ pub(super) fn spawn_v4_loop( engine.emit(EngineEvent::Status { connection_id: id.clone(), status: ConnectionStatus::Disconnected, - error: Some("服务端断开".into()), + error: Some(i18n::t("engine.disconnected_by_server").into()), reason_code: None, session_present: None, }); @@ -259,19 +269,19 @@ pub(super) fn spawn_v4_loop( } Ok(rv4::Event::Incoming(rv4::Packet::SubAck(s))) => { engine.log(&id, LogLevel::Info, "suback", - format!("订阅确认 (packet {})", s.pkid), None); + i18n::tf("engine.suback", &[("n", &s.pkid)]), None); } Ok(rv4::Event::Incoming(rv4::Packet::UnsubAck(a))) => { engine.log(&id, LogLevel::Info, "unsuback", - format!("取消订阅确认 (packet {})", a.pkid), None); + i18n::tf("engine.unsuback_packet", &[("n", &a.pkid)]), None); } Ok(rv4::Event::Incoming(rv4::Packet::PubAck(a))) => { engine.log(&id, LogLevel::Debug, "puback", - format!("发布确认 (packet {})", a.pkid), None); + i18n::tf("engine.puback", &[("n", &a.pkid)]), None); } Ok(rv4::Event::Outgoing(rv4::Outgoing::Publish(pkid))) => { engine.log(&id, LogLevel::Debug, "publish_outgoing", - format!("消息已发出 (packet {pkid})"), None); + i18n::tf("engine.publish_outgoing", &[("n", &pkid)]), None); } Ok(_) => {} Err(rv4::ConnectionError::RequestsDone) => { @@ -293,19 +303,20 @@ pub(super) fn spawn_v4_loop( }); if !auto_reconnect || first_connect { engine.log(&id, LogLevel::Error, "connection_error", - format!("连接错误: {e}"), None); + i18n::tf("engine.connection_error", &[("e", &e)]), None); break; } // max_reconnect_times>0 时按次数停止重试;0 表示无限重连 if max_reconnect_times > 0 && reconnect_attempts >= max_reconnect_times { engine.log(&id, LogLevel::Error, "reconnect_give_up", - format!("已重连 {reconnect_attempts} 次仍失败,达到最大重连次数 {max_reconnect_times},停止重连: {e}"), None); + i18n::tf("engine.reconnect_give_up", &[("attempts", &reconnect_attempts), ("max", &max_reconnect_times), ("e", &e)]), None); break; } reconnect_attempts += 1; let delay = reconnect_delay(reconnect_attempts); + let delay_s = format!("{:.1}", delay.as_secs_f32()); engine.log(&id, LogLevel::Warn, "connection_error", - format!("连接错误,{:.1} 秒后重连(第 {reconnect_attempts} 次): {e}", delay.as_secs_f32()), None); + i18n::tf("engine.reconnect_retry", &[("delay", &delay_s), ("attempts", &reconnect_attempts), ("e", &e)]), None); // 退避等待也必须可取消:否则 close() 的取消信号要等一整个 // 退避周期才可见,期间会对用户已关闭的连接继续重连。 tokio::select! { @@ -316,7 +327,7 @@ pub(super) fn spawn_v4_loop( } } _ = cancel_rx.changed() => { - engine.log(&id, LogLevel::Info, "disconnect_cancelled", "连接已取消".into(), None); + engine.log(&id, LogLevel::Info, "disconnect_cancelled", i18n::t("engine.cancelled").into(), None); break; } } diff --git a/gpui-app/src/mqtt/v5.rs b/gpui-app/src/mqtt/v5.rs index eced9a3..f84f95d 100644 --- a/gpui-app/src/mqtt/v5.rs +++ b/gpui-app/src/mqtt/v5.rs @@ -19,6 +19,7 @@ use crate::model::{ use super::*; +use crate::ui::i18n; pub(super) fn build_v5( cfg: &ConnectionConfig, @@ -55,7 +56,7 @@ pub(super) fn build_v5( None => { opts.set_transport( V5Transport::try_tls_with_default_config() - .map_err(|e| format!("TLS 初始化失败: {e:?}"))?, + .map_err(|e| i18n::tf("tls.init_failed", &[("e", &format!("{e:?}"))]))?, ); } }; @@ -208,7 +209,14 @@ pub(super) fn spawn_v5_loop( &id, LogLevel::Info, "publish_received", - format!("收到消息: {topic} ({} 字节, QoS {})", p.payload.len(), v5_qos_u8(p.qos)), + i18n::tf( + "engine.received_message_qos", + &[ + ("topic", &topic), + ("n", &p.payload.len()), + ("qos", &v5_qos_u8(p.qos)), + ], + ), None, ); engine.emit(EngineEvent::Message(record)); @@ -225,7 +233,7 @@ pub(super) fn spawn_v5_loop( &id, LogLevel::Info, "connack", - format!("连接成功 (session_present: {})", ack.session_present), + i18n::tf("engine.connect_ok", &[("sp", &ack.session_present)]), None, ); engine.emit(EngineEvent::Status { @@ -240,7 +248,10 @@ pub(super) fn spawn_v5_loop( &id, LogLevel::Error, "connack", - format!("连接被拒绝: {:?} (code {code})", ack.code), + i18n::tf( + "engine.connack_rejected_code", + &[("code", &format!("{:?}", ack.code)), ("n", &code)], + ), None, ); engine.emit(EngineEvent::Status { @@ -259,7 +270,13 @@ pub(super) fn spawn_v5_loop( &id, LogLevel::Warn, "disconnect_received", - format!("收到断开连接: {:?} (code {})", d.reason_code, d.reason_code as u8), + i18n::tf( + "engine.disconnect_received", + &[ + ("code", &format!("{:?}", d.reason_code)), + ("n", &(d.reason_code as u8)), + ], + ), None, ); engine.emit(EngineEvent::Status { @@ -278,26 +295,26 @@ pub(super) fn spawn_v5_loop( .map(|c| format!("{c:?}")) .collect(); engine.log(&id, LogLevel::Info, "suback", - format!("订阅确认 (packet {}): {}", s.pkid, granted.join(", ")), None); + i18n::tf("engine.suback_codes", &[("n", &s.pkid), ("codes", &granted.join(", "))]), None); } Ok(V5Event::Incoming(V5Packet::UnsubAck(_))) => { - engine.log(&id, LogLevel::Info, "unsuback", "取消订阅确认".into(), None); + engine.log(&id, LogLevel::Info, "unsuback", i18n::t("engine.unsuback").into(), None); } Ok(V5Event::Incoming(V5Packet::PubAck(a))) => { engine.log(&id, LogLevel::Debug, "puback", - format!("发布确认 (packet {}, reason {:?})", a.pkid, a.reason), None); + i18n::tf("engine.puback_reason", &[("n", &a.pkid), ("reason", &format!("{:?}", a.reason))]), None); } Ok(V5Event::Incoming(V5Packet::PubRec(a))) => { engine.log(&id, LogLevel::Debug, "pubrec", - format!("QoS2 发布收到 (packet {})", a.pkid), None); + i18n::tf("engine.pubrec", &[("n", &a.pkid)]), None); } Ok(V5Event::Incoming(V5Packet::PubComp(a))) => { engine.log(&id, LogLevel::Debug, "pubcomp", - format!("QoS2 发布完成 (packet {})", a.pkid), None); + i18n::tf("engine.pubcomp", &[("n", &a.pkid)]), None); } Ok(V5Event::Outgoing(V5Outgoing::Publish(pkid))) => { engine.log(&id, LogLevel::Debug, "publish_outgoing", - format!("消息已发出 (packet {pkid})"), None); + i18n::tf("engine.publish_outgoing", &[("n", &pkid)]), None); } Ok(_) => {} Err(V5Error::RequestsDone) => { @@ -322,19 +339,20 @@ pub(super) fn spawn_v5_loop( }); if !auto_reconnect || first_connect { engine.log(&id, LogLevel::Error, "connection_error", - format!("连接错误: {e}"), None); + i18n::tf("engine.connection_error", &[("e", &e)]), None); break; } // max_reconnect_times>0 时按次数停止重试;0 表示无限重连 if max_reconnect_times > 0 && reconnect_attempts >= max_reconnect_times { engine.log(&id, LogLevel::Error, "reconnect_give_up", - format!("已重连 {reconnect_attempts} 次仍失败,达到最大重连次数 {max_reconnect_times},停止重连: {e}"), None); + i18n::tf("engine.reconnect_give_up", &[("attempts", &reconnect_attempts), ("max", &max_reconnect_times), ("e", &e)]), None); break; } reconnect_attempts += 1; let delay = reconnect_delay(reconnect_attempts); + let delay_s = format!("{:.1}", delay.as_secs_f32()); engine.log(&id, LogLevel::Warn, "connection_error", - format!("连接错误,{:.1} 秒后重连(第 {reconnect_attempts} 次): {e}", delay.as_secs_f32()), None); + i18n::tf("engine.reconnect_retry", &[("delay", &delay_s), ("attempts", &reconnect_attempts), ("e", &e)]), None); // 退避等待也必须可取消:否则 close() 的取消信号要等一整个 // 退避周期才可见,期间会对用户已关闭的连接继续重连。 tokio::select! { @@ -345,7 +363,7 @@ pub(super) fn spawn_v5_loop( } } _ = cancel_rx.changed() => { - engine.log(&id, LogLevel::Info, "disconnect_cancelled", "连接已取消".into(), None); + engine.log(&id, LogLevel::Info, "disconnect_cancelled", i18n::t("engine.cancelled").into(), None); break; } }