fix(i18n): 全局复查修复——多行调用点漏替换、动态 label 回归与死键清理
复查发现(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 测试全绿;英文模式实机截图复核(含回归点)通过。
This commit is contained in:
+33
-30
@@ -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} 失败"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+10
-10
@@ -72,7 +72,7 @@ impl Storage {
|
||||
|
||||
fn read_json_inner<T: for<'de> serde::Deserialize<'de>>(&self, p: &Path) -> Result<Option<T>> {
|
||||
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<Vec<ConnectionConfig>, 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<Vec<ConnectionConfig>, 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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -411,7 +411,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
alert
|
||||
.title(i18n::t("sidebar.delete"))
|
||||
.description(SharedString::from(i18n::tf(
|
||||
"确定删除「{name}」?将同时删除 {sub_count} 个订阅,并断开当前连接,此操作不可撤销。",
|
||||
"sidebar.delete_confirm",
|
||||
&[("name", &name), ("sub_count", &sub_count)],
|
||||
)))
|
||||
.button_props(
|
||||
|
||||
@@ -668,7 +668,7 @@ impl ConnectionForm {
|
||||
form.testing = false;
|
||||
match result {
|
||||
Ok(()) => window.push_notification(
|
||||
Notification::success("连接测试成功"),
|
||||
Notification::success(i18n::t("form.test_ok")),
|
||||
cx,
|
||||
),
|
||||
Err(e) => window.push_notification(Notification::error(format!(
|
||||
|
||||
@@ -644,7 +644,7 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
||||
.px_1p5()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(i18n::tf(
|
||||
"仅显示最新 {n} 条",
|
||||
"msg.latest_only",
|
||||
&[("n", &MAX_RENDERED_MESSAGES)],
|
||||
)),
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> 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<Self>) -> impl Into
|
||||
alert
|
||||
.title(i18n::t("pub.delete_title"))
|
||||
.description(i18n::tf(
|
||||
"确定删除预设「{name}」吗?此操作不可撤销。",
|
||||
"pub.delete_confirm",
|
||||
&[("name", &pname)],
|
||||
))
|
||||
.button_props(
|
||||
|
||||
@@ -457,7 +457,7 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context<Self>) -> 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()
|
||||
|
||||
@@ -119,7 +119,7 @@ pub(super) fn render_vars_panel(&self, cx: &mut Context<Self>) -> 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<Self>) -> impl IntoEleme
|
||||
.text_xs()
|
||||
.text_color(muted)
|
||||
.child(i18n::tf(
|
||||
"预览:{tp} | {pl}",
|
||||
"var.preview",
|
||||
&[("tp", &topic_rendered), ("pl", &payload_preview)],
|
||||
)),
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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())],
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -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<MqttXApp>, 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()
|
||||
|
||||
+13
-7
@@ -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<String> {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user