From 5db0972263a01bec8cda7b12d25007315f08354c Mon Sep 17 00:00:00 2001 From: JoeAllen <2494136784@qq.com> Date: Sun, 4 Oct 2026 23:49:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(i18n):=20=E5=85=A8=E5=B1=80=E5=A4=8D?= =?UTF-8?q?=E6=9F=A5=E4=BF=AE=E5=A4=8D=E2=80=94=E2=80=94=E5=A4=9A=E8=A1=8C?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E7=82=B9=E6=BC=8F=E6=9B=BF=E6=8D=A2=E3=80=81?= =?UTF-8?q?=E5=8A=A8=E6=80=81=20label=20=E5=9B=9E=E5=BD=92=E4=B8=8E?= =?UTF-8?q?=E6=AD=BB=E9=94=AE=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复查发现(audit 脚本 + 修正后的单测扫描器): - 27 处多行 tf("中文") 调用点未被键迁移的单行替换覆盖,英文模式下 回退显示中文;正则补丁全部换为语义键 - DirFilter/DetailFormat::label() 仍返回中文常量,t() 动态查键必然 miss(英文模式方向过滤/格式按钮显示中文);label() 改返回 common.all/dir.*、common.auto/text(Hex/Base64 语言中性字面量) - 单测扫描器与 audit 脚本的共同盲区:t(/tf( 与引号之间的空白换行 不被匹配;扫描器改为跳过空白后取字面量,多行调用纳入强制覆盖 - 6 处 UI 直写补翻:连接测试成功通知、复制连接"(copy)"后缀、 rfd 导入/导出对话框标题、设置对话框标题、订阅标识符字段标签 - store 导入/导出错误与 PayloadFormat 编码错误(均经 Notification 展示)key 化,共 14 条新词条 - 删除 10 个真死键;mqtt/tls/update 层诊断错误维持中文(技术诊断 流,不进 UI 渲染路径),边界记录于模块文档 语言包现状:354 键、en/zh-CN 双语并排、零死键、零 UI 中文直写; 42 测试全绿;英文模式实机截图复核(含回归点)通过。 --- gpui-app/locales/ui.yml | 63 ++++++++++---------- gpui-app/src/model/message.rs | 10 ++-- gpui-app/src/store.rs | 20 +++---- gpui-app/src/ui/app/actions.rs | 2 +- gpui-app/src/ui/app/events.rs | 2 +- gpui-app/src/ui/app/io.rs | 8 +-- gpui-app/src/ui/app/mod.rs | 4 +- gpui-app/src/ui/app/sidebar.rs | 2 +- gpui-app/src/ui/connection_form.rs | 2 +- gpui-app/src/ui/connection_view/messages.rs | 2 +- gpui-app/src/ui/connection_view/mod.rs | 12 ++-- gpui-app/src/ui/connection_view/publish.rs | 4 +- gpui-app/src/ui/connection_view/subscribe.rs | 2 +- gpui-app/src/ui/connection_view/vars.rs | 4 +- gpui-app/src/ui/dialogs/presets_dialog.rs | 6 +- gpui-app/src/ui/dialogs/resource_dialog.rs | 10 ++-- gpui-app/src/ui/dialogs/settings_dialog.rs | 18 +++--- gpui-app/src/ui/i18n.rs | 20 ++++--- 18 files changed, 101 insertions(+), 90 deletions(-) diff --git a/gpui-app/locales/ui.yml b/gpui-app/locales/ui.yml index 80a4263..5e970ec 100644 --- a/gpui-app/locales/ui.yml +++ b/gpui-app/locales/ui.yml @@ -130,27 +130,18 @@ _version: 2 "common.new": en: "New" zh-CN: "新建" -"common.ok": - en: "OK" - zh-CN: "确定" "common.pause": en: "Pause" zh-CN: "暂停" "common.resume": en: "Resume" zh-CN: "恢复" -"common.retain": - en: "Retain" - zh-CN: "Retain" "common.retry": en: "Retry" zh-CN: "重试" "common.save": en: "Save" zh-CN: "保存" -"common.saved": - en: "Saved" - zh-CN: "已保存" "common.send": en: "Send" zh-CN: "发送" @@ -160,9 +151,6 @@ _version: 2 "common.update": en: "Update" zh-CN: "更新" -"connview.connecting": - en: "Connecting…" - zh-CN: "连接中…" "connview.logs": en: "Logs" zh-CN: "日志" @@ -247,9 +235,6 @@ _version: 2 "form.keep_alive": en: "Keep Alive (s)" zh-CN: "Keep Alive (秒)" -"form.keep_alive_invalid": - en: "Keep Alive must be an integer 0-65535" - zh-CN: "Keep Alive (秒) 需要是 0~65535 的整数" "form.keep_alive_short": en: "Keep Alive" zh-CN: "Keep Alive" @@ -400,9 +385,6 @@ _version: 2 "kv.add": en: "Add property" zh-CN: "添加属性" -"kv.remove": - en: "Remove this property" - zh-CN: "删除此属性" "msg.binary_hint": en: " (binary — switch to Hex in details)" zh-CN: "(非文本,详情可切 Hex)" @@ -490,18 +472,12 @@ _version: 2 "preset.empty": en: "No presets yet — click \"New\" above to create one" zh-CN: "暂无发布预设,点击上方「新建」创建" -"preset.list": - en: "Presets" - zh-CN: "预设列表" "preset.manage_title": en: "Manage Publish Presets" zh-CN: "管理发布预设" "preset.name_exists": en: "Preset name \"{name}\" already exists" zh-CN: "预设名称「{name}」已存在" -"preset.new": - en: "New Preset" - zh-CN: "新建预设" "preset.payload_field": en: "Payload (stored raw; variables rendered on send)" zh-CN: "Payload(原文保存,发送时渲染变量)" @@ -661,9 +637,6 @@ _version: 2 "resmon.threads_hint": en: "tokio engine + GPUI renderer" zh-CN: "tokio 引擎 + GPUI 渲染" -"resmon.title_hint": - en: "Self-process resource sampling" - zh-CN: "自身进程资源采样" "resmon.uptime": en: "Up {t}" zh-CN: "运行 {t}" @@ -727,9 +700,6 @@ _version: 2 "sidebar.collapse": en: "Collapse sidebar (Ctrl+B)" zh-CN: "收起侧栏(Ctrl+B)" -"sidebar.collapse_short": - en: "Collapse sidebar" - zh-CN: "收起侧栏" "sidebar.connections": en: "Connections" zh-CN: "连接" @@ -1033,3 +1003,36 @@ _version: 2 "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/src/model/message.rs b/gpui-app/src/model/message.rs index bb4e2bc..1cbc02c 100644 --- a/gpui-app/src/model/message.rs +++ b/gpui-app/src/model/message.rs @@ -53,23 +53,23 @@ impl PayloadFormat { use base64::Engine as _; base64::engine::general_purpose::STANDARD .decode(text.trim()) - .map_err(|e| format!("Base64 解码失败: {e}")) + .map_err(|e| crate::ui::i18n::tf("encode.base64_failed", &[("e", &e.to_string())])) } Self::Hex => { let cleaned: String = text.chars().filter(|c| !c.is_whitespace()).collect(); if !cleaned.len().is_multiple_of(2) { - return Err("Hex 长度必须为偶数(每两个字符一个字节)".into()); + return Err(crate::ui::i18n::t("encode.hex_odd").into()); } // 必须先做全字节校验:长度检查按字节计数,多字节字符(如中文) // 可凑成偶数字节,直接按字节索引切片会落在字符边界内而 panic。 if !cleaned.bytes().all(|b| b.is_ascii_hexdigit()) { - return Err("Hex 解码失败: 含非十六进制字符".into()); + return Err(crate::ui::i18n::t("encode.hex_invalid").into()); } (0..cleaned.len()) .step_by(2) .map(|i| { u8::from_str_radix(&cleaned[i..i + 2], 16) - .map_err(|e| format!("Hex 解码失败: {e}")) + .map_err(|e| crate::ui::i18n::tf("encode.hex_failed", &[("e", &e.to_string())])) }) .collect() } @@ -154,7 +154,7 @@ pub fn compute_preview(payload: &str) -> String { } }; if payload.contains('\u{FFFD}') { - out.push_str("(非文本,详情可切 Hex)"); + out.push_str(crate::ui::i18n::t("msg.binary_hint")); } out } diff --git a/gpui-app/src/store.rs b/gpui-app/src/store.rs index 09eeb10..57cf09a 100644 --- a/gpui-app/src/store.rs +++ b/gpui-app/src/store.rs @@ -72,7 +72,7 @@ impl Storage { fn read_json_inner serde::Deserialize<'de>>(&self, p: &Path) -> Result> { let mut text = std::fs::read_to_string(p) - .with_context(|| format!("读取 {} 失败", p.display()))?; + .with_context(|| crate::ui::i18n::tf("io.err_read", &[("p", &p.display().to_string())]))?; // 容忍 Windows 编辑器/PowerShell 写入的 UTF-8 BOM(BOM 只有 1 个字符,逐字符剥离) if let Some(rest) = text.strip_prefix('\u{feff}') { text = rest.to_string(); @@ -81,7 +81,7 @@ impl Storage { return Ok(None); } let value = - serde_json::from_str(&text).with_context(|| format!("解析 {} 失败", p.display()))?; + serde_json::from_str(&text).with_context(|| crate::ui::i18n::tf("io.err_parse_json_path", &[("p", &p.display().to_string())]))?; Ok(Some(value)) } @@ -245,8 +245,8 @@ impl Default for Storage { /// 导出连接为 JSON 数组(与 connections.json 同构:缩进格式化、UTF-8 无 BOM)。 pub fn export_connections_to(path: &Path, conns: &[ConnectionConfig]) -> Result<(), String> { let text = serde_json::to_string_pretty(conns) - .map_err(|e| format!("序列化连接失败: {e}"))?; - std::fs::write(path, text).map_err(|e| format!("写入 {} 失败: {e}", path.display())) + .map_err(|e| crate::ui::i18n::tf("io.err_serialize", &[("e", &e.to_string())]))?; + std::fs::write(path, text).map_err(|e| crate::ui::i18n::tf("io.err_write", &[("p", &path.display().to_string()), ("e", &e.to_string())])) } /// 从 JSON 文件导入连接。 @@ -254,21 +254,21 @@ pub fn export_connections_to(path: &Path, conns: &[ConnectionConfig]) -> Result< /// 容错规则:容忍 UTF-8 BOM;顶层允许是数组,也可能是 `{ "connections": [...] }` 对象; /// 任一项不是合法连接配置、或 `id` 为空白时,返回带序号的错误信息。 pub fn import_connections_from(path: &Path) -> Result, String> { - let bytes = std::fs::read(path).map_err(|e| format!("读取 {} 失败: {e}", path.display()))?; + let bytes = std::fs::read(path).map_err(|e| crate::ui::i18n::tf("io.err_read", &[("p", &path.display().to_string()), ("e", &e.to_string())]))?; let mut text = - String::from_utf8(bytes).map_err(|_| format!("{} 不是有效的 UTF-8 文件", path.display()))?; + String::from_utf8(bytes).map_err(|_| crate::ui::i18n::tf("io.err_utf8", &[("p", &path.display().to_string())]))?; if let Some(rest) = text.strip_prefix('\u{feff}') { text = rest.to_string(); } let value: serde_json::Value = - serde_json::from_str(text.trim()).map_err(|e| format!("解析 JSON 失败: {e}"))?; + serde_json::from_str(text.trim()).map_err(|e| crate::ui::i18n::tf("io.err_parse_json", &[("e", &e.to_string())]))?; let items = match value { serde_json::Value::Array(items) => items, serde_json::Value::Object(mut map) => match map.remove("connections") { Some(serde_json::Value::Array(items)) => items, _ => { return Err( - "JSON 对象中缺少 \"connections\" 数组字段".to_string(), + crate::ui::i18n::t("io.err_missing_field").to_string(), ); } }, @@ -280,11 +280,11 @@ pub fn import_connections_from(path: &Path) -> Result, Str Ok(c) => { // 空 id 会在去重/订阅关联时产生悬空引用,导入层直接拒绝 if c.id.trim().is_empty() { - return Err(format!("第 {} 项的 id 不能为空", i + 1)); + return Err(crate::ui::i18n::tf("io.err_item_id", &[("n", &(i + 1))])); } conns.push(c); } - Err(e) => return Err(format!("第 {} 项不是合法的连接配置: {e}", i + 1)), + Err(e) => return Err(crate::ui::i18n::tf("io.err_item_invalid", &[("n", &(i + 1)), ("e", &e.to_string())])), } } Ok(conns) diff --git a/gpui-app/src/ui/app/actions.rs b/gpui-app/src/ui/app/actions.rs index e4ad5be..cc2fca0 100644 --- a/gpui-app/src/ui/app/actions.rs +++ b/gpui-app/src/ui/app/actions.rs @@ -29,7 +29,7 @@ impl MqttXApp { }; let mut copy = src; copy.id = uuid::Uuid::new_v4().to_string(); - copy.name = format!("{} 副本", copy.name); + copy.name = i18n::tf("sidebar.dup_suffix", &[("name", ©.name)]); copy.client_id = format!("mqttx_{}", &uuid::Uuid::new_v4().simple().to_string()[..8]); self.connections.push(copy); diff --git a/gpui-app/src/ui/app/events.rs b/gpui-app/src/ui/app/events.rs index 020cb42..abe34c6 100644 --- a/gpui-app/src/ui/app/events.rs +++ b/gpui-app/src/ui/app/events.rs @@ -112,7 +112,7 @@ pub(super) fn on_engine_event(&mut self, event: EngineEvent, window: &mut Window if let Some(e) = error { window.push_notification( Notification::error(i18n::tf( - "订阅失败: {e}", + "sub.failed", &[("e", &e)], )), cx, diff --git a/gpui-app/src/ui/app/io.rs b/gpui-app/src/ui/app/io.rs index dec28f3..93a799f 100644 --- a/gpui-app/src/ui/app/io.rs +++ b/gpui-app/src/ui/app/io.rs @@ -64,7 +64,7 @@ impl MqttXApp { self.spawn_file_dialog( move || { rfd::FileDialog::new() - .set_title("导出连接") + .set_title(i18n::t("titlebar.export")) .set_directory(dir) .set_file_name("connections-export.json") .add_filter("JSON", &["json"]) @@ -78,7 +78,7 @@ impl MqttXApp { match crate::store::export_connections_to(&path, &app.connections) { Ok(()) => window.push_notification( Notification::success(i18n::tf( - "导出 {n} 条连接到 {path}", + "io.exported", &[ ("n", &count), ("path", &path.display()), @@ -100,7 +100,7 @@ impl MqttXApp { self.spawn_file_dialog( move || { let Some(path) = rfd::FileDialog::new() - .set_title("导入连接") + .set_title(i18n::t("titlebar.import")) .set_directory(dir) .add_filter("JSON", &["json"]) .pick_file() @@ -134,7 +134,7 @@ impl MqttXApp { } window.push_notification( Notification::success(i18n::tf( - "导入 {n} 条,跳过 {n2} 条", + "io.imported", &[ ("n", &added), ("n2", &skipped), diff --git a/gpui-app/src/ui/app/mod.rs b/gpui-app/src/ui/app/mod.rs index 2feae74..15d3b71 100644 --- a/gpui-app/src/ui/app/mod.rs +++ b/gpui-app/src/ui/app/mod.rs @@ -220,7 +220,7 @@ impl MqttXApp { .child( v_flex().gap_2().child( div().text_sm().child(i18n::tf( - "新版本 v{v} 已下载完成(已通过 sha256 校验)。", + "update.ready_body", &[("v", &staged.version)], )), ), @@ -263,7 +263,7 @@ impl MqttXApp { { window.push_notification( Notification::error(i18n::tf( - "安装失败:{e}", + "update.install_failed", &[("e", &e), ])), cx, diff --git a/gpui-app/src/ui/app/sidebar.rs b/gpui-app/src/ui/app/sidebar.rs index b26e86c..956e98f 100644 --- a/gpui-app/src/ui/app/sidebar.rs +++ b/gpui-app/src/ui/app/sidebar.rs @@ -411,7 +411,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context window.push_notification( - Notification::success("连接测试成功"), + Notification::success(i18n::t("form.test_ok")), cx, ), Err(e) => window.push_notification(Notification::error(format!( diff --git a/gpui-app/src/ui/connection_view/messages.rs b/gpui-app/src/ui/connection_view/messages.rs index c27066d..9584bea 100644 --- a/gpui-app/src/ui/connection_view/messages.rs +++ b/gpui-app/src/ui/connection_view/messages.rs @@ -644,7 +644,7 @@ pub(super) fn render_messages(&self, cx: &mut Context) -> impl IntoElement .px_1p5() .text_color(cx.theme().muted_foreground) .child(i18n::tf( - "仅显示最新 {n} 条", + "msg.latest_only", &[("n", &MAX_RENDERED_MESSAGES)], )), ) diff --git a/gpui-app/src/ui/connection_view/mod.rs b/gpui-app/src/ui/connection_view/mod.rs index 7139226..6f0f601 100644 --- a/gpui-app/src/ui/connection_view/mod.rs +++ b/gpui-app/src/ui/connection_view/mod.rs @@ -75,11 +75,12 @@ enum DirFilter { } impl DirFilter { + /// 返回 i18n 语义 key;Hex/Base64 为语言中性字面量。 fn label(self) -> &'static str { match self { - Self::All => "全部", - Self::Received => "接收", - Self::Published => "发布", + Self::All => "common.all", + Self::Received => "dir.received", + Self::Published => "dir.published", } } @@ -111,10 +112,11 @@ enum DetailFormat { } impl DetailFormat { + /// 返回 i18n 语义 key;Hex/Base64 为语言中性字面量。 fn label(self) -> &'static str { match self { - Self::Auto => "自动", - Self::Text => "文本", + Self::Auto => "common.auto", + Self::Text => "common.text", Self::Hex => "Hex", Self::Base64 => "Base64", } diff --git a/gpui-app/src/ui/connection_view/publish.rs b/gpui-app/src/ui/connection_view/publish.rs index 9d4ac9d..b0c0d39 100644 --- a/gpui-app/src/ui/connection_view/publish.rs +++ b/gpui-app/src/ui/connection_view/publish.rs @@ -280,7 +280,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context) -> impl Into .when(show_vars, |b| b.selected(true)) .tooltip(if count > 0 { i18n::tf( - "变量注入({n} 个占位符)", + "pub.vars_count", &[("n", &count)], ) } else { @@ -491,7 +491,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context) -> impl Into alert .title(i18n::t("pub.delete_title")) .description(i18n::tf( - "确定删除预设「{name}」吗?此操作不可撤销。", + "pub.delete_confirm", &[("name", &pname)], )) .button_props( diff --git a/gpui-app/src/ui/connection_view/subscribe.rs b/gpui-app/src/ui/connection_view/subscribe.rs index 4750627..1a030cd 100644 --- a/gpui-app/src/ui/connection_view/subscribe.rs +++ b/gpui-app/src/ui/connection_view/subscribe.rs @@ -457,7 +457,7 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context) -> impl In .child( div() .w(px(150.)) - .child(field("订阅标识符", Input::new(&sub_identifier).small())), + .child(field(i18n::t("sub.identifier_field"), Input::new(&sub_identifier).small())), ) .child( h_flex() diff --git a/gpui-app/src/ui/connection_view/vars.rs b/gpui-app/src/ui/connection_view/vars.rs index 21dd2be..c68eb18 100644 --- a/gpui-app/src/ui/connection_view/vars.rs +++ b/gpui-app/src/ui/connection_view/vars.rs @@ -119,7 +119,7 @@ pub(super) fn render_vars_panel(&self, cx: &mut Context) -> impl IntoEleme .text_xs() .font_semibold() .child(i18n::tf( - "变量({n})", + "var.section", &[("n", &self.var_rows.len())], )), ) @@ -160,7 +160,7 @@ pub(super) fn render_vars_panel(&self, cx: &mut Context) -> impl IntoEleme .text_xs() .text_color(muted) .child(i18n::tf( - "预览:{tp} | {pl}", + "var.preview", &[("tp", &topic_rendered), ("pl", &payload_preview)], )), ) diff --git a/gpui-app/src/ui/dialogs/presets_dialog.rs b/gpui-app/src/ui/dialogs/presets_dialog.rs index 84e88d2..9f5628b 100644 --- a/gpui-app/src/ui/dialogs/presets_dialog.rs +++ b/gpui-app/src/ui/dialogs/presets_dialog.rs @@ -300,7 +300,7 @@ impl PresetsDialog { { window.push_notification( Notification::error(i18n::tf( - "预设名称「{name}」已存在", + "preset.name_exists", &[("name", &name)], )), cx, @@ -324,7 +324,7 @@ impl PresetsDialog { } self.persist(cx); window.push_notification(Notification::success(i18n::tf( - "预设「{name}」已保存", + "preset.saved", &[("name", &name)], )), cx); } @@ -345,7 +345,7 @@ impl PresetsDialog { alert .title(i18n::t("pub.delete_title")) .description(i18n::tf( - "确定删除预设「{name}」吗?此操作不可撤销。", + "pub.delete_confirm", &[("name", &pname)], )) .button_props( diff --git a/gpui-app/src/ui/dialogs/resource_dialog.rs b/gpui-app/src/ui/dialogs/resource_dialog.rs index ff2e017..f9f96db 100644 --- a/gpui-app/src/ui/dialogs/resource_dialog.rs +++ b/gpui-app/src/ui/dialogs/resource_dialog.rs @@ -264,7 +264,7 @@ impl ResourceMonitorDialog { .text_xs() .text_color(muted) .child(i18n::tf( - "预算占用 {p}%({used} / {total})", + "resmon.budget", &[ ("p", &format!("{:.0}", pct)), ("used", &Self::fmt_bytes(c.bytes as u64)), @@ -384,7 +384,7 @@ impl Render for ResourceMonitorDialog { i18n::t("resmon.working_set"), Self::fmt_bytes(ws), Some(i18n::tf( - "峰值 {v}", + "resmon.peak", &[("v", &Self::fmt_bytes(s.peak_working_set))], )), Some(ws as f32 / peak as f32 * 100.), @@ -442,7 +442,7 @@ impl Render for ResourceMonitorDialog { .text_xs() .text_color(muted) .child(i18n::tf( - "{n} 个连接(已连接 {m})· 打开标签 {t} · 内存消息 {msgs} 条(每连接上限 {cap} 条)· 日志 {logs} 条", + "resmon.stats", &[ ("n", &self.conns.len()), ("m", &connected_count), @@ -466,12 +466,12 @@ impl Render for ResourceMonitorDialog { .text_color(muted) .child(if self.auto_refresh { i18n::tf( - "版本 v{v} · 每 1.5 秒自动刷新", + "resmon.version_live", &[("v", &crate::update::current_version())], ) } else { i18n::tf( - "版本 v{v} · 自动刷新已暂停", + "resmon.version_paused", &[("v", &crate::update::current_version())], ) }), diff --git a/gpui-app/src/ui/dialogs/settings_dialog.rs b/gpui-app/src/ui/dialogs/settings_dialog.rs index 2ee181a..423499d 100644 --- a/gpui-app/src/ui/dialogs/settings_dialog.rs +++ b/gpui-app/src/ui/dialogs/settings_dialog.rs @@ -213,7 +213,7 @@ impl SettingsDialog { row( Button::new("apply-update") .label(i18n::tf( - "立即更新到 v{v}", + "update.apply", &[("v", &ver)], )) .primary() @@ -223,7 +223,7 @@ impl SettingsDialog { })) .into_any_element(), i18n::tf( - "发现新版本 v{v},下载后自动替换重启", + "update.found", &[("v", &ver)], ) .into_any_element(), @@ -240,7 +240,7 @@ impl SettingsDialog { .text_xs() .text_color(cx.theme().accent) .child(i18n::tf( - "新版本 v{v} 已下载并通过校验,等待安装", + "update.ready", &[("v", &ver)], )), ) @@ -279,7 +279,7 @@ impl SettingsDialog { this.update = UpdateUi::Idle; window.push_notification( Notification::info(i18n::t( - "已暂存,下次启动时将自动安装", + "update.staged", )), cx, ); @@ -313,7 +313,7 @@ impl SettingsDialog { }; let status = if *total > 0 { i18n::tf( - "下载中 {pct}%({done} / {total})", + "update.downloading", &[ ("pct", &format!("{:.0}", pct * 100.)), ("done", &Self::fmt_mb(*downloaded)), @@ -322,7 +322,7 @@ impl SettingsDialog { ) } else { i18n::tf( - "下载中 {done}", + "update.downloading_size", &[("done", &Self::fmt_mb(*downloaded))], ) }; @@ -426,7 +426,7 @@ impl Render for SettingsDialog { .text_xs() .text_color(cx.theme().muted_foreground) .child(i18n::tf( - "另有每连接 {n}MB 的字节预算兜底:大报文流会优先按字节从最旧开始驱逐", + "settings.byte_budget", &[ ("n", &format!("{}", MAX_CONNECTION_MESSAGE_BYTES / (1024 * 1024))), ], @@ -534,7 +534,7 @@ impl Render for SettingsDialog { .text_xs() .text_color(cx.theme().muted_foreground) .child(i18n::tf( - "版本 {v}(更新源 {src})", + "settings.version_line", &[ ("v", &update::current_version()), ("src", &update::UPDATE_SOURCE), @@ -596,7 +596,7 @@ pub fn open(app: Entity, window: &mut Window, cx: &mut App) { let app_for_save = app_save.clone(); dialog .w(px(560.)) - .title("设置") + .title(i18n::t("titlebar.settings")) .child(body) .footer( h_flex() diff --git a/gpui-app/src/ui/i18n.rs b/gpui-app/src/ui/i18n.rs index da7736d..82dac4b 100644 --- a/gpui-app/src/ui/i18n.rs +++ b/gpui-app/src/ui/i18n.rs @@ -250,20 +250,26 @@ mod tests { } } - /// 手写扫描(避免引入 regex 依赖):找 `::t("` / `::tf("` 后的字面量。 - /// 两个针互不误伤:`::t("` 要求引号紧跟 t,不会命中 `::tf("`。 + /// 手写扫描(避免引入 regex 依赖):找 `::t(` / `::tf(` 后的字面量。 + /// `(` 与 `"` 之间允许空白/换行——覆盖多行调用的写法 + /// `i18n::tf(\n "key",\n &[...]`;`::t(` 针不会命中 `::tf(` + /// (f 不是空白,也到不了引号分支)。 fn extract_t_literals(src: &str) -> Vec { let b = src.as_bytes(); let mut out = Vec::new(); - for needle in [b"::tf(\"".as_slice(), b"::t(\"".as_slice()] { + for needle in [b"::tf(".as_slice(), b"::t(".as_slice()] { let mut i = 0; while let Some(rel) = find(b, i, needle) { - let start = rel + needle.len(); - let (lit, end) = read_string(b, start); - if let Some(lit) = lit { + let mut start = rel + needle.len(); + while start < b.len() && b[start].is_ascii_whitespace() { + start += 1; + } + if start < b.len() && b[start] == b'"' + && let Some(lit) = read_string(b, start).0 + { out.push(lit); } - i = end.max(i + 1); + i = rel + needle.len(); } } out