From 05b37a8997da358c342ca3fd7bed705c80954b02 Mon Sep 17 00:00:00 2001 From: JoeAllen <2494136784@qq.com> Date: Mon, 12 Oct 2026 15:11:48 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8C=89=20gpui-kit=20=E5=AE=98?= =?UTF-8?q?=E6=96=B9=E6=8C=87=E5=8D=97=E5=AF=B9=E9=BD=90=EF=BC=88locales?= =?UTF-8?q?=20=E6=8B=86=E5=88=86/render=20=E6=97=A0=E7=AA=81=E5=8F=98/?= =?UTF-8?q?=E5=8A=A8=E4=BD=9C=E7=BB=9F=E4=B8=80=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit skill 安装:npx skills add 因网络不可用,改为 gh-proxy 镜像克隆仓库后 手动覆盖 ~/.agents/skills 与 ~/.zcode/skills 两处安装(与官方最新一致)。 语言包按语言分文件(用户要求): - locales/ui.yml(合并格式)→ locales/en.yml + locales/zh-CN.yml (rust-i18n 以文件名 stem 识别 locale,扁平 "ns.key": "value") - i18n.rs 测试适配双文件:键集合一致性 + 占位符两侧一致强制保留 对齐 coding-guides / design-guides / auto-update: - render 无突变:分组过滤悬空复位从 render 移到 delete_connection/ save_connection 的 action 路径(normalize_group_filter) - 消除 UI 层 hsla 字面量:消息色条改 when_some 条件上色;色板描边 从白 60% 改主题底色(双主题下均为「与背景的缝隙」语义) - 动作统一:标题栏菜单的资源监控/导出/导入/设置改 dispatch_action, 与快捷键共享同一 Action(菜单/快捷键单一命令策略) - auto-update 对齐官方指南:自动检查加 24h 节流(.last-update-check 时间戳),手动检查更新不受限 保留(记录豁免理由): - 手写 collapsible_section:官方 Accordion 非受控(内部索引状态), 替代会丢「已配置默认展开」的受控行为 - 自研 update/:官方无内置 updater,指南推荐 self_update+应用自有 状态机,现有实现(状态机/后台 worker/sha256/consent/原子替换) 已符合该精神;换 self_update 反而放弃已有代理兜底与 7z 通道 - px() 几何常量:对话框/面板宽度与色点尺寸属文档化几何豁免, 机械换算会引入视觉回归;min 约束的 px(0.) 为布局断言 42 测试全绿,实机冒烟通过。 --- gpui-app/examples/flood.rs | 66 -- gpui-app/locales/en.yml | 347 ++++++ gpui-app/locales/ui.yml | 1038 ------------------ gpui-app/locales/zh-CN.yml | 347 ++++++ gpui-app/src/ui/app/actions.rs | 14 + gpui-app/src/ui/app/sidebar.rs | 10 +- gpui-app/src/ui/app/titlebar.rs | 30 +- gpui-app/src/ui/connection_view/messages.rs | 6 +- gpui-app/src/ui/connection_view/subscribe.rs | 7 +- gpui-app/src/ui/i18n.rs | 94 +- gpui-app/src/update/check.rs | 26 + 11 files changed, 801 insertions(+), 1184 deletions(-) delete mode 100644 gpui-app/examples/flood.rs create mode 100644 gpui-app/locales/en.yml delete mode 100644 gpui-app/locales/ui.yml create mode 100644 gpui-app/locales/zh-CN.yml diff --git a/gpui-app/examples/flood.rs b/gpui-app/examples/flood.rs deleted file mode 100644 index 168f219..0000000 --- a/gpui-app/examples/flood.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! 消息流量压测工具:连接 broker 后按指定速率向主题发布消息。 -//! -//! 用法:cargo run --example flood -- [payload_bytes] -//! 例:向私有 broker 发 500 条 1KB 消息、每秒 50 条: -//! cargo run --example flood -- mqtt.nex-i.cn 1883 mqttx-flood 500 50 1024 - -use std::time::{Duration, Instant}; - -fn main() { - let args: Vec = std::env::args().collect(); - if args.len() < 6 { - eprintln!("usage: flood [payload_bytes]"); - std::process::exit(1); - } - let host = args[1].clone(); - let port: u16 = args[2].parse().unwrap(); - let topic = args[3].clone(); - let count: u64 = args[4].parse().unwrap(); - let per_second: u64 = args[5].parse().unwrap(); - let payload_size: usize = args.get(6).map(|s| s.parse().unwrap()).unwrap_or(512); - - let rt = tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) - .enable_all() - .build() - .unwrap(); - rt.block_on(async move { - let opts = rumqttc::MqttOptions::new( - format!("mqttx_flood_{}", &uuid::Uuid::new_v4().simple().to_string()[..8]), - rumqttc::Broker::tcp(&host, port), - ); - let (client, mut eventloop) = rumqttc::AsyncClient::builder(opts).build(); - - // eventloop 独立持续 poll:请求出队全靠它 - tokio::spawn(async move { - loop { - if eventloop.poll().await.is_err() { - break; - } - } - }); - - let payload = vec![b'x'; payload_size]; - let interval = Duration::from_secs_f64(1.0 / per_second.max(1) as f64); - let start = Instant::now(); - for i in 0..count { - let topic_i = format!("{topic}/{i}"); - let options = rumqttc::PublishOptions::new(rumqttc::QoS::AtMostOnce); - if let Err(e) = client.publish(&topic_i, payload.clone(), options).await { - eprintln!("publish error at {i}: {e}"); - break; - } - let next = start + interval * (i as u32 + 1); - let now = Instant::now(); - if next > now { - tokio::time::sleep(next - now).await; - } - } - eprintln!( - "flooded {count} msgs ({payload_size}B each) in {:.1}s", - start.elapsed().as_secs_f64() - ); - // 等网络冲刷 - tokio::time::sleep(Duration::from_secs(3)).await; - }); -} diff --git a/gpui-app/locales/en.yml b/gpui-app/locales/en.yml new file mode 100644 index 0000000..c4ac3ae --- /dev/null +++ b/gpui-app/locales/en.yml @@ -0,0 +1,347 @@ +# MQTTX en 文案(rust-i18n:文件名即 locale,扁平 key: value)。 +# key = 语义标识符 ns.name;en/zh-CN 两文件键集合必须一致——单测强制。 +"aliyun.access_key_id": "AccessKey ID" +"aliyun.access_key_secret": "AccessKey Secret" +"aliyun.auth_mode": "Auth mode" +"aliyun.auth_token_mode": "Group token" +"aliyun.auth_triple_mode": "Per-device secret" +"aliyun.build_failed": "Failed to build connection: {e}" +"aliyun.device_id": "Device ID" +"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.instance_id": "Instance ID" +"aliyun.preset_name": "Preset name" +"aliyun.product_key": "ProductKey (per-device)" +"aliyun.product_key_ph": "Triple: ProductKey" +"aliyun.region": "Region" +"aliyun.region_ph": "e.g. cn-shanghai (optional)" +"aliyun.save_preset": "Save preset" +"aliyun.saved_presets": "Saved presets" +"aliyun.title": "Alibaba Cloud IoT" +"color.blue": "Blue" +"color.cyan": "Cyan" +"color.green": "Green" +"color.indigo": "Indigo" +"color.magenta": "Magenta" +"color.orange": "Orange" +"color.purple": "Purple" +"color.red": "Red" +"color.rose": "Rose" +"color.yellow": "Yellow" +"common.advanced": "Advanced" +"common.all": "All" +"common.auto": "Auto" +"common.browse": "Browse…" +"common.cancel": "Cancel" +"common.clear": "Clear" +"common.close": "Close" +"common.collapse": "Collapse" +"common.delete": "Delete" +"common.edit": "Edit" +"common.new": "New" +"common.pause": "Pause" +"common.resume": "Resume" +"common.retry": "Retry" +"common.save": "Save" +"common.send": "Send" +"common.text": "Text" +"common.update": "Update" +"connview.logs": "Logs" +"connview.messages": "Messages" +"connview.subscriptions": "Subscriptions" +"dir.published": "Published" +"dir.received": "Received" +"encode.base64_failed": "Base64 decode failed: {e}" +"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)" +"form.auth_section": "Auth & Keep-alive" +"form.auto_connect": "Connect on app start" +"form.auto_reconnect": "Auto reconnect on disconnect" +"form.auto_resub": "Auto-resubscribe on connect" +"form.basics": "Basics" +"form.cert_pair_required": "Client certificate and key must be provided together" +"form.clean_start": "Clean Start / Clean Session" +"form.client_id": "Client ID" +"form.client_id_required": "Client ID cannot be empty" +"form.conn_timeout": "Connect timeout (s)" +"form.conn_timeout_invalid": "Connect timeout must be an integer 1-65535 (seconds)" +"form.content_type_ph": "Content type (optional)" +"form.err_positive": "must be > 0 (0 is an MQTT 5 protocol error)" +"form.err_range": "must be an integer 0-65535" +"form.err_uint": "must be a non-negative integer" +"form.group": "Group" +"form.host": "Host" +"form.host_required": "Host cannot be empty" +"form.keep_alive": "Keep Alive (s)" +"form.keep_alive_short": "Keep Alive" +"form.max_packet": "Maximum Packet Size" +"form.max_reconnects": "Max reconnects (0 = unlimited)" +"form.max_reconnects_invalid": "Max reconnects must be a non-negative integer (0 = unlimited)" +"form.max_reconnects_ph": "0 = unlimited" +"form.name": "Name" +"form.name_ph": "Connection name" +"form.name_required": "Connection name cannot be empty" +"form.new_group": "New group" +"form.new_group_hint": "Overrides the selected group when filled" +"form.no_group": "No group" +"form.password": "Password" +"form.password_ph": "Password (optional)" +"form.path_missing": "{label}: path not found: {p}" +"form.port": "Port" +"form.port_invalid": "Port must be an integer 1-65535" +"form.protocol": "Protocol" +"form.receive_max": "Receive Maximum" +"form.receive_max_invalid": "Receive Maximum cannot exceed 65535" +"form.receive_max_ph": "Server receive maximum" +"form.receive_max_short": "Receive Maximum" +"form.regen_client_id": "Regenerate Client ID" +"form.response_topic_ph": "Response topic (optional)" +"form.saved_reconnect": "Saved. Takes effect after reconnect" +"form.session_expiry": "Session expiry (s)" +"form.session_expiry_invalid": "Session expiry must be a non-negative integer (0 = session ends with connection)" +"form.session_expiry_ph": "seconds (0 = session ends with connection)" +"form.test": "Test Connection" +"form.test_ok": "Connection test succeeded" +"form.testing": "Testing…" +"form.timeout_ph": "seconds (default 10)" +"form.title": "Connection Settings" +"form.topic_alias": "Topic Alias" +"form.topic_alias_invalid": "Topic Alias Maximum cannot exceed 65535" +"form.topic_alias_max": "Topic Alias Maximum" +"form.transport": "Transport" +"form.username": "Username" +"form.username_ph": "Username (optional)" +"form.will_content_type": "Content Type" +"form.will_enable": "Enable last will" +"form.will_payload": "Will payload" +"form.will_payload_ph": "Will message" +"form.will_response_topic": "Response Topic" +"form.will_section": "Last Will" +"form.will_topic": "Will topic" +"form.ws_path": "WS Path" +"form.ws_path_required": "WS path is required for WS/WSS transport" +"io.err_item_id": "Item {n}: id must not be empty" +"io.err_item_invalid": "Item {n} is not a valid connection: {e}" +"io.err_missing_field": "JSON object lacks the \"connections\" array" +"io.err_parse_json": "Failed to parse JSON: {e}" +"io.err_parse_json_path": "Failed to parse {p}" +"io.err_read": "Failed to read {p}: {e}" +"io.err_serialize": "Failed to serialize connections: {e}" +"io.err_utf8": "{p} is not valid UTF-8" +"io.err_write": "Failed to write {p}: {e}" +"io.exported": "Exported {n} connections to {path}" +"io.imported": "Imported {n}, skipped {n2}" +"kv.add": "Add property" +"msg.binary_hint": " (binary — switch to Hex in details)" +"msg.clear_confirm": "Clear all messages of this connection? This cannot be undone." +"msg.clear_sub_filter": "Clear subscription filter" +"msg.clear_title": "Clear Messages" +"msg.clear_tooltip": "Clear messages of this connection" +"msg.copy_details": "Copy full details (incl. MQTT 5 props)" +"msg.copy_payload": "Copy raw payload" +"msg.copy_topic": "Copy topic" +"msg.count": "{n} msgs" +"msg.details": "Details" +"msg.details_copied": "Details copied" +"msg.empty": "No messages yet — they will appear here once you subscribe" +"msg.expiry": "Message Expiry" +"msg.filter_placeholder": "Filter by topic or content…" +"msg.latest_only": "Showing latest {n} only" +"msg.log_title": "Connection Logs" +"msg.no_logs": "No logs yet" +"msg.no_match": "No matching messages" +"msg.pause_tooltip": "Pause message stream (messages keep arriving in background)" +"msg.payload": "Payload" +"msg.payload_copied": "Payload copied" +"msg.resume_tooltip": "Resume live scrolling" +"msg.seconds": "{n}s" +"msg.topic": "Topic" +"msg.topic_copied": "Topic copied" +"msg.truncated": "Truncated {n}KB" +"preset.default_name": "New preset" +"preset.default_name_n": "New preset {n}" +"preset.empty": "No presets yet — click \"New\" above to create one" +"preset.manage_title": "Manage Publish Presets" +"preset.name_exists": "Preset name \"{name}\" already exists" +"preset.payload_field": "Payload (stored raw; variables rendered on send)" +"preset.payload_format": "Payload format" +"preset.pick_delete_first": "Select a preset to delete first" +"preset.pick_first": "Select or create a preset first" +"preset.saved": "Preset \"{name}\" saved" +"preset.section": "Publish Presets" +"preset.user_props": "User Properties" +"pub.collapse": "Collapse publish panel" +"pub.collapsed_hint": "Publish panel collapsed" +"pub.content_type_ph": "Content-Type (optional)" +"pub.correlation_ph": "Correlation Data (optional)" +"pub.delete_confirm": "Delete preset \"{name}\"? This cannot be undone." +"pub.delete_item": "Delete \"{name}\"" +"pub.delete_title": "Delete Preset" +"pub.expand": "Expand publish panel" +"pub.expiry_field": "Expiry (s)" +"pub.expiry_invalid": "Message expiry must be a non-negative integer (seconds)" +"pub.json_invalid": "Invalid JSON: {e}" +"pub.manage_presets": "Manage presets…" +"pub.mqtt5_props": "MQTT 5 Properties" +"pub.offline_not_sent": "Offline: message not sent" +"pub.payload_placeholder": "Message payload; supports {{variables}} and {{$ts}} {{$uuid}}" +"pub.payload_preset_ph": "Message payload; supports {{variables}}" +"pub.preset_menu": "Message Presets" +"pub.preset_name_hint": "Preset name, e.g. temperature-report" +"pub.preset_name_required": "Preset name is required" +"pub.preset_overwritten": "Preset \"{name}\" overwritten" +"pub.preset_saved": "Preset saved" +"pub.response_topic_ph": "Response Topic (optional)" +"pub.save_preset": "Save current as preset…" +"pub.save_title": "Save Message Preset" +"pub.seconds_hint": "seconds, e.g. 60" +"pub.send_disabled": "Offline — cannot send" +"pub.sent_hidden": "Sent (hidden by current filter — not shown in the stream)" +"pub.topic_placeholder": "Publish topic" +"pub.topic_preset_ph": "Publish topic; supports {{variables}}" +"pub.topic_required": "Publish topic cannot be empty" +"pub.vars": "Variable injection" +"pub.vars_count": "Variable injection ({n} placeholders)" +"resmon.auto_refresh": "Auto refresh" +"resmon.budget": "Budget {p}% used ({used} / {total})" +"resmon.committed": "Committed memory" +"resmon.committed_hint": "Physical pages committed by the process" +"resmon.cpu": "CPU usage" +"resmon.cpu_hint": "Normalized by core count" +"resmon.n_msgs": "{n} msgs" +"resmon.no_conn": "No connections" +"resmon.peak": "Peak {v}" +"resmon.refresh_now": "Refresh now" +"resmon.section": "Messages & Connections" +"resmon.stats": "{n} connections ({m} connected) · {t} tabs · {msgs} messages in memory (cap {cap}/conn) · {logs} log lines" +"resmon.threads_handles": "Threads · handles" +"resmon.threads_hint": "tokio engine + GPUI renderer" +"resmon.uptime": "Up {t}" +"resmon.version_live": "Version v{v} · refreshes every 1.5s" +"resmon.version_paused": "Version v{v} · auto-refresh paused" +"resmon.working_set": "Working set" +"settings.about": "About" +"settings.auto_update": "Check for updates on start" +"settings.byte_budget": "Plus a per-connection byte budget of {n} MB: under heavy traffic, oldest messages are evicted by size first" +"settings.data_dir": "Data directory" +"settings.language": "Language" +"settings.license": "Apache-2.0 license" +"settings.log_dir": "Log directory" +"settings.max_messages": "Messages kept in memory per connection (100-100000)" +"settings.mkdir_failed": "Failed to create log folder: {e}" +"settings.open_dir": "Open folder" +"settings.open_dir_failed": "Failed to open folder: {e}" +"settings.open_log_dir": "Open log folder" +"settings.show_millis": "Show milliseconds in timestamps" +"settings.tagline": "A cross-platform MQTT debug client built with GPUI, aiming at MQTTX" +"settings.theme": "Theme" +"settings.version_line": "Version {v} (update source: {src})" +"sidebar.collapse": "Collapse sidebar (Ctrl+B)" +"sidebar.connections": "Connections" +"sidebar.delete": "Delete Connection" +"sidebar.delete_confirm": "Delete \"{name}\"? This also deletes {sub_count} subscriptions and disconnects. This cannot be undone." +"sidebar.disconnect": "Disconnect" +"sidebar.dup_suffix": "{name} (copy)" +"sidebar.duplicate": "Duplicate" +"sidebar.empty": "No connections yet. Click + above to create one" +"sidebar.expand": "Expand sidebar (Ctrl+B)" +"sidebar.no_match": "No matching connections" +"sidebar.search": "Search connections…" +"sidebar.ungrouped": "Ungrouped" +"ssl.ca": "CA certificate (PEM)" +"ssl.ca_ph": "Path to CA certificate (PEM, optional)" +"ssl.cert": "Client certificate (PEM)" +"ssl.cert_ph": "Path to client certificate (PEM, optional)" +"ssl.key": "Client key (PEM)" +"ssl.key_ph": "Path to client private key (PEM, optional)" +"ssl.n_configured": "{n} configured" +"ssl.pick_file": "Select certificate / key file" +"ssl.section": "SSL/TLS" +"ssl.skip_verify": "Skip CA verification (trust any server cert)" +"status.connected": "Connected" +"status.connecting": "Connecting" +"status.disconnected": "Disconnected" +"status.error": "Error" +"sub.alias": "Alias (optional)" +"sub.cancel_edit": "Cancel editing" +"sub.collapse": "Collapse subscriptions panel" +"sub.color": "Subscription color" +"sub.color_clear": "Clear color" +"sub.color_random": "Random color" +"sub.disable": "Disable subscription" +"sub.edit": "Edit subscription" +"sub.editing": "Editing: {topic}" +"sub.empty_hint": "Subscribe to a topic and messages will show up here" +"sub.enable": "Enable subscription" +"sub.expand": "Expand subscriptions panel" +"sub.failed": "Subscribe failed: {e}" +"sub.identifier": "Subscription Identifier" +"sub.identifier_field": "Subscription Identifier" +"sub.identifier_invalid": "Subscription identifier must be a positive integer" +"sub.mqtt5_options": "MQTT 5 Subscription Options" +"sub.no_local": "No Local" +"sub.rap": "Retain As Published" +"sub.retain_handling": "Retain Handling" +"sub.rh0": "0 Send at every subscribe" +"sub.rh1": "1 Send only for new subscription" +"sub.rh2": "2 Do not send" +"sub.saved_offline": "Offline: subscription saved" +"sub.saved_offline_resub": "Offline: subscription saved, restored on connect" +"sub.topic_placeholder": "Topic filter, supports # +; separate multiple with commas/spaces" +"sub.topic_required": "Topic filter cannot be empty" +"sub.unsubscribe": "Unsubscribe" +"tab.welcome_hint": "Pick a connection on the left, or create a new one to start debugging" +"tab.welcome_title": "Get started with MQTTX" +"theme.dark": "Dark" +"theme.light": "Light" +"theme.system": "Follow system" +"titlebar.aliyun": "Alibaba Cloud Devices" +"titlebar.export": "Export Connections" +"titlebar.import": "Import Connections" +"titlebar.new_connection": "New Connection" +"titlebar.pin": "Pin window on top" +"titlebar.pin_failed": "Failed to set always-on-top" +"titlebar.resmon": "Resource Monitor" +"titlebar.settings": "Settings" +"titlebar.subtitle": "MQTT debug client" +"titlebar.switch_dark": "Switch to dark theme" +"titlebar.switch_light": "Switch to light theme" +"titlebar.unpin": "Unpin window" +"titlebar.variables": "Global Variables" +"update.apply": "Update to v{v} now" +"update.check": "Check for updates" +"update.check_hint": "Check GitHub Releases for a new version" +"update.checking": "Checking…" +"update.checking_long": "Checking for updates…" +"update.discard": "Discard this update" +"update.discarded": "Staged update discarded" +"update.downloading": "Downloading {pct}% ({done} / {total})" +"update.downloading_size": "Downloading {done}" +"update.failed": "Update failed: {e}" +"update.found": "New version v{v} found — downloads, replaces itself and restarts" +"update.install_failed": "Install failed: {e}" +"update.install_later": "Install on next start" +"update.install_now": "Install now" +"update.not_now": "Not now" +"update.ready": "Version v{v} downloaded and verified, ready to install" +"update.ready_body": "Version v{v} has been downloaded (sha256 verified)." +"update.ready_title": "Update Ready" +"update.recheck": "Check again" +"update.staged": "Staged — it will install on next start" +"update.up_to_date": "You are up to date" +"var.builtin_inline": "Built-in: {{$ts}} (s), {{$ts_ms}} (ms), {{$uuid}} · injected on publish" +"var.dialog_builtin": "Built-in: {{$ts}} (s), {{$ts_ms}} (ms), {{$uuid}}" +"var.dialog_hint": "Reference {{variables}} in publish topics and payloads." +"var.no_reference": "No {{variable}} reference in topic/payload" +"var.preview": "Preview: {tp} | {pl}" +"var.reextract": "Re-extract placeholders" +"var.section": "Variables ({n})" +"var.value": "Value" +"will.enabled_hint": "Enabled" +"will.topic_required": "Will topic cannot be empty" +"will.topic_wildcard": "Will topic cannot contain wildcards + or #" diff --git a/gpui-app/locales/ui.yml b/gpui-app/locales/ui.yml deleted file mode 100644 index 5e970ec..0000000 --- a/gpui-app/locales/ui.yml +++ /dev/null @@ -1,1038 +0,0 @@ -# MQTTX 应用文案(rust-i18n v2 格式,key = 语义标识符 ns.name)。 -# 规则:en/zh-CN 必填、占位符 {x} 两侧一致、调用点全覆盖——ui/i18n.rs 单测强制。 -_version: 2 -"aliyun.access_key_id": - en: "AccessKey ID" - zh-CN: "AccessKey ID" -"aliyun.access_key_secret": - en: "AccessKey Secret" - zh-CN: "AccessKey Secret" -"aliyun.auth_mode": - en: "Auth mode" - zh-CN: "鉴权方式" -"aliyun.auth_token_mode": - en: "Group token" - zh-CN: "Group 令牌 (Token)" -"aliyun.auth_triple_mode": - en: "Per-device secret" - zh-CN: "一机一密" -"aliyun.build_failed": - en: "Failed to build connection: {e}" - zh-CN: "无法生成连接: {e}" -"aliyun.device_id": - en: "Device ID" - zh-CN: "设备 ID" -"aliyun.device_secret": - en: "DeviceSecret (per-device)" - zh-CN: "DeviceSecret(一机一密)" -"aliyun.device_secret_ph": - en: "Triple: DeviceSecret" - zh-CN: "一机一密:DeviceSecret" -"aliyun.generate_connect": - en: "Generate & connect" - zh-CN: "生成并连接" -"aliyun.group": - en: "Group (optional)" - zh-CN: "分组(可选)" -"aliyun.group_id": - en: "Group ID" - zh-CN: "Group ID" -"aliyun.help": - en: "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." - zh-CN: "Token 模式(云消息队列 MQTT 版):接入点 {实例ID}.mqtt.aliyuncs.com,clientId={GroupId}@@@{设备ID},密码为 HMAC-SHA1 签名;一机一密(物联网平台):接入点 {ProductKey}.iot-as-mqtt.{地域}.aliyuncs.com,按官方三元组规范签名" -"aliyun.instance_id": - en: "Instance ID" - zh-CN: "实例 ID" -"aliyun.preset_name": - en: "Preset name" - zh-CN: "预设名称" -"aliyun.product_key": - en: "ProductKey (per-device)" - zh-CN: "ProductKey(一机一密)" -"aliyun.product_key_ph": - en: "Triple: ProductKey" - zh-CN: "一机一密:ProductKey" -"aliyun.region": - en: "Region" - zh-CN: "地域" -"aliyun.region_ph": - en: "e.g. cn-shanghai (optional)" - zh-CN: "如 cn-shanghai(可留空)" -"aliyun.save_preset": - en: "Save preset" - zh-CN: "保存预设" -"aliyun.saved_presets": - en: "Saved presets" - zh-CN: "已保存预设" -"aliyun.title": - en: "Alibaba Cloud IoT" - zh-CN: "阿里云 IoT 设备" -"color.blue": - en: "Blue" - zh-CN: "蓝" -"color.cyan": - en: "Cyan" - zh-CN: "青" -"color.green": - en: "Green" - zh-CN: "绿" -"color.indigo": - en: "Indigo" - zh-CN: "靛" -"color.magenta": - en: "Magenta" - zh-CN: "品红" -"color.orange": - en: "Orange" - zh-CN: "橙" -"color.purple": - en: "Purple" - zh-CN: "紫" -"color.red": - en: "Red" - zh-CN: "红" -"color.rose": - en: "Rose" - zh-CN: "玫红" -"color.yellow": - en: "Yellow" - zh-CN: "黄" -"common.advanced": - en: "Advanced" - zh-CN: "高级" -"common.all": - en: "All" - zh-CN: "全部" -"common.auto": - en: "Auto" - zh-CN: "自动" -"common.browse": - en: "Browse…" - zh-CN: "浏览…" -"common.cancel": - en: "Cancel" - zh-CN: "取消" -"common.clear": - en: "Clear" - zh-CN: "清空" -"common.close": - en: "Close" - zh-CN: "关闭" -"common.collapse": - en: "Collapse" - zh-CN: "收起" -"common.delete": - en: "Delete" - zh-CN: "删除" -"common.edit": - en: "Edit" - zh-CN: "编辑" -"common.new": - en: "New" - zh-CN: "新建" -"common.pause": - en: "Pause" - zh-CN: "暂停" -"common.resume": - en: "Resume" - zh-CN: "恢复" -"common.retry": - en: "Retry" - zh-CN: "重试" -"common.save": - en: "Save" - zh-CN: "保存" -"common.send": - en: "Send" - zh-CN: "发送" -"common.text": - en: "Text" - zh-CN: "文本" -"common.update": - en: "Update" - zh-CN: "更新" -"connview.logs": - en: "Logs" - zh-CN: "日志" -"connview.messages": - en: "Messages" - zh-CN: "消息流" -"connview.subscriptions": - en: "Subscriptions" - zh-CN: "订阅" -"dir.published": - en: "Published" - zh-CN: "发布" -"dir.received": - en: "Received" - zh-CN: "接收" -"encode.base64_failed": - en: "Base64 decode failed: {e}" - zh-CN: "Base64 解码失败: {e}" -"encode.hex_failed": - en: "Hex decode failed: {e}" - zh-CN: "Hex 解码失败: {e}" -"encode.hex_invalid": - en: "Hex decode failed: non-hex character present" - zh-CN: "Hex 解码失败: 含非十六进制字符" -"encode.hex_odd": - en: "Hex length must be even (two chars per byte)" - zh-CN: "Hex 长度必须为偶数(每两个字符一个字节)" -"form.auth_section": - en: "Auth & Keep-alive" - zh-CN: "认证与心跳" -"form.auto_connect": - en: "Connect on app start" - zh-CN: "启动时自动连接" -"form.auto_reconnect": - en: "Auto reconnect on disconnect" - zh-CN: "断线自动重连" -"form.auto_resub": - en: "Auto-resubscribe on connect" - zh-CN: "连接后自动恢复订阅" -"form.basics": - en: "Basics" - zh-CN: "基本信息" -"form.cert_pair_required": - en: "Client certificate and key must be provided together" - zh-CN: "客户端证书与客户端密钥必须同时填写" -"form.clean_start": - en: "Clean Start / Clean Session" - zh-CN: "Clean Start / Clean Session" -"form.client_id": - en: "Client ID" - zh-CN: "Client ID" -"form.client_id_required": - en: "Client ID cannot be empty" - zh-CN: "Client ID 不能为空" -"form.conn_timeout": - en: "Connect timeout (s)" - zh-CN: "连接超时 (秒)" -"form.conn_timeout_invalid": - en: "Connect timeout must be an integer 1-65535 (seconds)" - zh-CN: "连接超时需要是 1~65535 的整数(秒)" -"form.content_type_ph": - en: "Content type (optional)" - zh-CN: "内容类型(可选)" -"form.err_positive": - en: "must be > 0 (0 is an MQTT 5 protocol error)" - zh-CN: "必须大于 0(0 是 MQTT 5 协议错误)" -"form.err_range": - en: "must be an integer 0-65535" - zh-CN: "需要是 0~65535 的整数" -"form.err_uint": - en: "must be a non-negative integer" - zh-CN: "需要是非负整数" -"form.group": - en: "Group" - zh-CN: "分组" -"form.host": - en: "Host" - zh-CN: "主机" -"form.host_required": - en: "Host cannot be empty" - zh-CN: "主机地址不能为空" -"form.keep_alive": - en: "Keep Alive (s)" - zh-CN: "Keep Alive (秒)" -"form.keep_alive_short": - en: "Keep Alive" - zh-CN: "Keep Alive" -"form.max_packet": - en: "Maximum Packet Size" - zh-CN: "最大报文长度" -"form.max_reconnects": - en: "Max reconnects (0 = unlimited)" - zh-CN: "最大重连次数 (0=无限)" -"form.max_reconnects_invalid": - en: "Max reconnects must be a non-negative integer (0 = unlimited)" - zh-CN: "最大重连次数需要是非负整数(0=无限)" -"form.max_reconnects_ph": - en: "0 = unlimited" - zh-CN: "0=无限重连" -"form.name": - en: "Name" - zh-CN: "名称" -"form.name_ph": - en: "Connection name" - zh-CN: "连接名称" -"form.name_required": - en: "Connection name cannot be empty" - zh-CN: "连接名称不能为空" -"form.new_group": - en: "New group" - zh-CN: "新建分组" -"form.new_group_hint": - en: "Overrides the selected group when filled" - zh-CN: "填写后优先于左侧所选分组" -"form.no_group": - en: "No group" - zh-CN: "无分组" -"form.password": - en: "Password" - zh-CN: "密码" -"form.password_ph": - en: "Password (optional)" - zh-CN: "密码(可选)" -"form.path_missing": - en: "{label}: path not found: {p}" - zh-CN: "{label}路径不存在:{p}" -"form.port": - en: "Port" - zh-CN: "端口" -"form.port_invalid": - en: "Port must be an integer 1-65535" - zh-CN: "端口需要是 1~65535 的整数" -"form.protocol": - en: "Protocol" - zh-CN: "协议" -"form.receive_max": - en: "Receive Maximum" - zh-CN: "接收最大值" -"form.receive_max_invalid": - en: "Receive Maximum cannot exceed 65535" - zh-CN: "接收上限不能超过 65535" -"form.receive_max_ph": - en: "Server receive maximum" - zh-CN: "服务端接收上限" -"form.receive_max_short": - en: "Receive Maximum" - zh-CN: "接收上限" -"form.regen_client_id": - en: "Regenerate Client ID" - zh-CN: "重新生成 Client ID" -"form.response_topic_ph": - en: "Response topic (optional)" - zh-CN: "响应主题(可选)" -"form.saved_reconnect": - en: "Saved. Takes effect after reconnect" - zh-CN: "配置已保存,重连后生效" -"form.session_expiry": - en: "Session expiry (s)" - zh-CN: "会话过期间隔(秒)" -"form.session_expiry_invalid": - en: "Session expiry must be a non-negative integer (0 = session ends with connection)" - zh-CN: "会话过期间隔需要是非负整数(0=会话随连接结束)" -"form.session_expiry_ph": - en: "seconds (0 = session ends with connection)" - zh-CN: "秒(0=会话随连接结束)" -"form.test": - en: "Test Connection" - zh-CN: "测试连接" -"form.test_ok": - en: "Connection test succeeded" - zh-CN: "连接测试成功" -"form.testing": - en: "Testing…" - zh-CN: "测试中…" -"form.timeout_ph": - en: "seconds (default 10)" - zh-CN: "秒(默认 10)" -"form.title": - en: "Connection Settings" - zh-CN: "连接配置" -"form.topic_alias": - en: "Topic Alias" - zh-CN: "主题别名" -"form.topic_alias_invalid": - en: "Topic Alias Maximum cannot exceed 65535" - zh-CN: "主题别名上限不能超过 65535" -"form.topic_alias_max": - en: "Topic Alias Maximum" - zh-CN: "主题别名上限" -"form.transport": - en: "Transport" - zh-CN: "传输" -"form.username": - en: "Username" - zh-CN: "用户名" -"form.username_ph": - en: "Username (optional)" - zh-CN: "用户名(可选)" -"form.will_content_type": - en: "Content Type" - zh-CN: "内容类型 (Content Type)" -"form.will_enable": - en: "Enable last will" - zh-CN: "启用遗嘱消息" -"form.will_payload": - en: "Will payload" - zh-CN: "遗嘱内容" -"form.will_payload_ph": - en: "Will message" - zh-CN: "遗嘱消息" -"form.will_response_topic": - en: "Response Topic" - zh-CN: "响应主题 (Response Topic)" -"form.will_section": - en: "Last Will" - zh-CN: "遗嘱消息 (Last Will)" -"form.will_topic": - en: "Will topic" - zh-CN: "遗嘱主题" -"form.ws_path": - en: "WS Path" - zh-CN: "WS 路径" -"form.ws_path_required": - en: "WS path is required for WS/WSS transport" - zh-CN: "WS/WSS 传输下 WS 路径不能为空" -"io.exported": - en: "Exported {n} connections to {path}" - zh-CN: "导出 {n} 条连接到 {path}" -"io.imported": - en: "Imported {n}, skipped {n2}" - zh-CN: "导入 {n} 条,跳过 {n2} 条" -"kv.add": - en: "Add property" - zh-CN: "添加属性" -"msg.binary_hint": - en: " (binary — switch to Hex in details)" - zh-CN: "(非文本,详情可切 Hex)" -"msg.clear_confirm": - en: "Clear all messages of this connection? This cannot be undone." - zh-CN: "确定清空当前连接的全部消息吗?此操作不可撤销。" -"msg.clear_sub_filter": - en: "Clear subscription filter" - zh-CN: "清除订阅过滤" -"msg.clear_title": - en: "Clear Messages" - zh-CN: "清空消息" -"msg.clear_tooltip": - en: "Clear messages of this connection" - zh-CN: "清空当前连接的消息" -"msg.copy_details": - en: "Copy full details (incl. MQTT 5 props)" - zh-CN: "复制完整详情(含 v5 属性)" -"msg.copy_payload": - en: "Copy raw payload" - zh-CN: "复制负载原文" -"msg.copy_topic": - en: "Copy topic" - zh-CN: "复制主题" -"msg.count": - en: "{n} msgs" - zh-CN: "{n} 条" -"msg.details": - en: "Details" - zh-CN: "详情" -"msg.details_copied": - en: "Details copied" - zh-CN: "已复制详情" -"msg.empty": - en: "No messages yet — they will appear here once you subscribe" - zh-CN: "暂无消息,订阅主题后消息会显示在这里" -"msg.expiry": - en: "Message Expiry" - zh-CN: "消息过期" -"msg.filter_placeholder": - en: "Filter by topic or content…" - zh-CN: "过滤主题或内容…" -"msg.latest_only": - en: "Showing latest {n} only" - zh-CN: "仅显示最新 {n} 条" -"msg.log_title": - en: "Connection Logs" - zh-CN: "连接日志" -"msg.no_logs": - en: "No logs yet" - zh-CN: "暂无日志" -"msg.no_match": - en: "No matching messages" - zh-CN: "没有匹配的消息" -"msg.pause_tooltip": - en: "Pause message stream (messages keep arriving in background)" - zh-CN: "暂停消息流滚动(消息仍在后台接收)" -"msg.payload": - en: "Payload" - zh-CN: "负载" -"msg.payload_copied": - en: "Payload copied" - zh-CN: "已复制负载" -"msg.resume_tooltip": - en: "Resume live scrolling" - zh-CN: "恢复实时滚动" -"msg.seconds": - en: "{n}s" - zh-CN: "{n} 秒" -"msg.topic": - en: "Topic" - zh-CN: "主题" -"msg.topic_copied": - en: "Topic copied" - zh-CN: "已复制主题" -"msg.truncated": - en: "Truncated {n}KB" - zh-CN: "已截断 {n}KB" -"preset.default_name": - en: "New preset" - zh-CN: "新预设" -"preset.default_name_n": - en: "New preset {n}" - zh-CN: "新预设 {n}" -"preset.empty": - en: "No presets yet — click \"New\" above to create one" - zh-CN: "暂无发布预设,点击上方「新建」创建" -"preset.manage_title": - en: "Manage Publish Presets" - zh-CN: "管理发布预设" -"preset.name_exists": - en: "Preset name \"{name}\" already exists" - zh-CN: "预设名称「{name}」已存在" -"preset.payload_field": - en: "Payload (stored raw; variables rendered on send)" - zh-CN: "Payload(原文保存,发送时渲染变量)" -"preset.payload_format": - en: "Payload format" - zh-CN: "Payload 格式" -"preset.pick_delete_first": - en: "Select a preset to delete first" - zh-CN: "请先选择要删除的预设" -"preset.pick_first": - en: "Select or create a preset first" - zh-CN: "请先选择或新建预设" -"preset.saved": - en: "Preset \"{name}\" saved" - zh-CN: "预设「{name}」已保存" -"preset.section": - en: "Publish Presets" - zh-CN: "发布预设" -"preset.user_props": - en: "User Properties" - zh-CN: "用户属性" -"pub.collapse": - en: "Collapse publish panel" - zh-CN: "收起发布面板" -"pub.collapsed_hint": - en: "Publish panel collapsed" - zh-CN: "发布面板已收起" -"pub.content_type_ph": - en: "Content-Type (optional)" - zh-CN: "Content-Type(可选)" -"pub.correlation_ph": - en: "Correlation Data (optional)" - zh-CN: "Correlation Data(可选)" -"pub.delete_confirm": - en: "Delete preset \"{name}\"? This cannot be undone." - zh-CN: "确定删除预设「{name}」吗?此操作不可撤销。" -"pub.delete_item": - en: "Delete \"{name}\"" - zh-CN: "删除「{name}」" -"pub.delete_title": - en: "Delete Preset" - zh-CN: "删除预设" -"pub.expand": - en: "Expand publish panel" - zh-CN: "展开发布面板" -"pub.expiry_field": - en: "Expiry (s)" - zh-CN: "消息过期(秒)" -"pub.expiry_invalid": - en: "Message expiry must be a non-negative integer (seconds)" - zh-CN: "消息过期须为非负整数(秒)" -"pub.json_invalid": - en: "Invalid JSON: {e}" - zh-CN: "JSON 格式无效: {e}" -"pub.manage_presets": - en: "Manage presets…" - zh-CN: "管理预设…" -"pub.mqtt5_props": - en: "MQTT 5 Properties" - zh-CN: "MQTT 5 属性" -"pub.offline_not_sent": - en: "Offline: message not sent" - zh-CN: "未连接:消息未发送" -"pub.payload_placeholder": - en: "Message payload; supports {{variables}} and {{$ts}} {{$uuid}}" - zh-CN: "输入消息负载,支持 {{变量名}} 与 {{$ts}} {{$uuid}}" -"pub.payload_preset_ph": - en: "Message payload; supports {{variables}}" - zh-CN: "消息负载,支持 {{变量名}}" -"pub.preset_menu": - en: "Message Presets" - zh-CN: "消息预设" -"pub.preset_name_hint": - en: "Preset name, e.g. temperature-report" - zh-CN: "预设名称,如 温度上报" -"pub.preset_name_required": - en: "Preset name is required" - zh-CN: "请填写预设名称" -"pub.preset_overwritten": - en: "Preset \"{name}\" overwritten" - zh-CN: "已覆盖预设「{name}」" -"pub.preset_saved": - en: "Preset saved" - zh-CN: "预设已保存" -"pub.response_topic_ph": - en: "Response Topic (optional)" - zh-CN: "Response Topic(可选)" -"pub.save_preset": - en: "Save current as preset…" - zh-CN: "保存当前为预设…" -"pub.save_title": - en: "Save Message Preset" - zh-CN: "保存消息预设" -"pub.seconds_hint": - en: "seconds, e.g. 60" - zh-CN: "秒,如 60" -"pub.send_disabled": - en: "Offline — cannot send" - zh-CN: "未连接,无法发送" -"pub.sent_hidden": - en: "Sent (hidden by current filter — not shown in the stream)" - zh-CN: "已发送(被当前过滤条件隐藏,消息流中不显示)" -"pub.topic_placeholder": - en: "Publish topic" - zh-CN: "发布主题" -"pub.topic_preset_ph": - en: "Publish topic; supports {{variables}}" - zh-CN: "发布主题,支持 {{变量名}}" -"pub.topic_required": - en: "Publish topic cannot be empty" - zh-CN: "发布主题不能为空" -"pub.vars": - en: "Variable injection" - zh-CN: "变量注入" -"pub.vars_count": - en: "Variable injection ({n} placeholders)" - zh-CN: "变量注入({n} 个占位符)" -"resmon.auto_refresh": - en: "Auto refresh" - zh-CN: "自动刷新" -"resmon.budget": - en: "Budget {p}% used ({used} / {total})" - zh-CN: "预算占用 {p}%({used} / {total})" -"resmon.committed": - en: "Committed memory" - zh-CN: "提交内存" -"resmon.committed_hint": - en: "Physical pages committed by the process" - zh-CN: "进程独占的物理页承诺" -"resmon.cpu": - en: "CPU usage" - zh-CN: "CPU 占用" -"resmon.cpu_hint": - en: "Normalized by core count" - zh-CN: "按可用核数归一" -"resmon.n_msgs": - en: "{n} msgs" - zh-CN: "{n} 条消息" -"resmon.no_conn": - en: "No connections" - zh-CN: "暂无连接" -"resmon.peak": - en: "Peak {v}" - zh-CN: "峰值 {v}" -"resmon.refresh_now": - en: "Refresh now" - zh-CN: "立即刷新" -"resmon.section": - en: "Messages & Connections" - zh-CN: "消息与连接" -"resmon.stats": - en: "{n} connections ({m} connected) · {t} tabs · {msgs} messages in memory (cap {cap}/conn) · {logs} log lines" - zh-CN: "{n} 个连接(已连接 {m})· 打开标签 {t} · 内存消息 {msgs} 条(每连接上限 {cap} 条)· 日志 {logs} 条" -"resmon.threads_handles": - en: "Threads · handles" - zh-CN: "线程 · 句柄" -"resmon.threads_hint": - en: "tokio engine + GPUI renderer" - zh-CN: "tokio 引擎 + GPUI 渲染" -"resmon.uptime": - en: "Up {t}" - zh-CN: "运行 {t}" -"resmon.version_live": - en: "Version v{v} · refreshes every 1.5s" - zh-CN: "版本 v{v} · 每 1.5 秒自动刷新" -"resmon.version_paused": - en: "Version v{v} · auto-refresh paused" - zh-CN: "版本 v{v} · 自动刷新已暂停" -"resmon.working_set": - en: "Working set" - zh-CN: "工作集内存" -"settings.about": - en: "About" - zh-CN: "关于" -"settings.auto_update": - en: "Check for updates on start" - zh-CN: "自动检查更新(启动时)" -"settings.byte_budget": - en: "Plus a per-connection byte budget of {n} MB: under heavy traffic, oldest messages are evicted by size first" - zh-CN: "另有每连接 {n}MB 的字节预算兜底:大报文流会优先按字节从最旧开始驱逐" -"settings.data_dir": - en: "Data directory" - zh-CN: "数据目录" -"settings.language": - en: "Language" - zh-CN: "界面语言" -"settings.license": - en: "Apache-2.0 license" - zh-CN: "Apache-2.0 许可证" -"settings.log_dir": - en: "Log directory" - zh-CN: "日志目录" -"settings.max_messages": - en: "Messages kept in memory per connection (100-100000)" - zh-CN: "每条连接内存中保留的消息条数(100~100000)" -"settings.mkdir_failed": - en: "Failed to create log folder: {e}" - zh-CN: "创建日志目录失败: {e}" -"settings.open_dir": - en: "Open folder" - zh-CN: "打开目录" -"settings.open_dir_failed": - en: "Failed to open folder: {e}" - zh-CN: "打开目录失败: {e}" -"settings.open_log_dir": - en: "Open log folder" - zh-CN: "打开日志目录" -"settings.show_millis": - en: "Show milliseconds in timestamps" - zh-CN: "时间戳显示毫秒" -"settings.tagline": - en: "A cross-platform MQTT debug client built with GPUI, aiming at MQTTX" - zh-CN: "对标 MQTTX 的 GPUI 跨平台 MQTT 调试客户端" -"settings.theme": - en: "Theme" - zh-CN: "界面主题" -"settings.version_line": - en: "Version {v} (update source: {src})" - zh-CN: "版本 {v}(更新源 {src})" -"sidebar.collapse": - en: "Collapse sidebar (Ctrl+B)" - zh-CN: "收起侧栏(Ctrl+B)" -"sidebar.connections": - en: "Connections" - zh-CN: "连接" -"sidebar.delete": - en: "Delete Connection" - zh-CN: "删除连接" -"sidebar.delete_confirm": - en: "Delete \"{name}\"? This also deletes {sub_count} subscriptions and disconnects. This cannot be undone." - zh-CN: "确定删除「{name}」?将同时删除 {sub_count} 个订阅,并断开当前连接,此操作不可撤销。" -"sidebar.disconnect": - en: "Disconnect" - zh-CN: "断开" -"sidebar.duplicate": - en: "Duplicate" - zh-CN: "复制连接" -"sidebar.empty": - en: "No connections yet. Click + above to create one" - zh-CN: "还没有连接,点击上方 + 新建" -"sidebar.expand": - en: "Expand sidebar (Ctrl+B)" - zh-CN: "展开连接侧栏(Ctrl+B)" -"sidebar.no_match": - en: "No matching connections" - zh-CN: "没有匹配的连接" -"sidebar.search": - en: "Search connections…" - zh-CN: "搜索连接…" -"sidebar.ungrouped": - en: "Ungrouped" - zh-CN: "未分组" -"ssl.ca": - en: "CA certificate (PEM)" - zh-CN: "CA 证书 (PEM)" -"ssl.ca_ph": - en: "Path to CA certificate (PEM, optional)" - zh-CN: "CA 证书 PEM 路径(可选)" -"ssl.cert": - en: "Client certificate (PEM)" - zh-CN: "客户端证书 (PEM)" -"ssl.cert_ph": - en: "Path to client certificate (PEM, optional)" - zh-CN: "客户端证书 PEM 路径(可选)" -"ssl.key": - en: "Client key (PEM)" - zh-CN: "客户端密钥 (PEM)" -"ssl.key_ph": - en: "Path to client private key (PEM, optional)" - zh-CN: "客户端私钥 PEM 路径(可选)" -"ssl.n_configured": - en: "{n} configured" - zh-CN: "已配置 {n} 项" -"ssl.pick_file": - en: "Select certificate / key file" - zh-CN: "选择证书 / 密钥文件" -"ssl.section": - en: "SSL/TLS" - zh-CN: "SSL/TLS" -"ssl.skip_verify": - en: "Skip CA verification (trust any server cert)" - zh-CN: "忽略 CA 校验(不验证服务器证书)" -"status.connected": - en: "Connected" - zh-CN: "已连接" -"status.connecting": - en: "Connecting" - zh-CN: "连接中" -"status.disconnected": - en: "Disconnected" - zh-CN: "未连接" -"status.error": - en: "Error" - zh-CN: "错误" -"sub.alias": - en: "Alias (optional)" - zh-CN: "别名(可选)" -"sub.cancel_edit": - en: "Cancel editing" - zh-CN: "取消编辑" -"sub.collapse": - en: "Collapse subscriptions panel" - zh-CN: "收起订阅面板" -"sub.color": - en: "Subscription color" - zh-CN: "订阅颜色" -"sub.color_clear": - en: "Clear color" - zh-CN: "清除颜色" -"sub.color_random": - en: "Random color" - zh-CN: "随机颜色" -"sub.disable": - en: "Disable subscription" - zh-CN: "停用订阅" -"sub.edit": - en: "Edit subscription" - zh-CN: "编辑订阅" -"sub.editing": - en: "Editing: {topic}" - zh-CN: "正在编辑:{topic}" -"sub.empty_hint": - en: "Subscribe to a topic and messages will show up here" - zh-CN: "订阅主题后,消息会显示在右侧" -"sub.enable": - en: "Enable subscription" - zh-CN: "启用订阅" -"sub.expand": - en: "Expand subscriptions panel" - zh-CN: "展开订阅面板" -"sub.failed": - en: "Subscribe failed: {e}" - zh-CN: "订阅失败: {e}" -"sub.identifier": - en: "Subscription Identifier" - zh-CN: "订阅标识符" -"sub.identifier_invalid": - en: "Subscription identifier must be a positive integer" - zh-CN: "订阅标识符须为正整数" -"sub.mqtt5_options": - en: "MQTT 5 Subscription Options" - zh-CN: "MQTT 5 订阅选项" -"sub.no_local": - en: "No Local" - zh-CN: "No Local" -"sub.rap": - en: "Retain As Published" - zh-CN: "Retain As Published" -"sub.retain_handling": - en: "Retain Handling" - zh-CN: "Retain Handling" -"sub.rh0": - en: "0 Send at every subscribe" - zh-CN: "0 每次发送" -"sub.rh1": - en: "1 Send only for new subscription" - zh-CN: "1 仅新订阅" -"sub.rh2": - en: "2 Do not send" - zh-CN: "2 不发送" -"sub.saved_offline": - en: "Offline: subscription saved" - zh-CN: "未连接:订阅已保存" -"sub.saved_offline_resub": - en: "Offline: subscription saved, restored on connect" - zh-CN: "未连接:订阅已保存,连接后自动恢复" -"sub.topic_placeholder": - en: "Topic filter, supports # +; separate multiple with commas/spaces" - zh-CN: "订阅主题,支持 # +,多个用逗号/空格分隔" -"sub.topic_required": - en: "Topic filter cannot be empty" - zh-CN: "订阅主题不能为空" -"sub.unsubscribe": - en: "Unsubscribe" - zh-CN: "取消订阅" -"tab.welcome_hint": - en: "Pick a connection on the left, or create a new one to start debugging" - zh-CN: "从左侧选择一个连接,或新建连接开始调试" -"tab.welcome_title": - en: "Get started with MQTTX" - zh-CN: "开始使用 MQTTX" -"theme.dark": - en: "Dark" - zh-CN: "深色" -"theme.light": - en: "Light" - zh-CN: "浅色" -"theme.system": - en: "Follow system" - zh-CN: "跟随系统" -"titlebar.aliyun": - en: "Alibaba Cloud Devices" - zh-CN: "阿里云设备" -"titlebar.export": - en: "Export Connections" - zh-CN: "导出连接" -"titlebar.import": - en: "Import Connections" - zh-CN: "导入连接" -"titlebar.new_connection": - en: "New Connection" - zh-CN: "新建连接" -"titlebar.pin": - en: "Pin window on top" - zh-CN: "置顶窗口" -"titlebar.pin_failed": - en: "Failed to set always-on-top" - zh-CN: "置顶操作失败" -"titlebar.resmon": - en: "Resource Monitor" - zh-CN: "资源监控" -"titlebar.settings": - en: "Settings" - zh-CN: "设置" -"titlebar.subtitle": - en: "MQTT debug client" - zh-CN: "MQTT 调试客户端" -"titlebar.switch_dark": - en: "Switch to dark theme" - zh-CN: "切换为深色" -"titlebar.switch_light": - en: "Switch to light theme" - zh-CN: "切换为浅色" -"titlebar.unpin": - en: "Unpin window" - zh-CN: "取消窗口置顶" -"titlebar.variables": - en: "Global Variables" - zh-CN: "全局变量" -"update.apply": - en: "Update to v{v} now" - zh-CN: "立即更新到 v{v}" -"update.check": - en: "Check for updates" - zh-CN: "检查更新" -"update.check_hint": - en: "Check GitHub Releases for a new version" - zh-CN: "检查 GitHub Releases 上的新版本" -"update.checking": - en: "Checking…" - zh-CN: "检查中…" -"update.checking_long": - en: "Checking for updates…" - zh-CN: "正在检查更新…" -"update.discard": - en: "Discard this update" - zh-CN: "放弃此次更新" -"update.discarded": - en: "Staged update discarded" - zh-CN: "已删除暂存的更新包" -"update.downloading": - en: "Downloading {pct}% ({done} / {total})" - zh-CN: "下载中 {pct}%({done} / {total})" -"update.downloading_size": - en: "Downloading {done}" - zh-CN: "下载中 {done}" -"update.failed": - en: "Update failed: {e}" - zh-CN: "更新失败:{e}" -"update.found": - en: "New version v{v} found — downloads, replaces itself and restarts" - zh-CN: "发现新版本 v{v},下载后自动替换重启" -"update.install_failed": - en: "Install failed: {e}" - zh-CN: "安装失败:{e}" -"update.install_later": - en: "Install on next start" - zh-CN: "下次启动安装" -"update.install_now": - en: "Install now" - zh-CN: "立即安装" -"update.not_now": - en: "Not now" - zh-CN: "暂不更新" -"update.ready": - en: "Version v{v} downloaded and verified, ready to install" - zh-CN: "新版本 v{v} 已下载并通过校验,等待安装" -"update.ready_body": - en: "Version v{v} has been downloaded (sha256 verified)." - zh-CN: "新版本 v{v} 已下载完成(已通过 sha256 校验)。" -"update.ready_title": - en: "Update Ready" - zh-CN: "更新就绪" -"update.recheck": - en: "Check again" - zh-CN: "重新检查" -"update.staged": - en: "Staged — it will install on next start" - zh-CN: "已暂存,下次启动时将自动安装" -"update.up_to_date": - en: "You are up to date" - zh-CN: "已是最新版本" -"var.builtin_inline": - en: "Built-in: {{$ts}} (s), {{$ts_ms}} (ms), {{$uuid}} · injected on publish" - zh-CN: "内置:{{$ts}}(秒) {{$ts_ms}}(毫秒) {{$uuid}} · 发布时注入" -"var.dialog_builtin": - en: "Built-in: {{$ts}} (s), {{$ts_ms}} (ms), {{$uuid}}" - zh-CN: "内置变量:{{$ts}}(秒)、{{$ts_ms}}(毫秒)、{{$uuid}}" -"var.dialog_hint": - en: "Reference {{variables}} in publish topics and payloads." - zh-CN: "在发布主题、消息负载中使用 {{变量名}} 引用。" -"var.no_reference": - en: "No {{variable}} reference in topic/payload" - zh-CN: "主题/负载中没有 {{变量}} 引用" -"var.preview": - en: "Preview: {tp} | {pl}" - zh-CN: "预览:{tp} | {pl}" -"var.reextract": - en: "Re-extract placeholders" - zh-CN: "重新提取占位符" -"var.section": - en: "Variables ({n})" - zh-CN: "变量({n})" -"var.value": - en: "Value" - zh-CN: "变量值" -"will.enabled_hint": - en: "Enabled" - zh-CN: "已启用" -"will.topic_required": - en: "Will topic cannot be empty" - zh-CN: "遗嘱主题不能为空" -"will.topic_wildcard": - en: "Will topic cannot contain wildcards + or #" - zh-CN: "遗嘱主题不能包含通配符 + 或 #" -"io.err_item_id": - en: "Item {n}: id must not be empty" - zh-CN: "第 {n} 项的 id 不能为空" -"io.err_item_invalid": - en: "Item {n} is not a valid connection: {e}" - zh-CN: "第 {n} 项不是合法的连接配置: {e}" -"io.err_missing_field": - en: "JSON object lacks the \"connections\" array" - zh-CN: "JSON 对象中缺少 \"connections\" 数组字段" -"io.err_parse_json": - en: "Failed to parse JSON: {e}" - zh-CN: "解析 JSON 失败: {e}" -"io.err_read": - en: "Failed to read {p}: {e}" - zh-CN: "读取 {p} 失败: {e}" -"io.err_serialize": - en: "Failed to serialize connections: {e}" - zh-CN: "序列化连接失败: {e}" -"io.err_utf8": - en: "{p} is not valid UTF-8" - zh-CN: "{p} 不是有效的 UTF-8 文件" -"io.err_write": - en: "Failed to write {p}: {e}" - zh-CN: "写入 {p} 失败: {e}" -"sidebar.dup_suffix": - en: "{name} (copy)" - zh-CN: "{name} 副本" -"sub.identifier_field": - en: "Subscription Identifier" - zh-CN: "订阅标识符" -"io.err_parse_json_path": - en: "Failed to parse {p}" - zh-CN: "解析 {p} 失败" diff --git a/gpui-app/locales/zh-CN.yml b/gpui-app/locales/zh-CN.yml new file mode 100644 index 0000000..627e28c --- /dev/null +++ b/gpui-app/locales/zh-CN.yml @@ -0,0 +1,347 @@ +# MQTTX zh-CN 文案(rust-i18n:文件名即 locale,扁平 key: value)。 +# key = 语义标识符 ns.name;en/zh-CN 两文件键集合必须一致——单测强制。 +"aliyun.access_key_id": "AccessKey ID" +"aliyun.access_key_secret": "AccessKey Secret" +"aliyun.auth_mode": "鉴权方式" +"aliyun.auth_token_mode": "Group 令牌 (Token)" +"aliyun.auth_triple_mode": "一机一密" +"aliyun.build_failed": "无法生成连接: {e}" +"aliyun.device_id": "设备 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.instance_id": "实例 ID" +"aliyun.preset_name": "预设名称" +"aliyun.product_key": "ProductKey(一机一密)" +"aliyun.product_key_ph": "一机一密:ProductKey" +"aliyun.region": "地域" +"aliyun.region_ph": "如 cn-shanghai(可留空)" +"aliyun.save_preset": "保存预设" +"aliyun.saved_presets": "已保存预设" +"aliyun.title": "阿里云 IoT 设备" +"color.blue": "蓝" +"color.cyan": "青" +"color.green": "绿" +"color.indigo": "靛" +"color.magenta": "品红" +"color.orange": "橙" +"color.purple": "紫" +"color.red": "红" +"color.rose": "玫红" +"color.yellow": "黄" +"common.advanced": "高级" +"common.all": "全部" +"common.auto": "自动" +"common.browse": "浏览…" +"common.cancel": "取消" +"common.clear": "清空" +"common.close": "关闭" +"common.collapse": "收起" +"common.delete": "删除" +"common.edit": "编辑" +"common.new": "新建" +"common.pause": "暂停" +"common.resume": "恢复" +"common.retry": "重试" +"common.save": "保存" +"common.send": "发送" +"common.text": "文本" +"common.update": "更新" +"connview.logs": "日志" +"connview.messages": "消息流" +"connview.subscriptions": "订阅" +"dir.published": "发布" +"dir.received": "接收" +"encode.base64_failed": "Base64 解码失败: {e}" +"encode.hex_failed": "Hex 解码失败: {e}" +"encode.hex_invalid": "Hex 解码失败: 含非十六进制字符" +"encode.hex_odd": "Hex 长度必须为偶数(每两个字符一个字节)" +"form.auth_section": "认证与心跳" +"form.auto_connect": "启动时自动连接" +"form.auto_reconnect": "断线自动重连" +"form.auto_resub": "连接后自动恢复订阅" +"form.basics": "基本信息" +"form.cert_pair_required": "客户端证书与客户端密钥必须同时填写" +"form.clean_start": "Clean Start / Clean Session" +"form.client_id": "Client ID" +"form.client_id_required": "Client ID 不能为空" +"form.conn_timeout": "连接超时 (秒)" +"form.conn_timeout_invalid": "连接超时需要是 1~65535 的整数(秒)" +"form.content_type_ph": "内容类型(可选)" +"form.err_positive": "必须大于 0(0 是 MQTT 5 协议错误)" +"form.err_range": "需要是 0~65535 的整数" +"form.err_uint": "需要是非负整数" +"form.group": "分组" +"form.host": "主机" +"form.host_required": "主机地址不能为空" +"form.keep_alive": "Keep Alive (秒)" +"form.keep_alive_short": "Keep Alive" +"form.max_packet": "最大报文长度" +"form.max_reconnects": "最大重连次数 (0=无限)" +"form.max_reconnects_invalid": "最大重连次数需要是非负整数(0=无限)" +"form.max_reconnects_ph": "0=无限重连" +"form.name": "名称" +"form.name_ph": "连接名称" +"form.name_required": "连接名称不能为空" +"form.new_group": "新建分组" +"form.new_group_hint": "填写后优先于左侧所选分组" +"form.no_group": "无分组" +"form.password": "密码" +"form.password_ph": "密码(可选)" +"form.path_missing": "{label}路径不存在:{p}" +"form.port": "端口" +"form.port_invalid": "端口需要是 1~65535 的整数" +"form.protocol": "协议" +"form.receive_max": "接收最大值" +"form.receive_max_invalid": "接收上限不能超过 65535" +"form.receive_max_ph": "服务端接收上限" +"form.receive_max_short": "接收上限" +"form.regen_client_id": "重新生成 Client ID" +"form.response_topic_ph": "响应主题(可选)" +"form.saved_reconnect": "配置已保存,重连后生效" +"form.session_expiry": "会话过期间隔(秒)" +"form.session_expiry_invalid": "会话过期间隔需要是非负整数(0=会话随连接结束)" +"form.session_expiry_ph": "秒(0=会话随连接结束)" +"form.test": "测试连接" +"form.test_ok": "连接测试成功" +"form.testing": "测试中…" +"form.timeout_ph": "秒(默认 10)" +"form.title": "连接配置" +"form.topic_alias": "主题别名" +"form.topic_alias_invalid": "主题别名上限不能超过 65535" +"form.topic_alias_max": "主题别名上限" +"form.transport": "传输" +"form.username": "用户名" +"form.username_ph": "用户名(可选)" +"form.will_content_type": "内容类型 (Content Type)" +"form.will_enable": "启用遗嘱消息" +"form.will_payload": "遗嘱内容" +"form.will_payload_ph": "遗嘱消息" +"form.will_response_topic": "响应主题 (Response Topic)" +"form.will_section": "遗嘱消息 (Last Will)" +"form.will_topic": "遗嘱主题" +"form.ws_path": "WS 路径" +"form.ws_path_required": "WS/WSS 传输下 WS 路径不能为空" +"io.err_item_id": "第 {n} 项的 id 不能为空" +"io.err_item_invalid": "第 {n} 项不是合法的连接配置: {e}" +"io.err_missing_field": "JSON 对象中缺少 \"connections\" 数组字段" +"io.err_parse_json": "解析 JSON 失败: {e}" +"io.err_parse_json_path": "解析 {p} 失败" +"io.err_read": "读取 {p} 失败: {e}" +"io.err_serialize": "序列化连接失败: {e}" +"io.err_utf8": "{p} 不是有效的 UTF-8 文件" +"io.err_write": "写入 {p} 失败: {e}" +"io.exported": "导出 {n} 条连接到 {path}" +"io.imported": "导入 {n} 条,跳过 {n2} 条" +"kv.add": "添加属性" +"msg.binary_hint": "(非文本,详情可切 Hex)" +"msg.clear_confirm": "确定清空当前连接的全部消息吗?此操作不可撤销。" +"msg.clear_sub_filter": "清除订阅过滤" +"msg.clear_title": "清空消息" +"msg.clear_tooltip": "清空当前连接的消息" +"msg.copy_details": "复制完整详情(含 v5 属性)" +"msg.copy_payload": "复制负载原文" +"msg.copy_topic": "复制主题" +"msg.count": "{n} 条" +"msg.details": "详情" +"msg.details_copied": "已复制详情" +"msg.empty": "暂无消息,订阅主题后消息会显示在这里" +"msg.expiry": "消息过期" +"msg.filter_placeholder": "过滤主题或内容…" +"msg.latest_only": "仅显示最新 {n} 条" +"msg.log_title": "连接日志" +"msg.no_logs": "暂无日志" +"msg.no_match": "没有匹配的消息" +"msg.pause_tooltip": "暂停消息流滚动(消息仍在后台接收)" +"msg.payload": "负载" +"msg.payload_copied": "已复制负载" +"msg.resume_tooltip": "恢复实时滚动" +"msg.seconds": "{n} 秒" +"msg.topic": "主题" +"msg.topic_copied": "已复制主题" +"msg.truncated": "已截断 {n}KB" +"preset.default_name": "新预设" +"preset.default_name_n": "新预设 {n}" +"preset.empty": "暂无发布预设,点击上方「新建」创建" +"preset.manage_title": "管理发布预设" +"preset.name_exists": "预设名称「{name}」已存在" +"preset.payload_field": "Payload(原文保存,发送时渲染变量)" +"preset.payload_format": "Payload 格式" +"preset.pick_delete_first": "请先选择要删除的预设" +"preset.pick_first": "请先选择或新建预设" +"preset.saved": "预设「{name}」已保存" +"preset.section": "发布预设" +"preset.user_props": "用户属性" +"pub.collapse": "收起发布面板" +"pub.collapsed_hint": "发布面板已收起" +"pub.content_type_ph": "Content-Type(可选)" +"pub.correlation_ph": "Correlation Data(可选)" +"pub.delete_confirm": "确定删除预设「{name}」吗?此操作不可撤销。" +"pub.delete_item": "删除「{name}」" +"pub.delete_title": "删除预设" +"pub.expand": "展开发布面板" +"pub.expiry_field": "消息过期(秒)" +"pub.expiry_invalid": "消息过期须为非负整数(秒)" +"pub.json_invalid": "JSON 格式无效: {e}" +"pub.manage_presets": "管理预设…" +"pub.mqtt5_props": "MQTT 5 属性" +"pub.offline_not_sent": "未连接:消息未发送" +"pub.payload_placeholder": "输入消息负载,支持 {{变量名}} 与 {{$ts}} {{$uuid}}" +"pub.payload_preset_ph": "消息负载,支持 {{变量名}}" +"pub.preset_menu": "消息预设" +"pub.preset_name_hint": "预设名称,如 温度上报" +"pub.preset_name_required": "请填写预设名称" +"pub.preset_overwritten": "已覆盖预设「{name}」" +"pub.preset_saved": "预设已保存" +"pub.response_topic_ph": "Response Topic(可选)" +"pub.save_preset": "保存当前为预设…" +"pub.save_title": "保存消息预设" +"pub.seconds_hint": "秒,如 60" +"pub.send_disabled": "未连接,无法发送" +"pub.sent_hidden": "已发送(被当前过滤条件隐藏,消息流中不显示)" +"pub.topic_placeholder": "发布主题" +"pub.topic_preset_ph": "发布主题,支持 {{变量名}}" +"pub.topic_required": "发布主题不能为空" +"pub.vars": "变量注入" +"pub.vars_count": "变量注入({n} 个占位符)" +"resmon.auto_refresh": "自动刷新" +"resmon.budget": "预算占用 {p}%({used} / {total})" +"resmon.committed": "提交内存" +"resmon.committed_hint": "进程独占的物理页承诺" +"resmon.cpu": "CPU 占用" +"resmon.cpu_hint": "按可用核数归一" +"resmon.n_msgs": "{n} 条消息" +"resmon.no_conn": "暂无连接" +"resmon.peak": "峰值 {v}" +"resmon.refresh_now": "立即刷新" +"resmon.section": "消息与连接" +"resmon.stats": "{n} 个连接(已连接 {m})· 打开标签 {t} · 内存消息 {msgs} 条(每连接上限 {cap} 条)· 日志 {logs} 条" +"resmon.threads_handles": "线程 · 句柄" +"resmon.threads_hint": "tokio 引擎 + GPUI 渲染" +"resmon.uptime": "运行 {t}" +"resmon.version_live": "版本 v{v} · 每 1.5 秒自动刷新" +"resmon.version_paused": "版本 v{v} · 自动刷新已暂停" +"resmon.working_set": "工作集内存" +"settings.about": "关于" +"settings.auto_update": "自动检查更新(启动时)" +"settings.byte_budget": "另有每连接 {n}MB 的字节预算兜底:大报文流会优先按字节从最旧开始驱逐" +"settings.data_dir": "数据目录" +"settings.language": "界面语言" +"settings.license": "Apache-2.0 许可证" +"settings.log_dir": "日志目录" +"settings.max_messages": "每条连接内存中保留的消息条数(100~100000)" +"settings.mkdir_failed": "创建日志目录失败: {e}" +"settings.open_dir": "打开目录" +"settings.open_dir_failed": "打开目录失败: {e}" +"settings.open_log_dir": "打开日志目录" +"settings.show_millis": "时间戳显示毫秒" +"settings.tagline": "对标 MQTTX 的 GPUI 跨平台 MQTT 调试客户端" +"settings.theme": "界面主题" +"settings.version_line": "版本 {v}(更新源 {src})" +"sidebar.collapse": "收起侧栏(Ctrl+B)" +"sidebar.connections": "连接" +"sidebar.delete": "删除连接" +"sidebar.delete_confirm": "确定删除「{name}」?将同时删除 {sub_count} 个订阅,并断开当前连接,此操作不可撤销。" +"sidebar.disconnect": "断开" +"sidebar.dup_suffix": "{name} 副本" +"sidebar.duplicate": "复制连接" +"sidebar.empty": "还没有连接,点击上方 + 新建" +"sidebar.expand": "展开连接侧栏(Ctrl+B)" +"sidebar.no_match": "没有匹配的连接" +"sidebar.search": "搜索连接…" +"sidebar.ungrouped": "未分组" +"ssl.ca": "CA 证书 (PEM)" +"ssl.ca_ph": "CA 证书 PEM 路径(可选)" +"ssl.cert": "客户端证书 (PEM)" +"ssl.cert_ph": "客户端证书 PEM 路径(可选)" +"ssl.key": "客户端密钥 (PEM)" +"ssl.key_ph": "客户端私钥 PEM 路径(可选)" +"ssl.n_configured": "已配置 {n} 项" +"ssl.pick_file": "选择证书 / 密钥文件" +"ssl.section": "SSL/TLS" +"ssl.skip_verify": "忽略 CA 校验(不验证服务器证书)" +"status.connected": "已连接" +"status.connecting": "连接中" +"status.disconnected": "未连接" +"status.error": "错误" +"sub.alias": "别名(可选)" +"sub.cancel_edit": "取消编辑" +"sub.collapse": "收起订阅面板" +"sub.color": "订阅颜色" +"sub.color_clear": "清除颜色" +"sub.color_random": "随机颜色" +"sub.disable": "停用订阅" +"sub.edit": "编辑订阅" +"sub.editing": "正在编辑:{topic}" +"sub.empty_hint": "订阅主题后,消息会显示在右侧" +"sub.enable": "启用订阅" +"sub.expand": "展开订阅面板" +"sub.failed": "订阅失败: {e}" +"sub.identifier": "订阅标识符" +"sub.identifier_field": "订阅标识符" +"sub.identifier_invalid": "订阅标识符须为正整数" +"sub.mqtt5_options": "MQTT 5 订阅选项" +"sub.no_local": "No Local" +"sub.rap": "Retain As Published" +"sub.retain_handling": "Retain Handling" +"sub.rh0": "0 每次发送" +"sub.rh1": "1 仅新订阅" +"sub.rh2": "2 不发送" +"sub.saved_offline": "未连接:订阅已保存" +"sub.saved_offline_resub": "未连接:订阅已保存,连接后自动恢复" +"sub.topic_placeholder": "订阅主题,支持 # +,多个用逗号/空格分隔" +"sub.topic_required": "订阅主题不能为空" +"sub.unsubscribe": "取消订阅" +"tab.welcome_hint": "从左侧选择一个连接,或新建连接开始调试" +"tab.welcome_title": "开始使用 MQTTX" +"theme.dark": "深色" +"theme.light": "浅色" +"theme.system": "跟随系统" +"titlebar.aliyun": "阿里云设备" +"titlebar.export": "导出连接" +"titlebar.import": "导入连接" +"titlebar.new_connection": "新建连接" +"titlebar.pin": "置顶窗口" +"titlebar.pin_failed": "置顶操作失败" +"titlebar.resmon": "资源监控" +"titlebar.settings": "设置" +"titlebar.subtitle": "MQTT 调试客户端" +"titlebar.switch_dark": "切换为深色" +"titlebar.switch_light": "切换为浅色" +"titlebar.unpin": "取消窗口置顶" +"titlebar.variables": "全局变量" +"update.apply": "立即更新到 v{v}" +"update.check": "检查更新" +"update.check_hint": "检查 GitHub Releases 上的新版本" +"update.checking": "检查中…" +"update.checking_long": "正在检查更新…" +"update.discard": "放弃此次更新" +"update.discarded": "已删除暂存的更新包" +"update.downloading": "下载中 {pct}%({done} / {total})" +"update.downloading_size": "下载中 {done}" +"update.failed": "更新失败:{e}" +"update.found": "发现新版本 v{v},下载后自动替换重启" +"update.install_failed": "安装失败:{e}" +"update.install_later": "下次启动安装" +"update.install_now": "立即安装" +"update.not_now": "暂不更新" +"update.ready": "新版本 v{v} 已下载并通过校验,等待安装" +"update.ready_body": "新版本 v{v} 已下载完成(已通过 sha256 校验)。" +"update.ready_title": "更新就绪" +"update.recheck": "重新检查" +"update.staged": "已暂存,下次启动时将自动安装" +"update.up_to_date": "已是最新版本" +"var.builtin_inline": "内置:{{$ts}}(秒) {{$ts_ms}}(毫秒) {{$uuid}} · 发布时注入" +"var.dialog_builtin": "内置变量:{{$ts}}(秒)、{{$ts_ms}}(毫秒)、{{$uuid}}" +"var.dialog_hint": "在发布主题、消息负载中使用 {{变量名}} 引用。" +"var.no_reference": "主题/负载中没有 {{变量}} 引用" +"var.preview": "预览:{tp} | {pl}" +"var.reextract": "重新提取占位符" +"var.section": "变量({n})" +"var.value": "变量值" +"will.enabled_hint": "已启用" +"will.topic_required": "遗嘱主题不能为空" +"will.topic_wildcard": "遗嘱主题不能包含通配符 + 或 #" diff --git a/gpui-app/src/ui/app/actions.rs b/gpui-app/src/ui/app/actions.rs index cc2fca0..791b25b 100644 --- a/gpui-app/src/ui/app/actions.rs +++ b/gpui-app/src/ui/app/actions.rs @@ -21,6 +21,7 @@ impl MqttXApp { self.connections.push(cfg); } self.storage.save_connections(&self.connections); + self.normalize_group_filter(); } pub fn duplicate_connection(&mut self, id: &str) { @@ -36,6 +37,18 @@ impl MqttXApp { self.storage.save_connections(&self.connections); } + /// 分组被改名/删除后过滤器可能悬空(列表恒空),在数据变更的 action + /// 路径复位到「全部」;render 只读,不做突变。 + fn normalize_group_filter(&mut self) { + if let GroupFilter::Named(name) = &self.group_filter + && !self.connections.iter().any(|c| { + c.group.as_deref().map(str::trim) == Some(name.as_str()) + }) + { + self.group_filter = GroupFilter::All; + } + } + pub fn delete_connection(&mut self, id: &str, cx: &mut Context) { self.engine.close(id, false); self.connections.retain(|c| c.id != id); @@ -50,6 +63,7 @@ impl MqttXApp { } self.storage.save_connections(&self.connections); self.storage.save_subscriptions(&self.subscriptions); + self.normalize_group_filter(); self.persist_ui_state(); cx.notify(); } diff --git a/gpui-app/src/ui/app/sidebar.rs b/gpui-app/src/ui/app/sidebar.rs index 956e98f..00ea996 100644 --- a/gpui-app/src/ui/app/sidebar.rs +++ b/gpui-app/src/ui/app/sidebar.rs @@ -52,14 +52,8 @@ impl MqttXApp { } pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let query = self.search.read(cx).value().to_lowercase(); let search = self.search.clone(); - // 分组被改名/删除后过滤器可能悬空(列表恒空),渲染前复位到「全部」 - let dangling = matches!(&self.group_filter, GroupFilter::Named(name) - if !self.connections.iter().any(|c| { - c.group.as_deref().map(str::trim) == Some(name.as_str()) - })); - if dangling { - self.group_filter = GroupFilter::All; - } + // 分组过滤器复位发生在删除/保存连接的 action 路径(normalize_group_filter), + // render 只读——规范禁止渲染期突变状态 let filter = self.group_filter.clone(); let total = self.connections.len(); diff --git a/gpui-app/src/ui/app/titlebar.rs b/gpui-app/src/ui/app/titlebar.rs index d54dd25..3cea52e 100644 --- a/gpui-app/src/ui/app/titlebar.rs +++ b/gpui-app/src/ui/app/titlebar.rs @@ -10,6 +10,7 @@ use crate::model::ThemeModePref; use crate::ui::IconName; use super::*; +use gpui_kit::Action as _; use crate::ui::i18n; impl MqttXApp { @@ -107,11 +108,7 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context< .dropdown_menu(move |menu, _, _| { let w1 = app_weak.clone(); let w2 = app_weak.clone(); - let w3 = app_weak.clone(); - let w4 = app_weak.clone(); - let w5 = app_weak.clone(); - let w6 = app_weak.clone(); - menu.item( + menu.item( PopupMenuItem::new(i18n::t("titlebar.aliyun")) .on_click(move |_, window, cx| { if let Some(entity) = w1.upgrade() { @@ -128,36 +125,27 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context< )) .item(PopupMenuItem::new(i18n::t("titlebar.resmon")).on_click( move |_, window, cx| { - if let Some(entity) = w6.upgrade() { - crate::ui::dialogs::resource_dialog::open(entity, window, cx); - } + window.dispatch_action( + OpenResourceMonitor.boxed_clone(), + cx, + ); }, )) .separator() .item(PopupMenuItem::new(i18n::t("titlebar.export")).on_click( move |_, window, cx| { - if let Some(entity) = w4.upgrade() { - entity.update(cx, |app, cx| { - app.export_connections(window, cx) - }); - } + window.dispatch_action(ExportConnections.boxed_clone(), cx); }, )) .item(PopupMenuItem::new(i18n::t("titlebar.import")).on_click( move |_, window, cx| { - if let Some(entity) = w5.upgrade() { - entity.update(cx, |app, cx| { - app.import_connections(window, cx) - }); - } + window.dispatch_action(ImportConnections.boxed_clone(), cx); }, )) .separator() .item(PopupMenuItem::new(i18n::t("titlebar.settings")).on_click( move |_, window, cx| { - if let Some(entity) = w3.upgrade() { - crate::ui::dialogs::settings_dialog::open(entity, window, cx); - } + window.dispatch_action(OpenSettings.boxed_clone(), cx); }, )) }), diff --git a/gpui-app/src/ui/connection_view/messages.rs b/gpui-app/src/ui/connection_view/messages.rs index 9584bea..a4f95f2 100644 --- a/gpui-app/src/ui/connection_view/messages.rs +++ b/gpui-app/src/ui/connection_view/messages.rs @@ -6,7 +6,7 @@ use gpui_kit::component::dialog::DialogButtonProps; use gpui_kit::component::input::Input; use gpui_kit::component::{ h_flex, notification::Notification, v_flex, }; use crate::ui::IconName; -use gpui_kit::{ div, hsla, px, ClipboardItem, Context, IntoElement, SharedString, }; +use gpui_kit::{ div, px, ClipboardItem, Context, IntoElement, SharedString, }; use crate::model::{ render_will_templates, ConnectionStatus, Direction, LogLevel, MqttRecord, Subscription, MAX_PAYLOAD_RETAIN, }; use crate::ui::widgets::format_time; @@ -408,7 +408,7 @@ pub(super) fn render_top_bar(&self, cx: &mut Context) -> impl IntoElement .border_color(border) .hover(move |this| this.bg(hover_bg)) .cursor_pointer() - // 左侧色条:有订阅色时显示,否则透明以保持行内对齐; + // 左侧色条:有订阅色时条件上色(无色不设 bg,天然透明保持行内对齐); // 上下留 2px 间隙,避免相邻行的色条连成一整条竖线 .child( div() @@ -416,7 +416,7 @@ pub(super) fn render_top_bar(&self, cx: &mut Context) -> impl IntoElement .self_stretch() .my_0p5() .rounded_full() - .bg(sub_color.map(hue_color).unwrap_or(hsla(0., 0., 0., 0.))), + .when_some(sub_color, |bar, hue| bar.bg(hue_color(hue))), ) .child(body) .on_click(cx.listener(move |this, _, _, cx| { diff --git a/gpui-app/src/ui/connection_view/subscribe.rs b/gpui-app/src/ui/connection_view/subscribe.rs index 1a030cd..fac6516 100644 --- a/gpui-app/src/ui/connection_view/subscribe.rs +++ b/gpui-app/src/ui/connection_view/subscribe.rs @@ -8,7 +8,7 @@ use gpui_kit::component::select::Select; use gpui_kit::component::switch::Switch; use gpui_kit::component::{ h_flex, notification::Notification, v_flex, }; use crate::ui::IconName; -use gpui_kit::{ div, hsla, px, Context, IntoElement, SharedString, Window, }; +use gpui_kit::{ div, px, Context, IntoElement, SharedString, Window, }; use gpui_kit::component::IndexPath; use crate::model::{ SubscribeOptions, Subscription, }; @@ -577,6 +577,9 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context) -> impl IntoEl .child("+"), ) }; + // 色点描边用主题底色(双主题下都是「与背景的缝隙」语义); + // 闭包签名拿不到带主题的 cx,提前在 render 取值捕获 + let swatch_border = cx.theme().background; trigger.dropdown_menu(move |mut menu, _, _| { for (hue, name) in PRESET_HUES { let weak = weak_menu.clone(); @@ -595,7 +598,7 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context) -> impl IntoEl .rounded_full() .bg(swatch) .border_1() - .border_color(hsla(0., 0., 1., 0.6)), + .border_color(swatch_border), ) .child(div().text_sm().child(label.clone())) }) diff --git a/gpui-app/src/ui/i18n.rs b/gpui-app/src/ui/i18n.rs index 82dac4b..2dff5f1 100644 --- a/gpui-app/src/ui/i18n.rs +++ b/gpui-app/src/ui/i18n.rs @@ -1,9 +1,11 @@ //! i18n 门面:语义键(`ns.key`)+ rust-i18n(gpui-kit 官方方案)。 //! //! # 架构 -//! - 语言包在 `locales/ui.yml`(en / zh-CN 双语并排,`_version: 2`), +//! - 语言包在 `locales/`,**按语言一分文件**(`en.yml` / `zh-CN.yml`, +//! rust-i18n 以文件名 stem 识别 locale,扁平 `"ns.key": "文案"` 格式), //! 由 crate 根的 `rust_i18n::i18n!("locales", fallback = "en")` 编译期内嵌; -//! gpui-kit 组件内置文案经 main.rs 的 `rust_i18n::extend!` 随语言切换。 +//! gpui-kit 组件内置文案经 lib.rs 的 `init_i18n()`(`rust_i18n::extend!`) +//! 随语言切换。 //! - 调用点写 `t("common.save")` / `tf("msg.count", &[("n", &count)])`, //! key 是**稳定标识符**,与任何自然语言解耦——改文案措辞不动代码, //! 也不存在「一词多义撞 key」。 @@ -12,7 +14,8 @@ //! 值同样可查。 //! //! # 规则(单元测试强制,违反即测试失败) -//! 1. 新增文案:调用点 `t("ns.key")`,`locales/ui.yml` 加 key 块(en/zh-CN 必填)。 +//! 1. 新增文案:调用点 `t("ns.key")`,`locales/en.yml` 与 `locales/zh-CN.yml` +//! 各加一行(两文件键集合必须一致)。 //! 2. 源码每个 `t("…")` / `tf("…")` 字面量必须在语言包中存在。 //! 3. 模型 `label()` 返回的 key 必须有**非自映射**的 en 词条。 //! 4. 同一 key 的 zh-CN 与 en 占位符 `{name}` 集合必须一致 @@ -127,43 +130,33 @@ fn placeholders(s: &str) -> Vec { out } -/// 解析生成器产出的规整 YAML(key 行 + 缩进的 en/zh-CN 值行)。 +/// 解析 `locales/{locale}.yml`(规整格式:每行 `"key": "value"`,# 注释)。 /// 格式意外时 panic——语言包是构建产物,格式受控。 #[cfg(test)] -fn load_ui_yml() -> std::collections::BTreeMap { - let text = std::fs::read_to_string( - std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("locales/ui.yml"), - ) - .expect("locales/ui.yml 应存在"); - let unquote = |v: &str| -> String { - let v = v.trim(); - let inner = v - .strip_prefix('"') - .and_then(|s| s.strip_suffix('"')) - .unwrap_or(v); - inner.replace("\\\"", "\"").replace("\\\\", "\\") - }; +fn load_locale_yml(locale: &str) -> std::collections::BTreeMap { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("locales") + .join(format!("{locale}.yml")); + let text = + std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{} 应存在: {e}", path.display())); let mut map = std::collections::BTreeMap::new(); - let mut key: Option = None; - let mut en: Option = None; - let mut zh: Option = None; for line in text.lines() { - if line.starts_with('#') || line.starts_with("_version") || line.trim().is_empty() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { continue; } - if !line.starts_with(' ') { - if let (Some(k), Some(e), Some(z)) = (key.take(), en.take(), zh.take()) { - map.insert(k, (e, z)); - } - key = Some(unquote(line.trim_end_matches(':'))); - } else if let Some(v) = line.trim().strip_prefix("en:") { - en = Some(unquote(v)); - } else if let Some(v) = line.trim().strip_prefix("zh-CN:") { - zh = Some(unquote(v)); - } - } - if let (Some(k), Some(e), Some(z)) = (key, en, zh) { - map.insert(k, (e, z)); + let rest = line.strip_prefix('"').unwrap_or_else(|| panic!("行缺开引号: {line:?}")); + let (key, value) = rest + .split_once("\": ") + .unwrap_or_else(|| panic!("行缺 `\": ` 分隔: {line:?}")); + let unquote_value = |v: &str| { + let inner = v + .strip_prefix('"') + .and_then(|s| s.strip_suffix('"')) + .unwrap_or_else(|| panic!("值缺引号: {v:?}")); + inner.replace("\\\"", "\"").replace("\\\\", "\\") + }; + map.insert(key.to_string(), unquote_value(value)); } map } @@ -172,17 +165,26 @@ fn load_ui_yml() -> std::collections::BTreeMap { mod tests { use super::*; - /// 每个 key 都必须有非空 en 与 zh-CN;占位符两侧集合一致。 + /// 两个语言文件键集合必须一致;每个 key 的 en/zh-CN 非空且 + /// 占位符两侧集合一致。 #[test] fn yml_complete_and_placeholder_parity() { - let map = load_ui_yml(); - assert!(!map.is_empty(), "语言包不能为空"); - for (k, (en, zh)) in &map { - assert!(!en.trim().is_empty(), "en 为空: {k:?}"); - assert!(!zh.trim().is_empty(), "zh-CN 为空: {k:?}"); + let en = load_locale_yml("en"); + let zh = load_locale_yml("zh-CN"); + assert!(!en.is_empty(), "en 语言包不能为空"); + let en_only: Vec<_> = en.keys().filter(|k| !zh.contains_key(*k)).collect(); + let zh_only: Vec<_> = zh.keys().filter(|k| !en.contains_key(*k)).collect(); + assert!( + en_only.is_empty() && zh_only.is_empty(), + "键集合不一致: en 独有 {en_only:?}, zh 独有 {zh_only:?}" + ); + for (k, en_v) in &en { + let zh_v = &zh[k]; + assert!(!en_v.trim().is_empty(), "en 为空: {k:?}"); + assert!(!zh_v.trim().is_empty(), "zh-CN 为空: {k:?}"); assert_eq!( - placeholders(zh), - placeholders(en), + placeholders(zh_v), + placeholders(en_v), "占位符两侧不一致: {k:?}" ); } @@ -190,9 +192,10 @@ mod tests { /// 扫描 src/ 全部源码里的 `t("…")` / `tf("…")` 字面量, /// 逐一断言语言包有词条——漏翻是测试失败,不再是静默回退。 + /// `(` 与 `"` 之间允许空白/换行(覆盖多行调用写法)。 #[test] fn call_sites_covered_by_yml() { - let map = load_ui_yml(); + let map = load_locale_yml("en"); let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); let mut files = Vec::new(); collect_rs(&root, &mut files); @@ -251,9 +254,8 @@ mod tests { } /// 手写扫描(避免引入 regex 依赖):找 `::t(` / `::tf(` 后的字面量。 - /// `(` 与 `"` 之间允许空白/换行——覆盖多行调用的写法 - /// `i18n::tf(\n "key",\n &[...]`;`::t(` 针不会命中 `::tf(` - /// (f 不是空白,也到不了引号分支)。 + /// `(` 与 `"` 之间允许空白/换行——覆盖多行调用的写法;`::t(` 针不会 + /// 命中 `::tf(`(f 不是空白,也到不了引号分支)。 fn extract_t_literals(src: &str) -> Vec { let b = src.as_bytes(); let mut out = Vec::new(); diff --git a/gpui-app/src/update/check.rs b/gpui-app/src/update/check.rs index 9c55e18..e82abad 100644 --- a/gpui-app/src/update/check.rs +++ b/gpui-app/src/update/check.rs @@ -165,8 +165,34 @@ fn tag_from_releases_url(uri: &str) -> Option<&str> { (tag.starts_with('v') && !tag.contains('/')).then_some(tag) } +/// 自动检查的节流窗口:24h 内启动不再重复请求 GitHub(官方 auto-update +/// 指南建议缓存上次检查时间)。手动「检查更新」不受此限制。 +const AUTO_CHECK_INTERVAL_SECS: i64 = 24 * 60 * 60; + +fn stamp_path() -> std::path::PathBuf { + crate::store::Storage::new().dir().join(".last-update-check") +} + +fn last_check_recent() -> bool { + let Ok(meta) = std::fs::metadata(stamp_path()) else { return false }; + let Ok(modified) = meta.modified() else { return false }; + modified + .elapsed() + .map(|d| d.as_secs() < AUTO_CHECK_INTERVAL_SECS as u64) + .unwrap_or(false) +} + +fn touch_check_stamp() { + let _ = std::fs::write(stamp_path(), chrono::Local::now().to_rfc3339()); +} + /// 返回 Ok(None) 表示已是最新;错误仅记录、不打扰用户。 +/// 走 24h 节流:距上次自动检查不足一天时直接跳过(返回 None)。 pub fn check_and_download() -> Result, String> { + if last_check_recent() { + return Ok(None); + } + touch_check_stamp(); let Some(info) = check_latest()? else { return Ok(None); };