refactor(i18n): 迁移到 rust-i18n 语义键体系(ns.key),废弃中文原文作 key
旧实现的问题(按用户反馈):
- 中文原文作 key:文案措辞与代码耦合,一词多义撞 key("主题" Topic/Theme)
- 巨型 match 文案表:重复键靠编译警告暴露;漏翻静默回退,无任何校验
- tf 参数到处 &n.to_string() 噪音
新体系(gpui-kit 官方方案):
- locales/ui.yml 语言包(rust-i18n v2,en/zh-CN 双语并排),key 为
语义标识符(common.save / conn.connect / status.connected…),
与自然语言彻底解耦;改措辞不动代码
- crate 根 i18n! 编译期内嵌;init_i18n() 经 extend! 并入 gpui-kit
组件内置文案(按钮/空态等随语言切换)
- t/tf 薄门面查 _rust_i18n_translate(运行时 key),模型 label()
改返回语义 key;tf 参数只需 Display
- 四个强制单测:语言包 en/zh-CN 完整、占位符 {x} 两侧一致、
全源码 t()/tf() 字面量必须存在词条、模型 label 必须有真实译文
- 语言中性文案("QoS"、"/mqtt" 等)去掉 t() 包裹
- 顺带补翻 30 条漏网文案(资源监控统计行/表单校验/阿里云说明段等)
42 测试全绿;中英双语实机截图验证零裸 key。
This commit is contained in:
Generated
+1
@@ -4264,6 +4264,7 @@ dependencies = [
|
||||
"rfd",
|
||||
"rumqttc-v4-next",
|
||||
"rumqttc-v5-next",
|
||||
"rust-i18n",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"sentry",
|
||||
|
||||
@@ -26,6 +26,7 @@ rumqttc-v4 = { package = "rumqttc-v4-next", version = "0.34", features = ["webso
|
||||
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
sys-locale = "0.3"
|
||||
rust-i18n = "4.2"
|
||||
chrono = { version = "0.4", features = ["clock"] }
|
||||
dirs = "7"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,16 @@
|
||||
//! MQTTX:数据模型、持久化、MQTT 引擎与 GPUI 界面。
|
||||
|
||||
// 语言包编译期内嵌(key = 语义标识符,en/zh-CN 并排);miss 回退 en
|
||||
rust_i18n::i18n!("locales", fallback = "en");
|
||||
|
||||
/// 把 gpui-kit 组件内置文案(按钮/日历/空态等)并入当前语言包。
|
||||
/// 启动时调用一次;extend! 展开引用本 crate 的 _rust_i18n_backend,
|
||||
/// 必须在 lib 内调用(bin crate 的 crate:: 指向不同根)。
|
||||
pub fn init_i18n() {
|
||||
use gpui_kit::component as gpui_component;
|
||||
rust_i18n::extend!(gpui_component);
|
||||
}
|
||||
|
||||
pub mod aliyun;
|
||||
pub mod model;
|
||||
pub mod mqtt;
|
||||
|
||||
@@ -59,6 +59,8 @@ fn main() {
|
||||
// 其余 IconName(MailOpen/SlidersHorizontal 等)运行时会渲染空白
|
||||
.with_assets(gpui_kit::assets::AllAssets)
|
||||
.run(|cx: &mut App| {
|
||||
// gpui-kit 组件内置文案(按钮/日历/空态等)并入当前语言包
|
||||
mqttx_desktop::init_i18n();
|
||||
gpui_kit::init(cx);
|
||||
|
||||
let options = WindowOptions {
|
||||
|
||||
@@ -17,8 +17,8 @@ pub enum Direction {
|
||||
impl Direction {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Received => "接收",
|
||||
Self::Published => "发布",
|
||||
Self::Received => "dir.received",
|
||||
Self::Published => "dir.published",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ pub enum ConnectionStatus {
|
||||
impl ConnectionStatus {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Disconnected => "未连接",
|
||||
Self::Connecting => "连接中",
|
||||
Self::Connected => "已连接",
|
||||
Self::Error => "错误",
|
||||
Self::Disconnected => "status.disconnected",
|
||||
Self::Connecting => "status.connecting",
|
||||
Self::Connected => "status.connected",
|
||||
Self::Error => "status.error",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,9 +42,9 @@ impl ThemeModePref {
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::System => "跟随系统",
|
||||
Self::Light => "浅色",
|
||||
Self::Dark => "深色",
|
||||
Self::System => "theme.system",
|
||||
Self::Light => "theme.light",
|
||||
Self::Dark => "theme.dark",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ pub(super) fn close_tab(&mut self, id: &str, cx: &mut Context<Self>) {
|
||||
let testing = form_test.read(cx).is_testing();
|
||||
dialog
|
||||
.w(px(680.))
|
||||
.title(i18n::t("连接配置"))
|
||||
.title(i18n::t("form.title"))
|
||||
.child(form.clone())
|
||||
.footer(
|
||||
h_flex()
|
||||
@@ -242,7 +242,7 @@ pub(super) fn close_tab(&mut self, id: &str, cx: &mut Context<Self>) {
|
||||
} else {
|
||||
IconName::PlugZap
|
||||
})
|
||||
.label(if testing { i18n::t("测试中…") } else { i18n::t("测试连接") })
|
||||
.label(if testing { i18n::t("form.testing") } else { i18n::t("form.test") })
|
||||
.outline()
|
||||
.loading(testing)
|
||||
.on_click(move |_, window, cx| {
|
||||
@@ -252,13 +252,13 @@ pub(super) fn close_tab(&mut self, id: &str, cx: &mut Context<Self>) {
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
Button::new("form-cancel")
|
||||
.label(i18n::t("取消"))
|
||||
.label(i18n::t("common.cancel"))
|
||||
.outline()
|
||||
.on_click(|_, window, cx| window.close_dialog(cx)),
|
||||
)
|
||||
.child(
|
||||
Button::new("form-ok")
|
||||
.label(i18n::t("保存"))
|
||||
.label(i18n::t("common.save"))
|
||||
.primary()
|
||||
.on_click(move |_, window, cx| {
|
||||
let built = form_ok.read(cx).build(cx);
|
||||
@@ -288,7 +288,7 @@ pub(super) fn close_tab(&mut self, id: &str, cx: &mut Context<Self>) {
|
||||
window.close_dialog(cx);
|
||||
if stale {
|
||||
window.push_notification(
|
||||
Notification::info(i18n::t("配置已保存,重连后生效")),
|
||||
Notification::info(i18n::t("form.saved_reconnect")),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,8 +80,8 @@ impl MqttXApp {
|
||||
Notification::success(i18n::tf(
|
||||
"导出 {n} 条连接到 {path}",
|
||||
&[
|
||||
("n", &count.to_string()),
|
||||
("path", &path.display().to_string()),
|
||||
("n", &count),
|
||||
("path", &path.display()),
|
||||
],
|
||||
)),
|
||||
cx,
|
||||
@@ -136,8 +136,8 @@ impl MqttXApp {
|
||||
Notification::success(i18n::tf(
|
||||
"导入 {n} 条,跳过 {n2} 条",
|
||||
&[
|
||||
("n", &added.to_string()),
|
||||
("n2", &skipped.to_string()),
|
||||
("n", &added),
|
||||
("n2", &skipped),
|
||||
],
|
||||
)),
|
||||
cx,
|
||||
|
||||
@@ -27,18 +27,18 @@ pub(super) fn render_main(&mut self, _window: &mut Window, cx: &mut Context<Self
|
||||
cx.theme().muted,
|
||||
cx.theme().muted_foreground,
|
||||
))
|
||||
.child(div().text_base().font_medium().child(i18n::t("开始使用 MQTTX")))
|
||||
.child(div().text_base().font_medium().child(i18n::t("tab.welcome_title")))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.text_center()
|
||||
.child(i18n::t("从左侧选择一个连接,或新建连接开始调试")),
|
||||
.child(i18n::t("tab.welcome_hint")),
|
||||
)
|
||||
.child(
|
||||
Button::new("empty-new")
|
||||
.icon(IconName::Plus)
|
||||
.label(i18n::t("新建连接"))
|
||||
.label(i18n::t("titlebar.new_connection"))
|
||||
.outline()
|
||||
.small()
|
||||
.mt_1()
|
||||
|
||||
@@ -216,7 +216,7 @@ impl MqttXApp {
|
||||
let staged_consent = staged.clone();
|
||||
dialog
|
||||
.w(px(440.))
|
||||
.title(i18n::t("更新就绪"))
|
||||
.title(i18n::t("update.ready_title"))
|
||||
.child(
|
||||
v_flex().gap_2().child(
|
||||
div().text_sm().child(i18n::tf(
|
||||
@@ -232,7 +232,7 @@ impl MqttXApp {
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("upd-skip")
|
||||
.label(i18n::t("暂不更新"))
|
||||
.label(i18n::t("update.not_now"))
|
||||
.outline()
|
||||
.on_click(move |_, window, cx| {
|
||||
// 拒绝本次更新:删除暂存包,
|
||||
@@ -243,7 +243,7 @@ impl MqttXApp {
|
||||
)
|
||||
.child(
|
||||
Button::new("upd-later")
|
||||
.label(i18n::t("下次启动安装"))
|
||||
.label(i18n::t("update.install_later"))
|
||||
.outline()
|
||||
.on_click(move |_, window, cx| {
|
||||
// 同意标记:启动时据此自动安装;
|
||||
@@ -254,7 +254,7 @@ impl MqttXApp {
|
||||
)
|
||||
.child(
|
||||
Button::new("upd-now")
|
||||
.label(i18n::t("立即安装"))
|
||||
.label(i18n::t("update.install_now"))
|
||||
.primary()
|
||||
.on_click(move |_, window, cx| {
|
||||
window.close_dialog(cx);
|
||||
@@ -292,7 +292,7 @@ impl MqttXApp {
|
||||
.detach();
|
||||
}
|
||||
|
||||
let search = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("搜索连接…")));
|
||||
let search = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("sidebar.search")));
|
||||
|
||||
// 自验后门:MQTTX_OPEN_RESMON=1 启动时直接打开资源监控
|
||||
// (与 MQTTX_VERSION_OVERRIDE 同类的测试入口,供自动化验证 UI)。
|
||||
|
||||
@@ -34,8 +34,8 @@ impl MqttXApp {
|
||||
.icon(IconName::PanelLeftOpen)
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip(i18n::t("展开连接侧栏(Ctrl+B)"))
|
||||
.accessibility_label(i18n::t("展开连接侧栏(Ctrl+B)"))
|
||||
.tooltip(i18n::t("sidebar.expand"))
|
||||
.accessibility_label(i18n::t("sidebar.expand"))
|
||||
.on_click(cx.listener(|this, _, _, cx| this.toggle_sidebar(cx))),
|
||||
)
|
||||
.child(
|
||||
@@ -43,8 +43,8 @@ impl MqttXApp {
|
||||
.icon(IconName::Plus)
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip(i18n::t("新建连接"))
|
||||
.accessibility_label(i18n::t("新建连接"))
|
||||
.tooltip(i18n::t("titlebar.new_connection"))
|
||||
.accessibility_label(i18n::t("titlebar.new_connection"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.open_connection_form(None, window, cx);
|
||||
})),
|
||||
@@ -174,7 +174,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(i18n::t("还没有连接,点击上方 + 新建")),
|
||||
.child(i18n::t("sidebar.empty")),
|
||||
)
|
||||
} else if shown == 0 {
|
||||
v_flex()
|
||||
@@ -186,7 +186,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(i18n::t("没有匹配的连接")),
|
||||
.child(i18n::t("sidebar.no_match")),
|
||||
)
|
||||
} else {
|
||||
v_flex().flex_1().child(rows)
|
||||
@@ -211,7 +211,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(div().text_sm().font_semibold().child(i18n::t("连接")))
|
||||
.child(div().text_sm().font_semibold().child(i18n::t("sidebar.connections")))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
@@ -227,8 +227,8 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
.icon(IconName::PanelLeftClose)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(i18n::t("收起侧栏(Ctrl+B)"))
|
||||
.accessibility_label(i18n::t("收起侧栏(Ctrl+B)"))
|
||||
.tooltip(i18n::t("sidebar.collapse"))
|
||||
.accessibility_label(i18n::t("sidebar.collapse"))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.toggle_sidebar(cx)
|
||||
})),
|
||||
@@ -236,8 +236,8 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
.child(
|
||||
Button::new("sidebar-add")
|
||||
.icon(IconName::Plus)
|
||||
.tooltip(i18n::t("新建连接"))
|
||||
.accessibility_label(i18n::t("新建连接"))
|
||||
.tooltip(i18n::t("titlebar.new_connection"))
|
||||
.accessibility_label(i18n::t("titlebar.new_connection"))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -281,13 +281,13 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
.overflow_y_scrollbar();
|
||||
row = row.child(self.render_group_chip(
|
||||
"chip-all",
|
||||
format!("{} {}", i18n::t("全部"), self.connections.len()),
|
||||
format!("{} {}", i18n::t("common.all"), self.connections.len()),
|
||||
GroupFilter::All,
|
||||
cx,
|
||||
));
|
||||
row = row.child(self.render_group_chip(
|
||||
"chip-ungrouped",
|
||||
format!("{} {ungrouped}", i18n::t("未分组")),
|
||||
format!("{} {ungrouped}", i18n::t("sidebar.ungrouped")),
|
||||
GroupFilter::Ungrouped,
|
||||
cx,
|
||||
));
|
||||
@@ -353,8 +353,8 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
.icon(if connected { IconName::Square } else { IconName::PlugZap })
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(if connected { i18n::t("断开") } else { i18n::t("连接") })
|
||||
.accessibility_label(if connected { i18n::t("断开") } else { i18n::t("连接") })
|
||||
.tooltip(if connected { i18n::t("sidebar.disconnect") } else { i18n::t("sidebar.connections") })
|
||||
.accessibility_label(if connected { i18n::t("sidebar.disconnect") } else { i18n::t("sidebar.connections") })
|
||||
.on_click(cx.listener(move |this, _, _, _| {
|
||||
this.toggle_connection(&cfg);
|
||||
}))
|
||||
@@ -370,7 +370,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
.dropdown_menu(move |menu, _, _| {
|
||||
let (e1, e2, e3) = (eid.clone(), eid.clone(), eid.clone());
|
||||
let (w1, w2, w3) = (weak.clone(), weak.clone(), weak.clone());
|
||||
menu.item(PopupMenuItem::new(i18n::t("编辑")).on_click(move |_, window, cx| {
|
||||
menu.item(PopupMenuItem::new(i18n::t("common.edit")).on_click(move |_, window, cx| {
|
||||
w1.update(cx, |app, cx| {
|
||||
let cfg = app.connections.iter().find(|c| c.id == e1).cloned();
|
||||
if let Some(cfg) = cfg {
|
||||
@@ -379,7 +379,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
.item(PopupMenuItem::new(i18n::t("复制连接")).on_click(move |_, _, cx| {
|
||||
.item(PopupMenuItem::new(i18n::t("sidebar.duplicate")).on_click(move |_, _, cx| {
|
||||
w2.update(cx, |app, cx| {
|
||||
app.duplicate_connection(&e2);
|
||||
cx.notify();
|
||||
@@ -387,7 +387,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
.ok();
|
||||
}))
|
||||
.separator()
|
||||
.item(PopupMenuItem::new(i18n::t("删除")).on_click(move |_, window, cx| {
|
||||
.item(PopupMenuItem::new(i18n::t("common.delete")).on_click(move |_, window, cx| {
|
||||
let Some(app) = w3.upgrade() else {
|
||||
return;
|
||||
};
|
||||
@@ -409,14 +409,14 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
||||
let app = app.clone();
|
||||
let confirm_id = confirm_id.clone();
|
||||
alert
|
||||
.title(i18n::t("删除连接"))
|
||||
.title(i18n::t("sidebar.delete"))
|
||||
.description(SharedString::from(i18n::tf(
|
||||
"确定删除「{name}」?将同时删除 {sub_count} 个订阅,并断开当前连接,此操作不可撤销。",
|
||||
&[("name", &name), ("sub_count", &sub_count.to_string())],
|
||||
&[("name", &name), ("sub_count", &sub_count)],
|
||||
)))
|
||||
.button_props(
|
||||
DialogButtonProps::default()
|
||||
.ok_text(i18n::t("删除"))
|
||||
.ok_text(i18n::t("common.delete"))
|
||||
.ok_variant(ButtonVariant::Danger)
|
||||
.show_cancel(true),
|
||||
)
|
||||
|
||||
@@ -27,14 +27,14 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip(if self.pinned {
|
||||
i18n::t("取消窗口置顶")
|
||||
i18n::t("titlebar.unpin")
|
||||
} else {
|
||||
i18n::t("置顶窗口")
|
||||
i18n::t("titlebar.pin")
|
||||
})
|
||||
.accessibility_label(if self.pinned {
|
||||
i18n::t("取消窗口置顶")
|
||||
i18n::t("titlebar.unpin")
|
||||
} else {
|
||||
i18n::t("置顶窗口")
|
||||
i18n::t("titlebar.pin")
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let next = !this.pinned;
|
||||
@@ -43,7 +43,7 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
||||
cx.notify();
|
||||
} else {
|
||||
window.push_notification(
|
||||
Notification::error(i18n::t("置顶操作失败")),
|
||||
Notification::error(i18n::t("titlebar.pin_failed")),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -74,7 +74,7 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(i18n::t("MQTT 调试客户端")),
|
||||
.child(i18n::t("titlebar.subtitle")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -90,7 +90,7 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
||||
.child(
|
||||
Button::new("new-connection")
|
||||
.icon(IconName::Plus)
|
||||
.label(i18n::t("新建连接"))
|
||||
.label(i18n::t("titlebar.new_connection"))
|
||||
.primary()
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -112,21 +112,21 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
||||
let w5 = app_weak.clone();
|
||||
let w6 = app_weak.clone();
|
||||
menu.item(
|
||||
PopupMenuItem::new(i18n::t("阿里云设备"))
|
||||
PopupMenuItem::new(i18n::t("titlebar.aliyun"))
|
||||
.on_click(move |_, window, cx| {
|
||||
if let Some(entity) = w1.upgrade() {
|
||||
crate::ui::dialogs::aliyun_dialog::open(entity, window, cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item(PopupMenuItem::new(i18n::t("全局变量")).on_click(
|
||||
.item(PopupMenuItem::new(i18n::t("titlebar.variables")).on_click(
|
||||
move |_, window, cx| {
|
||||
if let Some(entity) = w2.upgrade() {
|
||||
crate::ui::dialogs::variables_dialog::open(entity, window, cx);
|
||||
}
|
||||
},
|
||||
))
|
||||
.item(PopupMenuItem::new(i18n::t("资源监控")).on_click(
|
||||
.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);
|
||||
@@ -134,7 +134,7 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
||||
},
|
||||
))
|
||||
.separator()
|
||||
.item(PopupMenuItem::new(i18n::t("导出连接")).on_click(
|
||||
.item(PopupMenuItem::new(i18n::t("titlebar.export")).on_click(
|
||||
move |_, window, cx| {
|
||||
if let Some(entity) = w4.upgrade() {
|
||||
entity.update(cx, |app, cx| {
|
||||
@@ -143,7 +143,7 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
||||
}
|
||||
},
|
||||
))
|
||||
.item(PopupMenuItem::new(i18n::t("导入连接")).on_click(
|
||||
.item(PopupMenuItem::new(i18n::t("titlebar.import")).on_click(
|
||||
move |_, window, cx| {
|
||||
if let Some(entity) = w5.upgrade() {
|
||||
entity.update(cx, |app, cx| {
|
||||
@@ -153,7 +153,7 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
||||
},
|
||||
))
|
||||
.separator()
|
||||
.item(PopupMenuItem::new(i18n::t("设置")).on_click(
|
||||
.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);
|
||||
@@ -173,14 +173,14 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip(if dark {
|
||||
i18n::t("切换为浅色")
|
||||
i18n::t("titlebar.switch_light")
|
||||
} else {
|
||||
i18n::t("切换为深色")
|
||||
i18n::t("titlebar.switch_dark")
|
||||
})
|
||||
.accessibility_label(if dark {
|
||||
i18n::t("切换为浅色")
|
||||
i18n::t("titlebar.switch_light")
|
||||
} else {
|
||||
i18n::t("切换为深色")
|
||||
i18n::t("titlebar.switch_dark")
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let next = if cx.theme().mode == ThemeMode::Dark {
|
||||
|
||||
@@ -26,11 +26,11 @@ use crate::model::{ConnectionConfig, LastWill, ProtocolVersion, SslConfig, Trans
|
||||
use crate::mqtt::MqttEngine;
|
||||
use crate::ui::widgets::{field, field_ex, make_select, OptionDelegate};
|
||||
|
||||
fn protocol_labels() -> [&'static str; 2] { [i18n::t("MQTT 5.0"), "MQTT 3.1.1"] }
|
||||
fn protocol_labels() -> [&'static str; 2] { ["MQTT 5.0", "MQTT 3.1.1"] }
|
||||
const TRANSPORTS: [&str; 4] = ["TCP", "TLS", "WebSocket", "WSS"]; // 语言无关
|
||||
const QOS: [&str; 3] = ["QoS 0", "QoS 1", "QoS 2"]; // 语言无关
|
||||
/// 分组下拉的首项:选择它表示不归属任何分组。
|
||||
fn no_group_label() -> &'static str { i18n::t("无分组") }
|
||||
fn no_group_label() -> &'static str { i18n::t("form.no_group") }
|
||||
|
||||
pub struct ConnectionForm {
|
||||
engine: Arc<MqttEngine>,
|
||||
@@ -143,28 +143,28 @@ impl ConnectionForm {
|
||||
chrono::Local::now().timestamp()
|
||||
});
|
||||
|
||||
let name = input(&c.name, i18n::t("连接名称"), window, cx);
|
||||
let host = input(&c.host, i18n::t("broker.example.com"), window, cx);
|
||||
let port = input(&c.port.to_string(), i18n::t(i18n::t("端口")), window, cx);
|
||||
let path = input(&c.path, i18n::t("/mqtt"), window, cx);
|
||||
let client_id = input(&c.client_id, i18n::t("Client ID"), window, cx);
|
||||
let username = input(&c.username, i18n::t("用户名(可选)"), window, cx);
|
||||
let name = input(&c.name, i18n::t("form.name_ph"), window, cx);
|
||||
let host = input(&c.host, "broker.example.com", window, cx);
|
||||
let port = input(&c.port.to_string(), i18n::t(i18n::t("form.port")), window, cx);
|
||||
let path = input(&c.path, "/mqtt", window, cx);
|
||||
let client_id = input(&c.client_id, i18n::t("form.client_id"), window, cx);
|
||||
let username = input(&c.username, i18n::t("form.username_ph"), window, cx);
|
||||
let password = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(i18n::t("密码(可选)"))
|
||||
.placeholder(i18n::t("form.password_ph"))
|
||||
.default_value(&c.password)
|
||||
.masked(true)
|
||||
});
|
||||
let keep_alive = input(&c.keep_alive.to_string(), i18n::t("60"), window, cx);
|
||||
let keep_alive = input(&c.keep_alive.to_string(), "60", window, cx);
|
||||
let conn_timeout = input(
|
||||
&c.connection_timeout_secs.to_string(),
|
||||
i18n::t("秒(默认 10)"),
|
||||
i18n::t("form.timeout_ph"),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let max_reconnect = input(
|
||||
&c.max_reconnect_times.to_string(),
|
||||
i18n::t("0=无限重连"),
|
||||
i18n::t("form.max_reconnects_ph"),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -174,39 +174,39 @@ impl ConnectionForm {
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
i18n::t("秒(0=会话随连接结束)"),
|
||||
i18n::t("form.session_expiry_ph"),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let receive_max = optional_input(c.receive_maximum.map(u32::from), i18n::t("服务端接收上限"), window, cx);
|
||||
let max_packet = optional_input(c.maximum_packet_size, i18n::t("最大报文长度"), window, cx);
|
||||
let topic_alias = optional_input(c.topic_alias_maximum.map(u32::from), i18n::t("主题别名上限"), window, cx);
|
||||
let receive_max = optional_input(c.receive_maximum.map(u32::from), i18n::t("form.receive_max_ph"), window, cx);
|
||||
let max_packet = optional_input(c.maximum_packet_size, i18n::t("form.max_packet"), window, cx);
|
||||
let topic_alias = optional_input(c.topic_alias_maximum.map(u32::from), i18n::t("form.topic_alias_max"), window, cx);
|
||||
|
||||
let ssl_ca = input(&c.ssl.ca_file, i18n::t("CA 证书 PEM 路径(可选)"), window, cx);
|
||||
let ssl_client_cert = input(&c.ssl.client_cert_file, i18n::t("客户端证书 PEM 路径(可选)"), window, cx);
|
||||
let ssl_client_key = input(&c.ssl.client_key_file, i18n::t("客户端私钥 PEM 路径(可选)"), window, cx);
|
||||
let ssl_ca = input(&c.ssl.ca_file, i18n::t("ssl.ca_ph"), window, cx);
|
||||
let ssl_client_cert = input(&c.ssl.client_cert_file, i18n::t("ssl.cert_ph"), window, cx);
|
||||
let ssl_client_key = input(&c.ssl.client_key_file, i18n::t("ssl.key_ph"), window, cx);
|
||||
|
||||
let group_new = input("", i18n::t("填写后优先于左侧所选分组"), window, cx);
|
||||
let group_new = input("", i18n::t("form.new_group_hint"), window, cx);
|
||||
|
||||
let (will_topic, will_payload, will_enabled, will_retain, will_qos_idx, will_content_type, will_response_topic) =
|
||||
match &c.last_will {
|
||||
Some(w) => (
|
||||
input(&w.topic, i18n::t("遗嘱主题"), window, cx),
|
||||
input(&w.payload, i18n::t("遗嘱消息"), window, cx),
|
||||
input(&w.topic, i18n::t("form.will_topic"), window, cx),
|
||||
input(&w.payload, i18n::t("form.will_payload_ph"), window, cx),
|
||||
true,
|
||||
w.retain,
|
||||
w.qos as usize,
|
||||
input(w.content_type.as_deref().unwrap_or(""), i18n::t("内容类型(可选)"), window, cx),
|
||||
input(w.response_topic.as_deref().unwrap_or(""), i18n::t("响应主题(可选)"), window, cx),
|
||||
input(w.content_type.as_deref().unwrap_or(""), i18n::t("form.content_type_ph"), window, cx),
|
||||
input(w.response_topic.as_deref().unwrap_or(""), i18n::t("form.response_topic_ph"), window, cx),
|
||||
),
|
||||
None => (
|
||||
input("", i18n::t("遗嘱主题"), window, cx),
|
||||
input("", i18n::t("遗嘱消息"), window, cx),
|
||||
input("", i18n::t("form.will_topic"), window, cx),
|
||||
input("", i18n::t("form.will_payload_ph"), window, cx),
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
input("", i18n::t("内容类型(可选)"), window, cx),
|
||||
input("", i18n::t("响应主题(可选)"), window, cx),
|
||||
input("", i18n::t("form.content_type_ph"), window, cx),
|
||||
input("", i18n::t("form.response_topic_ph"), window, cx),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -402,15 +402,15 @@ impl ConnectionForm {
|
||||
fn parse_u16(s: &str, label: &'static str) -> Result<u16, String> {
|
||||
s.trim()
|
||||
.parse::<u16>()
|
||||
.map_err(|_| format!("{} {}", i18n::t(label), i18n::t("需要是 0~65535 的整数")))
|
||||
.map_err(|_| format!("{} {}", i18n::t(label), i18n::t("form.err_range")))
|
||||
}
|
||||
|
||||
/// 端口:1~65535。0 不是可连接端口,放行只会在 CONNECT 阶段报出难以理解的错误。
|
||||
fn parse_port(s: &str) -> Result<u16, String> {
|
||||
match s.trim().parse::<u16>() {
|
||||
Ok(0) => Err(i18n::t("端口需要是 1~65535 的整数").to_string()),
|
||||
Ok(0) => Err(i18n::t("form.port_invalid").to_string()),
|
||||
Ok(p) => Ok(p),
|
||||
Err(_) => Err(i18n::t("端口需要是 1~65535 的整数").to_string()),
|
||||
Err(_) => Err(i18n::t("form.port_invalid").to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,7 +419,7 @@ impl ConnectionForm {
|
||||
/// 且该值会被持久化,表现为"每次连接都失败"。
|
||||
fn parse_opt_positive_u32(s: &str, label: &'static str) -> Result<Option<u32>, String> {
|
||||
match Self::parse_opt_u32(s, label)? {
|
||||
Some(0) => Err(format!("{} {}", i18n::t(label), i18n::t("必须大于 0(0 是 MQTT 5 协议错误)"))),
|
||||
Some(0) => Err(format!("{} {}", i18n::t(label), i18n::t("form.err_positive"))),
|
||||
other => Ok(other),
|
||||
}
|
||||
}
|
||||
@@ -431,7 +431,7 @@ impl ConnectionForm {
|
||||
}
|
||||
s.parse::<u32>()
|
||||
.map(Some)
|
||||
.map_err(|_| format!("{} {}", i18n::t(label), i18n::t("需要是非负整数")))
|
||||
.map_err(|_| format!("{} {}", i18n::t(label), i18n::t("form.err_uint")))
|
||||
}
|
||||
|
||||
pub fn build(&self, cx: &App) -> Result<ConnectionConfig, String> {
|
||||
@@ -451,28 +451,28 @@ impl ConnectionForm {
|
||||
let mut name = self.val(&self.name, cx);
|
||||
if name.trim().is_empty() {
|
||||
if !for_test {
|
||||
return Err(self.fail("name", i18n::t("连接名称不能为空")));
|
||||
return Err(self.fail("name", i18n::t("form.name_required")));
|
||||
}
|
||||
name = i18n::t("测试连接").into();
|
||||
name = i18n::t("form.test").into();
|
||||
}
|
||||
// 判空已按 trim,保存也存 trim 后的值,避免首尾空白入库
|
||||
let name = name.trim().to_string();
|
||||
let host = self.val(&self.host, cx);
|
||||
if host.trim().is_empty() {
|
||||
return Err(self.fail("host", i18n::t("主机地址不能为空")));
|
||||
return Err(self.fail("host", i18n::t("form.host_required")));
|
||||
}
|
||||
let host = host.trim().to_string();
|
||||
let port = self.fail_as("port", Self::parse_port(&self.val(&self.port, cx)))?;
|
||||
let mut client_id = self.val(&self.client_id, cx);
|
||||
if client_id.trim().is_empty() {
|
||||
if !for_test {
|
||||
return Err(self.fail("client_id", i18n::t("Client ID 不能为空")));
|
||||
return Err(self.fail("client_id", i18n::t("form.client_id_required")));
|
||||
}
|
||||
client_id = format!("mqttx-test-{}", &uuid::Uuid::new_v4().simple().to_string()[..8]);
|
||||
}
|
||||
let client_id = client_id.trim().to_string();
|
||||
let keep_alive =
|
||||
self.fail_as("keep_alive", Self::parse_u16(&self.val(&self.keep_alive, cx), i18n::t("Keep Alive")))?;
|
||||
self.fail_as("keep_alive", Self::parse_u16(&self.val(&self.keep_alive, cx), i18n::t("form.keep_alive_short")))?;
|
||||
// 空串视为 0(会话随连接结束);非空必须是合法非负整数,不再静默吞掉 -1/abc
|
||||
let session_expiry = {
|
||||
let raw = self.val(&self.session_expiry, cx);
|
||||
@@ -483,23 +483,23 @@ impl ConnectionForm {
|
||||
t.parse::<u32>().map_err(|_| {
|
||||
self.fail(
|
||||
"session_expiry",
|
||||
i18n::t("会话过期间隔需要是非负整数(0=会话随连接结束)"),
|
||||
i18n::t("form.session_expiry_invalid"),
|
||||
)
|
||||
})?
|
||||
}
|
||||
};
|
||||
let receive_maximum = self.fail_as("receive_max", (|| -> Result<_, String> {
|
||||
Ok(Self::parse_opt_positive_u32(&self.val(&self.receive_max, cx), i18n::t("接收上限"))?
|
||||
.map(|v| u16::try_from(v).map_err(|_| i18n::t("接收上限不能超过 65535").to_string()))
|
||||
Ok(Self::parse_opt_positive_u32(&self.val(&self.receive_max, cx), i18n::t("form.receive_max_short"))?
|
||||
.map(|v| u16::try_from(v).map_err(|_| i18n::t("form.receive_max_invalid").to_string()))
|
||||
.transpose())
|
||||
})())??;
|
||||
let maximum_packet_size = self.fail_as(
|
||||
"max_packet",
|
||||
Self::parse_opt_positive_u32(&self.val(&self.max_packet, cx), i18n::t("最大报文长度")),
|
||||
Self::parse_opt_positive_u32(&self.val(&self.max_packet, cx), i18n::t("form.max_packet")),
|
||||
)?;
|
||||
let topic_alias_maximum = self.fail_as("topic_alias", (|| -> Result<_, String> {
|
||||
Ok(Self::parse_opt_positive_u32(&self.val(&self.topic_alias, cx), i18n::t("主题别名"))?
|
||||
.map(|v| u16::try_from(v).map_err(|_| i18n::t("主题别名上限不能超过 65535").to_string()))
|
||||
Ok(Self::parse_opt_positive_u32(&self.val(&self.topic_alias, cx), i18n::t("form.topic_alias"))?
|
||||
.map(|v| u16::try_from(v).map_err(|_| i18n::t("form.topic_alias_invalid").to_string()))
|
||||
.transpose())
|
||||
})())??;
|
||||
|
||||
@@ -512,7 +512,7 @@ impl ConnectionForm {
|
||||
.parse::<u16>()
|
||||
.ok()
|
||||
.filter(|v| *v >= 1)
|
||||
.ok_or_else(|| self.fail("conn_timeout", i18n::t("连接超时需要是 1~65535 的整数(秒)")))?
|
||||
.ok_or_else(|| self.fail("conn_timeout", i18n::t("form.conn_timeout_invalid")))?
|
||||
};
|
||||
let max_reconnect_raw = self.val(&self.max_reconnect, cx);
|
||||
let max_reconnect_times = if max_reconnect_raw.trim().is_empty() {
|
||||
@@ -521,7 +521,7 @@ impl ConnectionForm {
|
||||
max_reconnect_raw
|
||||
.trim()
|
||||
.parse::<u32>()
|
||||
.map_err(|_| self.fail("max_reconnect", i18n::t("最大重连次数需要是非负整数(0=无限)")))?
|
||||
.map_err(|_| self.fail("max_reconnect", i18n::t("form.max_reconnects_invalid")))?
|
||||
};
|
||||
|
||||
// 分组:新分组名非空时优先,否则取下拉所选(首项=无分组)
|
||||
@@ -559,7 +559,7 @@ impl ConnectionForm {
|
||||
// WS/WSS 依赖 path 建立升级请求,缺失时连接必然失败,保存前拦截
|
||||
let path = self.val(&self.path, cx).trim().to_string();
|
||||
if transport.is_websocket() && path.is_empty() {
|
||||
return Err(self.fail("path", i18n::t("WS/WSS 传输下 WS 路径不能为空")));
|
||||
return Err(self.fail("path", i18n::t("form.ws_path_required")));
|
||||
}
|
||||
|
||||
let ssl = SslConfig {
|
||||
@@ -572,7 +572,7 @@ impl ConnectionForm {
|
||||
if transport.is_tls()
|
||||
&& ssl.client_cert_file.is_empty() != ssl.client_key_file.is_empty()
|
||||
{
|
||||
return Err(self.fail("ssl_client_cert", i18n::t("客户端证书与客户端密钥必须同时填写")));
|
||||
return Err(self.fail("ssl_client_cert", i18n::t("form.cert_pair_required")));
|
||||
}
|
||||
// 证书文件不存在时握手才报错太晚,保存前给出字段级提示
|
||||
if transport.is_tls() {
|
||||
@@ -582,7 +582,7 @@ impl ConnectionForm {
|
||||
("ssl_client_key", "客户端私钥", &ssl.client_key_file),
|
||||
] {
|
||||
if !p.is_empty() && !std::path::Path::new(p).exists() {
|
||||
return Err(self.fail(key, i18n::tf("{label}路径不存在:{p}", &[("label", &i18n::t(label)), ("p", &p)])));
|
||||
return Err(self.fail(key, i18n::tf("form.path_missing", &[("label", &i18n::t(label)), ("p", &p)])));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -591,13 +591,13 @@ impl ConnectionForm {
|
||||
// 与其它字段一致:trim 后入库
|
||||
let topic = self.val(&self.will_topic, cx).trim().to_string();
|
||||
if topic.is_empty() {
|
||||
return Err(self.fail("will_topic", i18n::t("遗嘱主题不能为空")));
|
||||
return Err(self.fail("will_topic", i18n::t("will.topic_required")));
|
||||
}
|
||||
// 通配符只用于订阅过滤器;出现在 PUBLISH 主题上会被 broker 直接拒绝
|
||||
if topic.contains('+') || topic.contains('#') {
|
||||
return Err(self.fail(
|
||||
"will_topic",
|
||||
i18n::t("遗嘱主题不能包含通配符 + 或 #"),
|
||||
i18n::t("will.topic_wildcard"),
|
||||
));
|
||||
}
|
||||
let opt_str = |v: String| {
|
||||
@@ -703,7 +703,7 @@ impl ConnectionForm {
|
||||
)
|
||||
.child(
|
||||
Button::new(id)
|
||||
.label(i18n::t("浏览…"))
|
||||
.label(i18n::t("common.browse"))
|
||||
.outline()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
@@ -723,7 +723,7 @@ impl ConnectionForm {
|
||||
let (tx, rx) = smol::channel::bounded::<Option<std::path::PathBuf>>(1);
|
||||
std::thread::spawn(move || {
|
||||
let picked = rfd::FileDialog::new()
|
||||
.set_title(i18n::t("选择证书 / 密钥文件"))
|
||||
.set_title(i18n::t("ssl.pick_file"))
|
||||
.add_filter("证书 / 密钥 (PEM)", &["pem", "crt", "cer", "key", "der"])
|
||||
.pick_file();
|
||||
let _ = tx.send_blocking(picked);
|
||||
@@ -878,40 +878,40 @@ impl Render for ConnectionForm {
|
||||
.iter()
|
||||
.filter(|on| **on)
|
||||
.count();
|
||||
let ssl_hint = (ssl_count > 0).then(|| SharedString::from(i18n::tf("已配置 {n} 项", &[("n", &ssl_count.to_string())])));
|
||||
let ssl_hint = (ssl_count > 0).then(|| SharedString::from(i18n::tf("ssl.n_configured", &[("n", &ssl_count)])));
|
||||
let mqtt5_hint =
|
||||
(mqtt5_count > 0).then(|| SharedString::from(i18n::tf("已配置 {n} 项", &[("n", &mqtt5_count.to_string())])));
|
||||
(mqtt5_count > 0).then(|| SharedString::from(i18n::tf("ssl.n_configured", &[("n", &mqtt5_count)])));
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.max_h(px(560.))
|
||||
.overflow_y_scrollbar()
|
||||
.pr_1()
|
||||
// ── 基本信息(始终展开) ──
|
||||
.child(section(i18n::t("基本信息"), border, card_bg,
|
||||
.child(section(i18n::t("form.basics"), border, card_bg,
|
||||
v_flex().gap_2().child(
|
||||
// 主机与端口是一对,放同一行;名称与协议/传输同行
|
||||
h_flex().gap_2().w_full()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("主机"), Input::new(&self.host), true, err("host"), danger)))
|
||||
.child(div().w(px(120.)).child(field_ex(i18n::t(i18n::t("端口")), Input::new(&self.port), true, err("port"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("form.host"), Input::new(&self.host), true, err("host"), danger)))
|
||||
.child(div().w(px(120.)).child(field_ex(i18n::t(i18n::t("form.port")), Input::new(&self.port), true, err("port"), danger)))
|
||||
)
|
||||
.child(h_flex().gap_2().w_full()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("名称"), Input::new(&self.name), true, err("name"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("协议"), Select::new(&self.protocol))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("传输"), Select::new(&self.transport))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("form.name"), Input::new(&self.name), true, err("name"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("form.protocol"), Select::new(&self.protocol))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("form.transport"), Select::new(&self.transport))))
|
||||
)
|
||||
.child(h_flex().gap_2().w_full()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("分组"), Select::new(&self.group_select))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("新建分组"), Input::new(&self.group_new))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("form.group"), Select::new(&self.group_select))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("form.new_group"), Input::new(&self.group_new))))
|
||||
)
|
||||
.child(h_flex().gap_2().w_full().items_end()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("Client ID"), Input::new(&self.client_id), true, err("client_id"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("form.client_id"), Input::new(&self.client_id), true, err("client_id"), danger)))
|
||||
// 一键换一个随机 Client ID,与 ConnectionConfig::new 的生成逻辑一致
|
||||
.child(
|
||||
Button::new("cid-regen")
|
||||
.icon(IconName::RefreshCw)
|
||||
.ghost()
|
||||
.tooltip(i18n::t("重新生成 Client ID"))
|
||||
.accessibility_label(i18n::t("重新生成 Client ID"))
|
||||
.tooltip(i18n::t("form.regen_client_id"))
|
||||
.accessibility_label(i18n::t("form.regen_client_id"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let cid = crate::model::generate_client_id();
|
||||
this.client_id
|
||||
@@ -921,48 +921,48 @@ impl Render for ConnectionForm {
|
||||
)
|
||||
// WS 路径仅对 WebSocket/WSS 有意义,按需显示;该传输下必填
|
||||
.when(is_ws, |w| w.child(
|
||||
div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("WS 路径"), Input::new(&self.path), true, err("path"), danger))
|
||||
div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("form.ws_path"), Input::new(&self.path), true, err("path"), danger))
|
||||
))
|
||||
)
|
||||
))
|
||||
// ── 认证与心跳 ──
|
||||
.child(self.collapsible_section(
|
||||
"sec-auth", i18n::t("认证与心跳"), None, false, self.show_auth,
|
||||
"sec-auth", i18n::t("form.auth_section"), None, false, self.show_auth,
|
||||
|f| f.show_auth = !f.show_auth, border, card_bg,
|
||||
v_flex().gap_2()
|
||||
.child(h_flex().gap_2().w_full()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("用户名"), Input::new(&self.username))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("密码"), Input::new(&self.password).mask_toggle())))
|
||||
.child(div().w(px(110.)).child(field_ex(i18n::t("Keep Alive (秒)"), Input::new(&self.keep_alive), true, err("keep_alive"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("form.username"), Input::new(&self.username))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("form.password"), Input::new(&self.password).mask_toggle())))
|
||||
.child(div().w(px(110.)).child(field_ex(i18n::t("form.keep_alive"), Input::new(&self.keep_alive), true, err("keep_alive"), danger)))
|
||||
)
|
||||
.child(h_flex().gap_2().w_full()
|
||||
.child(div().w(px(150.)).child(field_ex(i18n::t("连接超时 (秒)"), Input::new(&self.conn_timeout), false, err("conn_timeout"), danger)))
|
||||
.child(div().w(px(170.)).child(field_ex(i18n::t("最大重连次数 (0=无限)"), Input::new(&self.max_reconnect), false, err("max_reconnect"), danger)))
|
||||
.child(div().w(px(150.)).child(field_ex(i18n::t("form.conn_timeout"), Input::new(&self.conn_timeout), false, err("conn_timeout"), danger)))
|
||||
.child(div().w(px(170.)).child(field_ex(i18n::t("form.max_reconnects"), Input::new(&self.max_reconnect), false, err("max_reconnect"), danger)))
|
||||
)
|
||||
.child(self.switch_row("clean_start", i18n::t("Clean Start / Clean Session"), self.clean_start, cx))
|
||||
.child(self.switch_row("auto_reconnect", i18n::t("断线自动重连"), self.auto_reconnect, cx))
|
||||
.child(self.switch_row("auto_resubscribe", i18n::t("连接后自动恢复订阅"), self.auto_resubscribe, cx))
|
||||
.child(self.switch_row("auto_connect", i18n::t("启动时自动连接"), self.auto_connect, cx)),
|
||||
.child(self.switch_row("clean_start", i18n::t("form.clean_start"), self.clean_start, cx))
|
||||
.child(self.switch_row("auto_reconnect", i18n::t("form.auto_reconnect"), self.auto_reconnect, cx))
|
||||
.child(self.switch_row("auto_resubscribe", i18n::t("form.auto_resub"), self.auto_resubscribe, cx))
|
||||
.child(self.switch_row("auto_connect", i18n::t("form.auto_connect"), self.auto_connect, cx)),
|
||||
cx,
|
||||
))
|
||||
// ── SSL/TLS(仅 TLS/WSS 显示) ──
|
||||
.when(is_tls, |w| w.child(self.collapsible_section(
|
||||
"sec-ssl", i18n::t("SSL/TLS"), ssl_hint, true, self.show_ssl,
|
||||
"sec-ssl", i18n::t("ssl.section"), ssl_hint, true, self.show_ssl,
|
||||
|f| f.show_ssl = !f.show_ssl, border, card_bg,
|
||||
v_flex().gap_2()
|
||||
.child(self.cert_path_row(
|
||||
"ssl-ca-browse", i18n::t("CA 证书 (PEM)"), self.ssl_ca.clone(),
|
||||
"ssl-ca-browse", i18n::t("ssl.ca"), self.ssl_ca.clone(),
|
||||
err("ssl_ca"), danger, cx,
|
||||
))
|
||||
.child(self.cert_path_row(
|
||||
"ssl-cert-browse", i18n::t("客户端证书 (PEM)"), self.ssl_client_cert.clone(),
|
||||
"ssl-cert-browse", i18n::t("ssl.cert"), self.ssl_client_cert.clone(),
|
||||
err("ssl_client_cert"), danger, cx,
|
||||
))
|
||||
.child(self.cert_path_row(
|
||||
"ssl-key-browse", i18n::t("客户端密钥 (PEM)"), self.ssl_client_key.clone(),
|
||||
"ssl-key-browse", i18n::t("ssl.key"), self.ssl_client_key.clone(),
|
||||
err("ssl_client_key"), danger, cx,
|
||||
))
|
||||
.child(self.switch_row("ssl_ignore_ca", i18n::t("忽略 CA 校验(不验证服务器证书)"), self.ssl_ignore_ca, cx)),
|
||||
.child(self.switch_row("ssl_ignore_ca", i18n::t("ssl.skip_verify"), self.ssl_ignore_ca, cx)),
|
||||
cx,
|
||||
)))
|
||||
// ── MQTT 5 高级属性(仅 v5 协议显示) ──
|
||||
@@ -970,29 +970,29 @@ impl Render for ConnectionForm {
|
||||
"sec-mqtt5", "MQTT 5 属性", mqtt5_hint, true, self.show_mqtt5,
|
||||
|f| f.show_mqtt5 = !f.show_mqtt5, border, card_bg,
|
||||
h_flex().gap_2().w_full()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("会话过期间隔(秒)"), Input::new(&self.session_expiry), false, err("session_expiry"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("接收最大值"), Input::new(&self.receive_max), false, err("receive_max"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("最大报文长度"), Input::new(&self.max_packet), false, err("max_packet"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("主题别名上限"), Input::new(&self.topic_alias), false, err("topic_alias"), danger))),
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("form.session_expiry"), Input::new(&self.session_expiry), false, err("session_expiry"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("form.receive_max"), Input::new(&self.receive_max), false, err("receive_max"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("form.max_packet"), Input::new(&self.max_packet), false, err("max_packet"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("form.topic_alias_max"), Input::new(&self.topic_alias), false, err("topic_alias"), danger))),
|
||||
cx,
|
||||
)))
|
||||
// ── 遗嘱 ──
|
||||
.child(self.collapsible_section(
|
||||
"sec-will", i18n::t("遗嘱消息 (Last Will)"),
|
||||
self.will_enabled.then(|| i18n::t("已启用").into()), true, self.show_will,
|
||||
"sec-will", i18n::t("form.will_section"),
|
||||
self.will_enabled.then(|| i18n::t("will.enabled_hint").into()), true, self.show_will,
|
||||
|f| f.show_will = !f.show_will, border, card_bg,
|
||||
v_flex().gap_2()
|
||||
.child(self.switch_row("will_enabled", i18n::t("启用遗嘱消息"), self.will_enabled, cx))
|
||||
.child(self.switch_row("will_enabled", i18n::t("form.will_enable"), self.will_enabled, cx))
|
||||
.when(self.will_enabled, |w| w
|
||||
.child(h_flex().gap_2()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("遗嘱主题"), Input::new(&self.will_topic), true, err("will_topic"), danger)))
|
||||
.child(div().w(px(130.)).child(field(i18n::t("QoS"), Select::new(&self.will_qos))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("form.will_topic"), Input::new(&self.will_topic), true, err("will_topic"), danger)))
|
||||
.child(div().w(px(130.)).child(field("QoS", Select::new(&self.will_qos))))
|
||||
)
|
||||
.child(field(i18n::t("遗嘱内容"), Input::new(&self.will_payload)))
|
||||
.child(field(i18n::t("form.will_payload"), Input::new(&self.will_payload)))
|
||||
// v5 遗嘱属性;3.1.1 连接会忽略这两项
|
||||
.child(h_flex().gap_2().w_full()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("内容类型 (Content Type)"), Input::new(&self.will_content_type))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("响应主题 (Response Topic)"), Input::new(&self.will_response_topic))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("form.will_content_type"), Input::new(&self.will_content_type))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("form.will_response_topic"), Input::new(&self.will_response_topic))))
|
||||
)
|
||||
.child(self.switch_row("will_retain", "Retain", self.will_retain, cx))
|
||||
),
|
||||
|
||||
@@ -92,11 +92,11 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
||||
IconName::PlugZap
|
||||
})
|
||||
.label(if connected {
|
||||
i18n::t("断开")
|
||||
i18n::t("sidebar.disconnect")
|
||||
} else if connecting {
|
||||
i18n::t("连接中")
|
||||
i18n::t("status.connecting")
|
||||
} else {
|
||||
i18n::t("连接")
|
||||
i18n::t("sidebar.connections")
|
||||
})
|
||||
.when(connected, |b| b.danger().ghost())
|
||||
.when(!connected, |b| b.primary().ghost())
|
||||
@@ -151,7 +151,7 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
||||
let border = cx.theme().border;
|
||||
let muted = cx.theme().muted_foreground;
|
||||
let mono = cx.theme().mono_font_family.clone();
|
||||
let direction_label = if received { i18n::t("接收") } else { i18n::t("发布") };
|
||||
let direction_label = if received { i18n::t("dir.received") } else { i18n::t("dir.published") };
|
||||
|
||||
let header = h_flex()
|
||||
.gap_2()
|
||||
@@ -223,7 +223,7 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().warning.alpha(0.15))
|
||||
.text_color(cx.theme().warning)
|
||||
.child(format!("已截断 {}KB", MAX_PAYLOAD_RETAIN / 1024)),
|
||||
.child(i18n::tf("msg.truncated", &[("n", &(MAX_PAYLOAD_RETAIN / 1024))])),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
@@ -291,43 +291,43 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
||||
.child(
|
||||
Button::new(SharedString::from(format!("msg-copy-topic-{}", seq)))
|
||||
.icon(IconName::Copy)
|
||||
.label(i18n::t("主题"))
|
||||
.label(i18n::t("msg.topic"))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(i18n::t("复制主题"))
|
||||
.accessibility_label(i18n::t("复制主题"))
|
||||
.tooltip(i18n::t("msg.copy_topic"))
|
||||
.accessibility_label(i18n::t("msg.copy_topic"))
|
||||
.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(copy_topic.to_string()));
|
||||
window.push_notification(Notification::success(i18n::t("已复制主题")), cx);
|
||||
window.push_notification(Notification::success(i18n::t("msg.topic_copied")), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new(SharedString::from(format!("msg-copy-payload-{}", seq)))
|
||||
.icon(IconName::Copy)
|
||||
.label(i18n::t("负载"))
|
||||
.label(i18n::t("msg.payload"))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(i18n::t("复制负载原文"))
|
||||
.accessibility_label(i18n::t("复制负载原文"))
|
||||
.tooltip(i18n::t("msg.copy_payload"))
|
||||
.accessibility_label(i18n::t("msg.copy_payload"))
|
||||
.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(copy_payload.to_string()));
|
||||
window.push_notification(Notification::success(i18n::t("已复制负载")), cx);
|
||||
window.push_notification(Notification::success(i18n::t("msg.payload_copied")), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new(SharedString::from(format!("msg-copy-detail-{}", seq)))
|
||||
.icon(IconName::Copy)
|
||||
.label(i18n::t("详情"))
|
||||
.label(i18n::t("msg.details"))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(i18n::t("复制完整详情(含 v5 属性)"))
|
||||
.accessibility_label(i18n::t("复制完整详情(含 v5 属性)"))
|
||||
.tooltip(i18n::t("msg.copy_details"))
|
||||
.accessibility_label(i18n::t("msg.copy_details"))
|
||||
.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(copy_detail.clone()));
|
||||
window.push_notification(Notification::success(i18n::t("已复制详情")), cx);
|
||||
window.push_notification(Notification::success(i18n::t("msg.details_copied")), cx);
|
||||
}),
|
||||
);
|
||||
details = details.child(detail_toolbar).child(
|
||||
@@ -350,7 +350,7 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
details = details
|
||||
.child(div().text_xs().font_semibold().child(i18n::t("用户属性")))
|
||||
.child(div().text_xs().font_semibold().child(i18n::t("preset.user_props")))
|
||||
.child(
|
||||
div()
|
||||
.font_family(mono.clone())
|
||||
@@ -363,10 +363,10 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
||||
("Response Topic", record.response_topic.clone()),
|
||||
("Correlation Data", record.correlation_data.clone()),
|
||||
(
|
||||
i18n::t("消息过期"),
|
||||
i18n::t("msg.expiry"),
|
||||
record
|
||||
.message_expiry_interval
|
||||
.map(|v| i18n::tf("{n} 秒", &[("n", &v.to_string())])),
|
||||
.map(|v| i18n::tf("msg.seconds", &[("n", &v)])),
|
||||
),
|
||||
(
|
||||
"订阅标识符",
|
||||
@@ -513,9 +513,9 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(if filtering {
|
||||
i18n::t("没有匹配的消息")
|
||||
i18n::t("msg.no_match")
|
||||
} else {
|
||||
i18n::t("暂无消息,订阅主题后消息会显示在这里")
|
||||
i18n::t("msg.empty")
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -585,8 +585,8 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
||||
.icon(IconName::Close)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(i18n::t("清除订阅过滤"))
|
||||
.accessibility_label(i18n::t("清除订阅过滤"))
|
||||
.tooltip(i18n::t("msg.clear_sub_filter"))
|
||||
.accessibility_label(i18n::t("msg.clear_sub_filter"))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.sub_filter = None;
|
||||
cx.notify();
|
||||
@@ -635,7 +635,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} 条", &[("n", &matched_total.to_string())])),
|
||||
.child(i18n::tf("msg.count", &[("n", &matched_total)])),
|
||||
)
|
||||
.when(matched_total > records.len(), |h| {
|
||||
h.child(
|
||||
@@ -645,7 +645,7 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(i18n::tf(
|
||||
"仅显示最新 {n} 条",
|
||||
&[("n", &MAX_RENDERED_MESSAGES.to_string())],
|
||||
&[("n", &MAX_RENDERED_MESSAGES)],
|
||||
)),
|
||||
)
|
||||
})
|
||||
@@ -662,19 +662,19 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
||||
} else {
|
||||
IconName::Pause
|
||||
})
|
||||
.label(if paused { i18n::t("恢复") } else { i18n::t("暂停") })
|
||||
.label(if paused { i18n::t("common.resume") } else { i18n::t("common.pause") })
|
||||
.ghost()
|
||||
.small()
|
||||
.when(paused, |b| b.selected(true))
|
||||
.tooltip(if paused {
|
||||
i18n::t("恢复实时滚动")
|
||||
i18n::t("msg.resume_tooltip")
|
||||
} else {
|
||||
i18n::t("暂停消息流滚动(消息仍在后台接收)")
|
||||
i18n::t("msg.pause_tooltip")
|
||||
})
|
||||
.accessibility_label(if paused {
|
||||
i18n::t("恢复")
|
||||
i18n::t("common.resume")
|
||||
} else {
|
||||
i18n::t("暂停")
|
||||
i18n::t("common.pause")
|
||||
})
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.paused = !this.paused;
|
||||
@@ -689,12 +689,12 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
||||
let empty = total == 0;
|
||||
Button::new(SharedString::from(format!("msg-clear-{}", self.conn_id)))
|
||||
.icon(IconName::Eraser)
|
||||
.label(i18n::t("清空"))
|
||||
.label(i18n::t("common.clear"))
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(empty)
|
||||
.tooltip(i18n::t("清空当前连接的消息"))
|
||||
.accessibility_label(i18n::t("清空当前连接的消息"))
|
||||
.tooltip(i18n::t("msg.clear_tooltip"))
|
||||
.accessibility_label(i18n::t("msg.clear_tooltip"))
|
||||
.on_click(move |_, window, cx| {
|
||||
// 清空不可撤销,先弹二次确认
|
||||
let weak = weak.clone();
|
||||
@@ -703,13 +703,13 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
||||
let weak = weak.clone();
|
||||
let clear_id = clear_id.clone();
|
||||
alert
|
||||
.title(i18n::t("清空消息"))
|
||||
.description(i18n::t("确定清空当前连接的全部消息吗?此操作不可撤销。"))
|
||||
.title(i18n::t("msg.clear_title"))
|
||||
.description(i18n::t("msg.clear_confirm"))
|
||||
.button_props(
|
||||
DialogButtonProps::default()
|
||||
.show_cancel(true)
|
||||
.cancel_text(i18n::t("取消"))
|
||||
.ok_text(i18n::t("清空"))
|
||||
.cancel_text(i18n::t("common.cancel"))
|
||||
.ok_text(i18n::t("common.clear"))
|
||||
.ok_variant(ButtonVariant::Danger),
|
||||
)
|
||||
.on_ok(move |_, _, cx| {
|
||||
@@ -769,7 +769,7 @@ pub(super) fn render_logs(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(i18n::t("暂无日志")),
|
||||
.child(i18n::t("msg.no_logs")),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -830,7 +830,7 @@ pub(super) fn render_logs(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
.text_xs()
|
||||
.text_color(muted)
|
||||
.font_family(cx.theme().mono_font_family.clone())
|
||||
.child(i18n::t("连接日志")),
|
||||
.child(i18n::t("msg.log_title")),
|
||||
),
|
||||
)
|
||||
.child(body)
|
||||
|
||||
@@ -41,23 +41,23 @@ const QOS: [&str; 3] = ["QoS 0", "QoS 1", "QoS 2"];
|
||||
const PAYLOAD_FORMATS: [&str; 4] = ["Plaintext", "JSON", "Base64", "Hex"];
|
||||
/// Retain Handling 0/1/2 的下拉文案
|
||||
fn retain_handling_labels() -> [&'static str; 3] {
|
||||
[i18n::t("0 每次发送"), i18n::t("1 仅新订阅"), i18n::t("2 不发送")]
|
||||
[i18n::t("sub.rh0"), i18n::t("sub.rh1"), i18n::t("sub.rh2")]
|
||||
}
|
||||
const MAX_RENDERED_MESSAGES: usize = 300;
|
||||
/// 日志渲染上限,与 app.rs 的 MAX_LOGS 保持一致
|
||||
const MAX_RENDERED_LOGS: usize = 3000;
|
||||
/// 订阅色板预设:色相(度)+ 名称
|
||||
const PRESET_HUES: [(f32, &str); 10] = [
|
||||
(0., "红"),
|
||||
(30., "橙"),
|
||||
(60., "黄"),
|
||||
(120., "绿"),
|
||||
(160., "青"),
|
||||
(200., "蓝"),
|
||||
(240., "靛"),
|
||||
(270., "紫"),
|
||||
(300., "品红"),
|
||||
(330., "玫红"),
|
||||
(0., "color.red"),
|
||||
(30., "color.orange"),
|
||||
(60., "color.yellow"),
|
||||
(120., "color.green"),
|
||||
(160., "color.cyan"),
|
||||
(200., "color.blue"),
|
||||
(240., "color.indigo"),
|
||||
(270., "color.purple"),
|
||||
(300., "color.magenta"),
|
||||
(330., "color.rose"),
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@@ -208,29 +208,29 @@ impl ConnectionView {
|
||||
) -> Self {
|
||||
|
||||
let sub_topic = cx.new(|cx| {
|
||||
InputState::new(window, cx).placeholder(i18n::t("订阅主题,支持 # +,多个用逗号/空格分隔"))
|
||||
InputState::new(window, cx).placeholder(i18n::t("sub.topic_placeholder"))
|
||||
});
|
||||
let sub_qos = make_select(&QOS, 0, window, cx);
|
||||
let sub_alias = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("别名(可选)")));
|
||||
let sub_identifier = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("订阅标识符")));
|
||||
let sub_alias = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("sub.alias")));
|
||||
let sub_identifier = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("sub.identifier")));
|
||||
let sub_retain_handling = make_select(&retain_handling_labels(), 0, window, cx);
|
||||
let pub_topic = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("发布主题")));
|
||||
let pub_topic = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("pub.topic_placeholder")));
|
||||
let payload = cx.new(|cx| {
|
||||
TextareaState::new(window, cx)
|
||||
.placeholder(i18n::t("输入消息负载,支持 {{变量名}} 与 {{$ts}} {{$uuid}}"))
|
||||
.placeholder(i18n::t("pub.payload_placeholder"))
|
||||
});
|
||||
let pub_qos = make_select(&QOS, 0, window, cx);
|
||||
let payload_format = make_select(&PAYLOAD_FORMATS, 0, window, cx);
|
||||
let content_type = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("Content-Type(可选)")));
|
||||
let msg_expiry = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("秒,如 60")));
|
||||
let content_type = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("pub.content_type_ph")));
|
||||
let msg_expiry = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("pub.seconds_hint")));
|
||||
let response_topic =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("Response Topic(可选)")));
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("pub.response_topic_ph")));
|
||||
let correlation_data =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("Correlation Data(可选)")));
|
||||
let filter = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("过滤主题或内容…")));
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("pub.correlation_ph")));
|
||||
let filter = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("msg.filter_placeholder")));
|
||||
let user_props = cx.new(|cx| KvEditor::new("pub", &[], window, cx));
|
||||
let preset_name =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("预设名称,如 温度上报")));
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("pub.preset_name_hint")));
|
||||
|
||||
Self {
|
||||
conn_id,
|
||||
@@ -332,7 +332,7 @@ impl Render for ConnectionView {
|
||||
.selected_index(if panel == Panel::Messages { 0 } else { 1 })
|
||||
.child(
|
||||
Tab::new()
|
||||
.label(i18n::t("消息流"))
|
||||
.label(i18n::t("connview.messages"))
|
||||
.selected(panel == Panel::Messages)
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.panel = Panel::Messages;
|
||||
@@ -341,7 +341,7 @@ impl Render for ConnectionView {
|
||||
)
|
||||
.child(
|
||||
Tab::new()
|
||||
.label(i18n::t("日志"))
|
||||
.label(i18n::t("connview.logs"))
|
||||
.selected(panel == Panel::Logs)
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.panel = Panel::Logs;
|
||||
|
||||
@@ -36,8 +36,8 @@ impl ConnectionView {
|
||||
.icon(IconName::PanelBottomOpen)
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip(i18n::t("展开发布面板"))
|
||||
.accessibility_label(i18n::t("展开发布面板"))
|
||||
.tooltip(i18n::t("pub.expand"))
|
||||
.accessibility_label(i18n::t("pub.expand"))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.publish_collapsed = false;
|
||||
cx.notify();
|
||||
@@ -47,7 +47,7 @@ impl ConnectionView {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(i18n::t("发布面板已收起")),
|
||||
.child(i18n::t("pub.collapsed_hint")),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,18 +56,18 @@ impl ConnectionView {
|
||||
pub(crate) fn do_publish(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// 未连接时消息发不出去,先拦截给出与订阅路径一致的提示
|
||||
if !self.engine.is_connected(&self.conn_id) {
|
||||
window.push_notification(Notification::warning(i18n::t("未连接:消息未发送")), cx);
|
||||
window.push_notification(Notification::warning(i18n::t("pub.offline_not_sent")), cx);
|
||||
return;
|
||||
}
|
||||
let mut params = self.collect_publish_params(cx);
|
||||
if params.topic.trim().is_empty() {
|
||||
window.push_notification(Notification::warning(i18n::t("发布主题不能为空")), cx);
|
||||
window.push_notification(Notification::warning(i18n::t("pub.topic_required")), cx);
|
||||
return;
|
||||
}
|
||||
// 过期秒数输入非法时 collect 会静默得到 None,这里显式拦截避免误发
|
||||
let expiry_raw = self.msg_expiry.read(cx).value().to_string();
|
||||
if !expiry_raw.trim().is_empty() && params.message_expiry_interval.is_none() {
|
||||
window.push_notification(Notification::warning(i18n::t("消息过期须为非负整数(秒)")), cx);
|
||||
window.push_notification(Notification::warning(i18n::t("pub.expiry_invalid")), cx);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ impl ConnectionView {
|
||||
params.payload = render_template(¶ms.payload, &vars);
|
||||
// {{topic}} 等变量渲染后可能变空串,渲染完成后再校验一次
|
||||
if params.topic.trim().is_empty() {
|
||||
window.push_notification(Notification::warning(i18n::t("发布主题不能为空")), cx);
|
||||
window.push_notification(Notification::warning(i18n::t("pub.topic_required")), cx);
|
||||
return;
|
||||
}
|
||||
// {{变量}} 同样作用于用户属性的 key/value
|
||||
@@ -93,7 +93,7 @@ impl ConnectionView {
|
||||
if let PayloadFormat::Json = params.payload_format
|
||||
&& let Err(e) = serde_json::from_str::<serde_json::Value>(¶ms.payload) {
|
||||
window.push_notification(
|
||||
Notification::warning(i18n::tf("JSON 格式无效: {e}", &[("e", &e.to_string())])),
|
||||
Notification::warning(i18n::tf("pub.json_invalid", &[("e", &e)])),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
@@ -129,7 +129,7 @@ impl ConnectionView {
|
||||
self.engine.publish(self.conn_id.clone(), params);
|
||||
if hidden {
|
||||
window.push_notification(
|
||||
Notification::success(i18n::t("已发送(被当前过滤条件隐藏,消息流中不显示)")),
|
||||
Notification::success(i18n::t("pub.sent_hidden")),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -281,12 +281,12 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
.tooltip(if count > 0 {
|
||||
i18n::tf(
|
||||
"变量注入({n} 个占位符)",
|
||||
&[("n", &count.to_string())],
|
||||
&[("n", &count)],
|
||||
)
|
||||
} else {
|
||||
i18n::t("变量注入").to_string()
|
||||
i18n::t("pub.vars").to_string()
|
||||
})
|
||||
.accessibility_label(i18n::t("变量注入").to_string())
|
||||
.accessibility_label(i18n::t("pub.vars").to_string())
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if this.show_vars {
|
||||
this.show_vars = false;
|
||||
@@ -315,8 +315,8 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
.ghost()
|
||||
.small()
|
||||
.when(show_props, |b| b.selected(true))
|
||||
.tooltip(i18n::t("MQTT 5 属性"))
|
||||
.accessibility_label(i18n::t("MQTT 5 属性"))
|
||||
.tooltip(i18n::t("pub.mqtt5_props"))
|
||||
.accessibility_label(i18n::t("pub.mqtt5_props"))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.show_props = !this.show_props;
|
||||
cx.notify();
|
||||
@@ -326,12 +326,12 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
.child(
|
||||
Button::new(SharedString::from(format!("publish-{}", self.conn_id)))
|
||||
.icon(IconName::SendHorizontal)
|
||||
.label(i18n::t("发送"))
|
||||
.label(i18n::t("common.send"))
|
||||
.primary()
|
||||
.small()
|
||||
// 未连接时禁用,Ctrl+Enter 路径由 do_publish 内的前置检查兜底
|
||||
.disabled(!connected)
|
||||
.when(!connected, |b| b.tooltip(i18n::t("未连接,无法发送")))
|
||||
.when(!connected, |b| b.tooltip(i18n::t("pub.send_disabled")))
|
||||
.on_click(cx.listener(|this, _, window, cx| this.do_publish(window, cx))),
|
||||
)
|
||||
// 发布栏抽屉把手:收起整个底部面板
|
||||
@@ -340,8 +340,8 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
.icon(IconName::PanelBottom)
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip(i18n::t("收起发布面板"))
|
||||
.accessibility_label(i18n::t("收起发布面板"))
|
||||
.tooltip(i18n::t("pub.collapse"))
|
||||
.accessibility_label(i18n::t("pub.collapse"))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.publish_collapsed = true;
|
||||
cx.notify();
|
||||
@@ -420,8 +420,8 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
.icon(IconName::Bookmark)
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip(i18n::t("消息预设"))
|
||||
.accessibility_label(i18n::t("消息预设"))
|
||||
.tooltip(i18n::t("pub.preset_menu"))
|
||||
.accessibility_label(i18n::t("pub.preset_menu"))
|
||||
.dropdown_menu(move |mut menu, window, cx| {
|
||||
// 应用预设
|
||||
for p in &presets {
|
||||
@@ -445,7 +445,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
// 保存当前
|
||||
let weak_save = weak_view.clone();
|
||||
let name_input = preset_name.clone();
|
||||
menu = menu.item(PopupMenuItem::new(i18n::t("保存当前为预设…")).on_click(
|
||||
menu = menu.item(PopupMenuItem::new(i18n::t("pub.save_preset")).on_click(
|
||||
move |_ev, window, cx| {
|
||||
if let Some(view) = weak_save.upgrade() {
|
||||
view.update(cx, |view, cx| {
|
||||
@@ -456,7 +456,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
));
|
||||
// 打开预设管理对话框(新建 / 编辑 / 删除)
|
||||
let manage_app = app_weak.clone();
|
||||
menu = menu.item(PopupMenuItem::new(i18n::t("管理预设…")).on_click(
|
||||
menu = menu.item(PopupMenuItem::new(i18n::t("pub.manage_presets")).on_click(
|
||||
move |_ev, window, cx| {
|
||||
if let Some(app) = manage_app.upgrade() {
|
||||
crate::ui::dialogs::presets_dialog::open(app, window, cx);
|
||||
@@ -477,7 +477,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
let weak2 = weak_del.clone();
|
||||
let id = p.id.clone();
|
||||
let name = p.name.clone();
|
||||
let label = i18n::tf("删除「{name}」", &[("name", &p.name)]);
|
||||
let label = i18n::tf("pub.delete_item", &[("name", &p.name)]);
|
||||
submenu = submenu.item(
|
||||
PopupMenuItem::new(SharedString::from(label)).on_click(
|
||||
move |_ev, window, cx| {
|
||||
@@ -489,7 +489,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
let weak = weak.clone();
|
||||
let pid = pid.clone();
|
||||
alert
|
||||
.title(i18n::t("删除预设"))
|
||||
.title(i18n::t("pub.delete_title"))
|
||||
.description(i18n::tf(
|
||||
"确定删除预设「{name}」吗?此操作不可撤销。",
|
||||
&[("name", &pname)],
|
||||
@@ -497,8 +497,8 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
.button_props(
|
||||
DialogButtonProps::default()
|
||||
.show_cancel(true)
|
||||
.cancel_text(i18n::t("取消"))
|
||||
.ok_text(i18n::t("删除"))
|
||||
.cancel_text(i18n::t("common.cancel"))
|
||||
.ok_text(i18n::t("common.delete"))
|
||||
.ok_variant(ButtonVariant::Danger),
|
||||
)
|
||||
.on_ok(move |_, _, cx| {
|
||||
@@ -548,9 +548,9 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
window.open_dialog(cx, move |dialog, _, _| {
|
||||
dialog
|
||||
.w(px(420.))
|
||||
.title(i18n::t("保存消息预设"))
|
||||
.title(i18n::t("pub.save_title"))
|
||||
.child(v_flex().gap_3().child(
|
||||
crate::ui::widgets::field(i18n::t("预设名称"), Input::new(&name_for_ok)),
|
||||
crate::ui::widgets::field(i18n::t("aliyun.preset_name"), Input::new(&name_for_ok)),
|
||||
))
|
||||
.footer(
|
||||
h_flex()
|
||||
@@ -559,13 +559,13 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("preset-save-cancel")
|
||||
.label(i18n::t("取消"))
|
||||
.label(i18n::t("common.cancel"))
|
||||
.outline()
|
||||
.on_click(move |_, window, cx| window.close_dialog(cx)),
|
||||
)
|
||||
.child(
|
||||
Button::new("preset-save-ok")
|
||||
.label(i18n::t("保存"))
|
||||
.label(i18n::t("common.save"))
|
||||
.primary()
|
||||
.on_click({
|
||||
let name_input = name_for_ok.clone();
|
||||
@@ -578,7 +578,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
.to_string();
|
||||
if name.is_empty() {
|
||||
window.push_notification(
|
||||
Notification::warning(i18n::t("请填写预设名称")),
|
||||
Notification::warning(i18n::t("pub.preset_name_required")),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
@@ -604,7 +604,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
&& params.message_expiry_interval.is_none()
|
||||
{
|
||||
window.push_notification(
|
||||
Notification::warning(i18n::t("消息过期须为非负整数(秒)")),
|
||||
Notification::warning(i18n::t("pub.expiry_invalid")),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
@@ -621,9 +621,9 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
||||
cx.notify();
|
||||
});
|
||||
let msg = if existed {
|
||||
i18n::tf("已覆盖预设「{name}」", &[("name", &name)])
|
||||
i18n::tf("pub.preset_overwritten", &[("name", &name)])
|
||||
} else {
|
||||
i18n::t("预设已保存").to_string()
|
||||
i18n::t("pub.preset_saved").to_string()
|
||||
};
|
||||
window.push_notification(
|
||||
Notification::success(msg),
|
||||
|
||||
@@ -35,8 +35,8 @@ impl ConnectionView {
|
||||
.icon(IconName::PanelLeftOpen)
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip(i18n::t("展开订阅面板"))
|
||||
.accessibility_label(i18n::t("展开订阅面板"))
|
||||
.tooltip(i18n::t("sub.expand"))
|
||||
.accessibility_label(i18n::t("sub.expand"))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.subs_collapsed = false;
|
||||
cx.notify();
|
||||
@@ -169,7 +169,7 @@ impl ConnectionView {
|
||||
let raw = self.sub_topic.read(cx).value().to_string();
|
||||
let topics = split_topic_list(&raw);
|
||||
if topics.is_empty() {
|
||||
window.push_notification(Notification::warning(i18n::t("订阅主题不能为空")), cx);
|
||||
window.push_notification(Notification::warning(i18n::t("sub.topic_required")), cx);
|
||||
return;
|
||||
}
|
||||
let qos = self.sub_qos.read(cx).selected_value().copied().unwrap_or(0) as u8;
|
||||
@@ -189,7 +189,7 @@ impl ConnectionView {
|
||||
Ok(v) if v > 0 => sub_identifier = Some(v),
|
||||
_ => {
|
||||
window.push_notification(
|
||||
Notification::warning(i18n::t("订阅标识符须为正整数")),
|
||||
Notification::warning(i18n::t("sub.identifier_invalid")),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
@@ -303,9 +303,9 @@ impl ConnectionView {
|
||||
.map(|c| c.auto_resubscribe)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
i18n::t("未连接:订阅已保存,连接后自动恢复")
|
||||
i18n::t("sub.saved_offline_resub")
|
||||
} else {
|
||||
i18n::t("未连接:订阅已保存")
|
||||
i18n::t("sub.saved_offline")
|
||||
};
|
||||
window.push_notification(Notification::warning(hint), cx);
|
||||
}
|
||||
@@ -364,7 +364,7 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context<Self>) -> impl In
|
||||
.bg(cx.theme().warning.alpha(0.15))
|
||||
.text_xs()
|
||||
.text_color(cx.theme().warning)
|
||||
.child(i18n::tf("正在编辑:{topic}", &[("topic", &topic)])),
|
||||
.child(i18n::tf("sub.editing", &[("topic", &topic)])),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
@@ -403,12 +403,12 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context<Self>) -> impl In
|
||||
h.child(
|
||||
Button::new(SharedString::from(format!("sub-adv-{}", self.conn_id)))
|
||||
.icon(IconName::SlidersHorizontal)
|
||||
.label(i18n::t("高级"))
|
||||
.label(i18n::t("common.advanced"))
|
||||
.ghost()
|
||||
.small()
|
||||
.when(show_advanced, |b| b.selected(true))
|
||||
.tooltip(i18n::t("MQTT 5 订阅选项"))
|
||||
.accessibility_label(i18n::t("MQTT 5 订阅选项"))
|
||||
.tooltip(i18n::t("sub.mqtt5_options"))
|
||||
.accessibility_label(i18n::t("sub.mqtt5_options"))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.sub_show_advanced = !this.sub_show_advanced;
|
||||
cx.notify();
|
||||
@@ -418,7 +418,7 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context<Self>) -> impl In
|
||||
.child(
|
||||
Button::new(SharedString::from(format!("subscribe-{}", self.conn_id)))
|
||||
.icon(if editing { IconName::Check } else { IconName::Plus })
|
||||
.label(if editing { i18n::t("更新") } else { i18n::t("订阅") })
|
||||
.label(if editing { i18n::t("common.update") } else { i18n::t("connview.subscriptions") })
|
||||
// 编辑用 warning 色与新增的 primary 区分
|
||||
.when(editing, |b| b.warning())
|
||||
.when(!editing, |b| b.primary())
|
||||
@@ -431,8 +431,8 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context<Self>) -> impl In
|
||||
.icon(IconName::Close)
|
||||
.ghost()
|
||||
.small()
|
||||
.tooltip(i18n::t("取消编辑"))
|
||||
.accessibility_label(i18n::t("取消编辑"))
|
||||
.tooltip(i18n::t("sub.cancel_edit"))
|
||||
.accessibility_label(i18n::t("sub.cancel_edit"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.editing = None;
|
||||
// 清空回填到输入框的主题/别名/高级字段,回到新增态
|
||||
@@ -470,7 +470,7 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context<Self>) -> impl In
|
||||
cx.notify();
|
||||
}),
|
||||
))
|
||||
.child(div().text_xs().child(i18n::t("No Local"))),
|
||||
.child(div().text_xs().child(i18n::t("sub.no_local"))),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -483,11 +483,11 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context<Self>) -> impl In
|
||||
cx.notify();
|
||||
}),
|
||||
))
|
||||
.child(div().text_xs().child(i18n::t("Retain As Published"))),
|
||||
.child(div().text_xs().child(i18n::t("sub.rap"))),
|
||||
)
|
||||
.child(
|
||||
div().w(px(150.)).child(field(
|
||||
i18n::t("Retain Handling"),
|
||||
i18n::t("sub.retain_handling"),
|
||||
Select::new(&sub_retain_handling).small(),
|
||||
)),
|
||||
),
|
||||
@@ -527,7 +527,7 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.text_center()
|
||||
.child(i18n::t("订阅主题后,消息会显示在右侧")),
|
||||
.child(i18n::t("sub.empty_hint")),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -548,8 +548,8 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
||||
Button::new(trigger_id)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(i18n::t("订阅颜色"))
|
||||
.accessibility_label(i18n::t("订阅颜色"))
|
||||
.tooltip(i18n::t("sub.color"))
|
||||
.accessibility_label(i18n::t("sub.color"))
|
||||
.child(
|
||||
div()
|
||||
.size(px(11.))
|
||||
@@ -562,8 +562,8 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
||||
Button::new(trigger_id)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(i18n::t("订阅颜色"))
|
||||
.accessibility_label(i18n::t("订阅颜色"))
|
||||
.tooltip(i18n::t("sub.color"))
|
||||
.accessibility_label(i18n::t("sub.color"))
|
||||
.child(
|
||||
div()
|
||||
.size(px(11.))
|
||||
@@ -583,7 +583,7 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
||||
let sid = sid.clone();
|
||||
let swatch = hue_color(hue);
|
||||
// 菜单只显示颜色名,角度数值对用户没有意义
|
||||
let label = SharedString::from(name);
|
||||
let label = SharedString::from(crate::ui::i18n::t(name));
|
||||
menu = menu.item(
|
||||
PopupMenuItem::element(move |_, _| {
|
||||
h_flex()
|
||||
@@ -611,7 +611,7 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
||||
menu = menu.separator();
|
||||
let weak_rand = weak_menu.clone();
|
||||
let sid_rand = sid.clone();
|
||||
menu = menu.item(PopupMenuItem::new(i18n::t("随机颜色")).on_click(move |_, _, cx| {
|
||||
menu = menu.item(PopupMenuItem::new(i18n::t("sub.color_random")).on_click(move |_, _, cx| {
|
||||
let hue = (uuid::Uuid::new_v4().as_u128() % 360) as f32;
|
||||
weak_rand
|
||||
.update(cx, |view, cx| {
|
||||
@@ -621,7 +621,7 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
||||
}));
|
||||
let weak_clear = weak_menu.clone();
|
||||
let sid_clear = sid.clone();
|
||||
menu = menu.item(PopupMenuItem::new(i18n::t("清除颜色")).on_click(move |_, _, cx| {
|
||||
menu = menu.item(PopupMenuItem::new(i18n::t("sub.color_clear")).on_click(move |_, _, cx| {
|
||||
weak_clear
|
||||
.update(cx, |view, cx| {
|
||||
view.set_subscription_color(&sid_clear, None, cx);
|
||||
@@ -637,8 +637,8 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
||||
.icon(if enabled { IconName::Eye } else { IconName::EyeOff })
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(if enabled { i18n::t("停用订阅") } else { i18n::t("启用订阅") })
|
||||
.accessibility_label(if enabled { i18n::t("停用订阅") } else { i18n::t("启用订阅") })
|
||||
.tooltip(if enabled { i18n::t("sub.disable") } else { i18n::t("sub.enable") })
|
||||
.accessibility_label(if enabled { i18n::t("sub.disable") } else { i18n::t("sub.enable") })
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.toggle_subscription_enabled(&eye_sid, cx);
|
||||
}));
|
||||
@@ -648,8 +648,8 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
||||
.icon(IconName::Pencil)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(i18n::t("编辑订阅"))
|
||||
.accessibility_label(i18n::t("编辑订阅"))
|
||||
.tooltip(i18n::t("sub.edit"))
|
||||
.accessibility_label(i18n::t("sub.edit"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
let sub = this
|
||||
.with_app(cx, |app| {
|
||||
@@ -670,8 +670,8 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
||||
.icon(IconName::Close)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(i18n::t("取消订阅"))
|
||||
.accessibility_label(i18n::t("取消订阅"))
|
||||
.tooltip(i18n::t("sub.unsubscribe"))
|
||||
.accessibility_label(i18n::t("sub.unsubscribe"))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
// 若删除的是当前过滤主题,一并清掉消息过滤
|
||||
if this.sub_filter.as_deref() == Some(del_topic.as_str()) {
|
||||
@@ -778,14 +778,14 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
||||
.px_3()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(div().text_sm().font_semibold().child(i18n::t("订阅")))
|
||||
.child(div().text_sm().font_semibold().child(i18n::t("connview.subscriptions")))
|
||||
.child(
|
||||
Button::new("subs-collapse")
|
||||
.icon(IconName::PanelLeftClose)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(i18n::t("收起订阅面板"))
|
||||
.accessibility_label(i18n::t("收起订阅面板"))
|
||||
.tooltip(i18n::t("sub.collapse"))
|
||||
.accessibility_label(i18n::t("sub.collapse"))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.subs_collapsed = true;
|
||||
cx.notify();
|
||||
|
||||
@@ -125,7 +125,7 @@ pub(super) fn format_payload_detail(payload: &str, raw: Option<&[u8]>, format: D
|
||||
pub(super) fn details_copy_text(record: &MqttRecord, show_millis: bool) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str(&format!("Topic: {}\n", record.topic));
|
||||
out.push_str(&format!("Direction: {}\n", record.direction.label()));
|
||||
out.push_str(&format!("Direction: {}\n", crate::ui::i18n::t(record.direction.label())));
|
||||
out.push_str(&format!("QoS: {}\n", record.qos));
|
||||
out.push_str(&format!("Retain: {}\n", record.retain));
|
||||
out.push_str(&format!(
|
||||
|
||||
@@ -21,7 +21,7 @@ pub(super) fn sync_var_rows(&mut self, window: &mut Window, cx: &mut Context<Sel
|
||||
if keys.is_empty() {
|
||||
self.show_vars = false;
|
||||
window.push_notification(
|
||||
Notification::warning(i18n::t("主题/负载中没有 {{变量}} 引用")),
|
||||
Notification::warning(i18n::t("var.no_reference")),
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
@@ -40,7 +40,7 @@ pub(super) fn sync_var_rows(&mut self, window: &mut Window, cx: &mut Context<Sel
|
||||
.map(|v| v.value.clone())
|
||||
.unwrap_or_default();
|
||||
let input = cx.new(|cx| {
|
||||
let mut s = InputState::new(window, cx).placeholder(i18n::t("变量值"));
|
||||
let mut s = InputState::new(window, cx).placeholder(i18n::t("var.value"));
|
||||
// set_value 不会触发 InputEvent::Change,不会误写回
|
||||
if !initial.is_empty() {
|
||||
s.set_value(initial.as_str(), window, cx);
|
||||
@@ -120,7 +120,7 @@ pub(super) fn render_vars_panel(&self, cx: &mut Context<Self>) -> impl IntoEleme
|
||||
.font_semibold()
|
||||
.child(i18n::tf(
|
||||
"变量({n})",
|
||||
&[("n", &self.var_rows.len().to_string())],
|
||||
&[("n", &self.var_rows.len())],
|
||||
)),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
@@ -129,8 +129,8 @@ pub(super) fn render_vars_panel(&self, cx: &mut Context<Self>) -> impl IntoEleme
|
||||
.icon(IconName::RefreshCw)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(i18n::t("重新提取占位符"))
|
||||
.accessibility_label(i18n::t("重新提取占位符"))
|
||||
.tooltip(i18n::t("var.reextract"))
|
||||
.accessibility_label(i18n::t("var.reextract"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.sync_var_rows(window, cx);
|
||||
})),
|
||||
@@ -140,8 +140,8 @@ pub(super) fn render_vars_panel(&self, cx: &mut Context<Self>) -> impl IntoEleme
|
||||
.icon(IconName::ChevronUp)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tooltip(i18n::t("收起"))
|
||||
.accessibility_label(i18n::t("收起"))
|
||||
.tooltip(i18n::t("common.collapse"))
|
||||
.accessibility_label(i18n::t("common.collapse"))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.show_vars = false;
|
||||
cx.notify();
|
||||
@@ -153,7 +153,7 @@ pub(super) fn render_vars_panel(&self, cx: &mut Context<Self>) -> impl IntoEleme
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(muted)
|
||||
.child(i18n::t("内置:{{$ts}}(秒) {{$ts_ms}}(毫秒) {{$uuid}} · 发布时注入")),
|
||||
.child(i18n::t("var.builtin_inline")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -20,7 +20,9 @@ use crate::ui::app::MqttXApp;
|
||||
use crate::ui::widgets::{field, make_select, OptionDelegate};
|
||||
use crate::ui::i18n;
|
||||
|
||||
const AUTH_MODES: [&str; 2] = ["Group 令牌 (Token)", "一机一密"];
|
||||
fn auth_mode_labels() -> [&'static str; 2] {
|
||||
[i18n::t("aliyun.auth_token_mode"), i18n::t("aliyun.auth_triple_mode")]
|
||||
}
|
||||
|
||||
struct AliyunDialog {
|
||||
app: Entity<MqttXApp>,
|
||||
@@ -60,12 +62,12 @@ impl AliyunDialog {
|
||||
app,
|
||||
presets,
|
||||
selected,
|
||||
name: text_field!(cx, window, &first.name, i18n::t("预设名称")),
|
||||
group: text_field!(cx, window, &first.group, i18n::t("分组(可选)")),
|
||||
name: text_field!(cx, window, &first.name, i18n::t("aliyun.preset_name")),
|
||||
group: text_field!(cx, window, &first.group, i18n::t("aliyun.group")),
|
||||
instance_id: text_field!(cx, window, &first.instance_id, "post-cn-xxxx"),
|
||||
region: text_field!(cx, window, &first.region, i18n::t("如 cn-shanghai(可留空)")),
|
||||
region: text_field!(cx, window, &first.region, i18n::t("aliyun.region_ph")),
|
||||
auth_mode: make_select(
|
||||
&AUTH_MODES,
|
||||
&auth_mode_labels(),
|
||||
match first.auth_mode {
|
||||
AliyunAuthMode::Token => 0,
|
||||
AliyunAuthMode::DeviceCredential => 1,
|
||||
@@ -81,12 +83,12 @@ impl AliyunDialog {
|
||||
.masked(true)
|
||||
}),
|
||||
group_id: text_field!(cx, window, &first.group_id, "GID-xxx"),
|
||||
device_id: text_field!(cx, window, &first.device_id, i18n::t("设备 ID")),
|
||||
product_key: text_field!(cx, window, &first.product_key, i18n::t("一机一密:ProductKey")),
|
||||
device_id: text_field!(cx, window, &first.device_id, i18n::t("aliyun.device_id")),
|
||||
product_key: text_field!(cx, window, &first.product_key, i18n::t("aliyun.product_key_ph")),
|
||||
device_secret: cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.default_value(&first.device_secret)
|
||||
.placeholder(i18n::t("一机一密:DeviceSecret"))
|
||||
.placeholder(i18n::t("aliyun.device_secret_ph"))
|
||||
.masked(true)
|
||||
}),
|
||||
}
|
||||
@@ -186,7 +188,7 @@ impl AliyunDialog {
|
||||
fn save_preset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let preset = self.collect(cx);
|
||||
if preset.name.trim().is_empty() {
|
||||
window.push_notification(Notification::warning(i18n::t("请填写预设名称")), cx);
|
||||
window.push_notification(Notification::warning(i18n::t("pub.preset_name_required")), cx);
|
||||
return;
|
||||
}
|
||||
match self.selected {
|
||||
@@ -198,7 +200,7 @@ impl AliyunDialog {
|
||||
a.save_aliyun(saved);
|
||||
cx.notify();
|
||||
});
|
||||
window.push_notification(Notification::success(i18n::t("预设已保存")), cx);
|
||||
window.push_notification(Notification::success(i18n::t("pub.preset_saved")), cx);
|
||||
}
|
||||
|
||||
fn connect(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -217,7 +219,7 @@ impl AliyunDialog {
|
||||
window.close_dialog(cx);
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(Notification::error(i18n::tf("无法生成连接: {e}", &[("e", &e.to_string())])), cx);
|
||||
window.push_notification(Notification::error(i18n::tf("aliyun.build_failed", &[("e", &e)])), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,7 +235,7 @@ impl Render for AliyunDialog {
|
||||
v_flex()
|
||||
.w(px(160.))
|
||||
.gap_1()
|
||||
.child(div().text_xs().font_semibold().child(i18n::t("已保存预设")))
|
||||
.child(div().text_xs().font_semibold().child(i18n::t("aliyun.saved_presets")))
|
||||
.children(
|
||||
self.presets
|
||||
.iter()
|
||||
@@ -257,7 +259,7 @@ impl Render for AliyunDialog {
|
||||
.child(
|
||||
Button::new("aliyun-new")
|
||||
.icon(IconName::Plus)
|
||||
.label(i18n::t("新建"))
|
||||
.label(i18n::t("common.new"))
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -271,36 +273,36 @@ impl Render for AliyunDialog {
|
||||
.flex_1()
|
||||
.min_w(px(0.))
|
||||
.gap_2()
|
||||
.child(field(i18n::t("预设名称"), Input::new(&self.name)))
|
||||
.child(field(i18n::t("aliyun.preset_name"), Input::new(&self.name)))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div().flex_1().min_w(px(0.)).child(field(
|
||||
i18n::t("实例 ID"),
|
||||
i18n::t("aliyun.instance_id"),
|
||||
Input::new(&self.instance_id),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().min_w(px(0.)).child(field(
|
||||
i18n::t("地域"),
|
||||
i18n::t("aliyun.region"),
|
||||
Input::new(&self.region),
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(field(i18n::t("鉴权方式"), Select::new(&self.auth_mode)))
|
||||
.child(field(i18n::t("aliyun.auth_mode"), Select::new(&self.auth_mode)))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div().flex_1().min_w(px(0.)).child(field(
|
||||
i18n::t("AccessKey ID"),
|
||||
i18n::t("aliyun.access_key_id"),
|
||||
Input::new(&self.access_key_id),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().min_w(px(0.)).child(field(
|
||||
i18n::t("AccessKey Secret"),
|
||||
i18n::t("aliyun.access_key_secret"),
|
||||
Input::new(&self.access_key_secret).mask_toggle(),
|
||||
)),
|
||||
),
|
||||
@@ -310,13 +312,13 @@ impl Render for AliyunDialog {
|
||||
.gap_2()
|
||||
.child(
|
||||
div().flex_1().min_w(px(0.)).child(field(
|
||||
i18n::t("Group ID"),
|
||||
i18n::t("aliyun.group_id"),
|
||||
Input::new(&self.group_id),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().min_w(px(0.)).child(field(
|
||||
i18n::t("设备 ID"),
|
||||
i18n::t("aliyun.device_id"),
|
||||
Input::new(&self.device_id),
|
||||
)),
|
||||
),
|
||||
@@ -326,13 +328,13 @@ impl Render for AliyunDialog {
|
||||
.gap_2()
|
||||
.child(
|
||||
div().flex_1().min_w(px(0.)).child(field(
|
||||
i18n::t("ProductKey(一机一密)"),
|
||||
i18n::t("aliyun.product_key"),
|
||||
Input::new(&self.product_key),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().min_w(px(0.)).child(field(
|
||||
i18n::t("DeviceSecret(一机一密)"),
|
||||
i18n::t("aliyun.device_secret"),
|
||||
Input::new(&self.device_secret).mask_toggle(),
|
||||
)),
|
||||
),
|
||||
@@ -341,7 +343,7 @@ impl Render for AliyunDialog {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Token 模式(云消息队列 MQTT 版):接入点 {实例ID}.mqtt.aliyuncs.com,clientId={GroupId}@@@{设备ID},密码为 HMAC-SHA1 签名;一机一密(物联网平台):接入点 {ProductKey}.iot-as-mqtt.{地域}.aliyuncs.com,按官方三元组规范签名"),
|
||||
.child(i18n::t("aliyun.help")),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -349,7 +351,7 @@ impl Render for AliyunDialog {
|
||||
.justify_end()
|
||||
.child(
|
||||
Button::new("aliyun-save")
|
||||
.label(i18n::t("保存预设"))
|
||||
.label(i18n::t("aliyun.save_preset"))
|
||||
.outline()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.save_preset(window, cx)
|
||||
@@ -357,7 +359,7 @@ impl Render for AliyunDialog {
|
||||
)
|
||||
.child(
|
||||
Button::new("aliyun-connect")
|
||||
.label(i18n::t("生成并连接"))
|
||||
.label(i18n::t("aliyun.generate_connect"))
|
||||
.primary()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.connect(window, cx)
|
||||
@@ -379,7 +381,7 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
||||
window.open_dialog(cx, move |dialog, _, _| {
|
||||
dialog
|
||||
.w(px(760.))
|
||||
.title(i18n::t("阿里云 IoT 设备"))
|
||||
.title(i18n::t("aliyun.title"))
|
||||
.child(dialog_view.clone())
|
||||
.footer(gpui_kit::div())
|
||||
});
|
||||
|
||||
@@ -81,17 +81,17 @@ impl PresetsDialog {
|
||||
let name_value = first.as_ref().map(|p| p.name.clone()).unwrap_or_default();
|
||||
let name = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(i18n::t("预设名称,如 温度上报"))
|
||||
.placeholder(i18n::t("pub.preset_name_hint"))
|
||||
.default_value(name_value)
|
||||
});
|
||||
let topic = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(i18n::t("发布主题,支持 {{变量名}}"))
|
||||
.placeholder(i18n::t("pub.topic_preset_ph"))
|
||||
.default_value(params.topic.clone())
|
||||
});
|
||||
let payload = cx.new(|cx| {
|
||||
TextareaState::new(window, cx)
|
||||
.placeholder(i18n::t("消息负载,支持 {{变量名}}"))
|
||||
.placeholder(i18n::t("pub.payload_preset_ph"))
|
||||
.default_value(params.payload.clone())
|
||||
});
|
||||
let qos = make_select(&QOS, params.qos.min(2) as usize, window, cx);
|
||||
@@ -102,11 +102,11 @@ impl PresetsDialog {
|
||||
let payload_format = make_select(&PAYLOAD_FORMATS, fmt_idx, window, cx);
|
||||
let content_type = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(i18n::t("Content-Type(可选)"))
|
||||
.placeholder(i18n::t("pub.content_type_ph"))
|
||||
.default_value(params.content_type.clone().unwrap_or_default())
|
||||
});
|
||||
let msg_expiry = cx.new(|cx| {
|
||||
InputState::new(window, cx).placeholder(i18n::t("秒,如 60")).default_value(
|
||||
InputState::new(window, cx).placeholder(i18n::t("pub.seconds_hint")).default_value(
|
||||
params
|
||||
.message_expiry_interval
|
||||
.map(|v| v.to_string())
|
||||
@@ -115,12 +115,12 @@ impl PresetsDialog {
|
||||
});
|
||||
let response_topic = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(i18n::t("Response Topic(可选)"))
|
||||
.placeholder(i18n::t("pub.response_topic_ph"))
|
||||
.default_value(params.response_topic.clone().unwrap_or_default())
|
||||
});
|
||||
let correlation_data = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(i18n::t("Correlation Data(可选)"))
|
||||
.placeholder(i18n::t("pub.correlation_ph"))
|
||||
.default_value(params.correlation_data.clone().unwrap_or_default())
|
||||
});
|
||||
let user_props =
|
||||
@@ -268,10 +268,10 @@ impl PresetsDialog {
|
||||
|
||||
/// 「新建」:创建默认名称预设(重名自动加序号)并立即落盘、进入编辑。
|
||||
fn create_new(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let mut name = i18n::t("新预设").to_string();
|
||||
let mut name = i18n::t("preset.default_name").to_string();
|
||||
let mut n = 2;
|
||||
while self.presets.iter().any(|p| p.name == name) {
|
||||
name = i18n::tf("新预设 {n}", &[("n", &n.to_string())]);
|
||||
name = i18n::tf("preset.default_name_n", &[("n", &n)]);
|
||||
n += 1;
|
||||
}
|
||||
self.presets.push(PublishPreset::new(name, PublishParams::default()));
|
||||
@@ -283,12 +283,12 @@ impl PresetsDialog {
|
||||
/// 「保存」:name 校验 + 冲突检查通过后按 id 更新,再落盘。
|
||||
fn save(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(idx) = self.selected else {
|
||||
window.push_notification(Notification::warning(i18n::t("请先选择或新建预设")), cx);
|
||||
window.push_notification(Notification::warning(i18n::t("preset.pick_first")), cx);
|
||||
return;
|
||||
};
|
||||
let name = self.name.read(cx).value().trim().to_string();
|
||||
if name.is_empty() {
|
||||
window.push_notification(Notification::warning(i18n::t("请填写预设名称")), cx);
|
||||
window.push_notification(Notification::warning(i18n::t("pub.preset_name_required")), cx);
|
||||
return;
|
||||
}
|
||||
// 与其他预设(不含自身)重名 → 报错且不落盘
|
||||
@@ -312,7 +312,7 @@ impl PresetsDialog {
|
||||
let params = self.collect(cx);
|
||||
if !expiry_raw.trim().is_empty() && params.message_expiry_interval.is_none() {
|
||||
window.push_notification(
|
||||
Notification::warning(i18n::t("消息过期须为非负整数(秒)")),
|
||||
Notification::warning(i18n::t("pub.expiry_invalid")),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
@@ -332,7 +332,7 @@ impl PresetsDialog {
|
||||
/// 「删除」:`open_alert_dialog` 二次确认后从列表移除并落盘。
|
||||
fn ask_delete(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(idx) = self.selected else {
|
||||
window.push_notification(Notification::warning(i18n::t("请先选择要删除的预设")), cx);
|
||||
window.push_notification(Notification::warning(i18n::t("preset.pick_delete_first")), cx);
|
||||
return;
|
||||
};
|
||||
let pid = self.presets[idx].id.clone();
|
||||
@@ -343,7 +343,7 @@ impl PresetsDialog {
|
||||
let weak = weak.clone();
|
||||
let pid = pid.clone();
|
||||
alert
|
||||
.title(i18n::t("删除预设"))
|
||||
.title(i18n::t("pub.delete_title"))
|
||||
.description(i18n::tf(
|
||||
"确定删除预设「{name}」吗?此操作不可撤销。",
|
||||
&[("name", &pname)],
|
||||
@@ -351,8 +351,8 @@ impl PresetsDialog {
|
||||
.button_props(
|
||||
DialogButtonProps::default()
|
||||
.show_cancel(true)
|
||||
.cancel_text(i18n::t("取消"))
|
||||
.ok_text(i18n::t("删除"))
|
||||
.cancel_text(i18n::t("common.cancel"))
|
||||
.ok_text(i18n::t("common.delete"))
|
||||
.ok_variant(ButtonVariant::Danger),
|
||||
)
|
||||
.on_ok(move |_, window, cx| {
|
||||
@@ -398,7 +398,7 @@ impl Render for PresetsDialog {
|
||||
.text_xs()
|
||||
.text_color(muted)
|
||||
.text_center()
|
||||
.child(i18n::t("暂无发布预设,点击上方「新建」创建")),
|
||||
.child(i18n::t("preset.empty")),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -460,10 +460,10 @@ impl Render for PresetsDialog {
|
||||
.flex_1()
|
||||
.min_w(px(0.))
|
||||
.gap_2()
|
||||
.child(field(i18n::t("预设名称"), Input::new(&self.name)))
|
||||
.child(field(i18n::t("发布主题"), Input::new(&self.topic)))
|
||||
.child(field(i18n::t("aliyun.preset_name"), Input::new(&self.name)))
|
||||
.child(field(i18n::t("pub.topic_placeholder"), Input::new(&self.topic)))
|
||||
.child(field(
|
||||
i18n::t("Payload(原文保存,发送时渲染变量)"),
|
||||
i18n::t("preset.payload_field"),
|
||||
Textarea::new(&self.payload).h(px(96.)),
|
||||
))
|
||||
.child(
|
||||
@@ -494,13 +494,13 @@ impl Render for PresetsDialog {
|
||||
.flex_1()
|
||||
.min_w(px(0.))
|
||||
.child(field(
|
||||
i18n::t("Payload 格式"),
|
||||
i18n::t("preset.payload_format"),
|
||||
Select::new(&self.payload_format).small(),
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(div().h(px(1.)).bg(cx.theme().border).w_full())
|
||||
.child(div().text_xs().font_semibold().child(i18n::t("MQTT 5 属性")))
|
||||
.child(div().text_xs().font_semibold().child(i18n::t("pub.mqtt5_props")))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
@@ -512,7 +512,7 @@ impl Render for PresetsDialog {
|
||||
)
|
||||
.child(
|
||||
div().w(px(120.)).child(field(
|
||||
i18n::t("消息过期(秒)"),
|
||||
i18n::t("pub.expiry_field"),
|
||||
Input::new(&self.msg_expiry).small(),
|
||||
)),
|
||||
),
|
||||
@@ -533,7 +533,7 @@ impl Render for PresetsDialog {
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(field(i18n::t("用户属性"), self.user_props.clone()));
|
||||
.child(field(i18n::t("preset.user_props"), self.user_props.clone()));
|
||||
|
||||
h_flex()
|
||||
.gap_3()
|
||||
@@ -548,11 +548,11 @@ impl Render for PresetsDialog {
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(div().text_sm().font_semibold().child(i18n::t("发布预设")))
|
||||
.child(div().text_sm().font_semibold().child(i18n::t("preset.section")))
|
||||
.child(
|
||||
Button::new("preset-new")
|
||||
.icon(IconName::Plus)
|
||||
.label(i18n::t("新建"))
|
||||
.label(i18n::t("common.new"))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -584,7 +584,7 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
||||
let has_selection = dialog_view.read(cx).selected.is_some();
|
||||
dialog
|
||||
.w(px(760.))
|
||||
.title(i18n::t("管理发布预设"))
|
||||
.title(i18n::t("preset.manage_title"))
|
||||
.child(body)
|
||||
.footer(
|
||||
h_flex()
|
||||
@@ -593,7 +593,7 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
||||
.child(
|
||||
Button::new("preset-delete")
|
||||
.icon(IconName::Trash)
|
||||
.label(i18n::t("删除"))
|
||||
.label(i18n::t("common.delete"))
|
||||
.danger()
|
||||
.outline()
|
||||
.disabled(!has_selection)
|
||||
@@ -606,13 +606,13 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("preset-close")
|
||||
.label(i18n::t("关闭"))
|
||||
.label(i18n::t("common.close"))
|
||||
.outline()
|
||||
.on_click(|_, window, cx| window.close_dialog(cx)),
|
||||
)
|
||||
.child(
|
||||
Button::new("preset-save")
|
||||
.label(i18n::t("保存"))
|
||||
.label(i18n::t("common.save"))
|
||||
.primary()
|
||||
.disabled(!has_selection)
|
||||
.on_click(move |_, window, cx| {
|
||||
|
||||
@@ -90,7 +90,7 @@ impl ResourceMonitorDialog {
|
||||
.get(&c.id)
|
||||
.copied()
|
||||
.map(|s| s.label())
|
||||
.unwrap_or(i18n::t("未连接")),
|
||||
.unwrap_or(i18n::t("status.disconnected")),
|
||||
connected: app.engine.is_connected(&c.id),
|
||||
messages,
|
||||
bytes,
|
||||
@@ -247,7 +247,7 @@ impl ResourceMonitorDialog {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(muted)
|
||||
.child(i18n::tf("{n} 条消息", &[("n", &c.messages.to_string())])),
|
||||
.child(i18n::tf("resmon.n_msgs", &[("n", &c.messages)])),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -263,11 +263,16 @@ impl ResourceMonitorDialog {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(muted)
|
||||
.child(format!(
|
||||
"预算占用 {:.0}%({} / {})",
|
||||
pct,
|
||||
Self::fmt_bytes(c.bytes as u64),
|
||||
Self::fmt_bytes(MAX_CONNECTION_MESSAGE_BYTES as u64)
|
||||
.child(i18n::tf(
|
||||
"预算占用 {p}%({used} / {total})",
|
||||
&[
|
||||
("p", &format!("{:.0}", pct)),
|
||||
("used", &Self::fmt_bytes(c.bytes as u64)),
|
||||
(
|
||||
"total",
|
||||
&Self::fmt_bytes(MAX_CONNECTION_MESSAGE_BYTES as u64),
|
||||
),
|
||||
]
|
||||
)),
|
||||
),
|
||||
),
|
||||
@@ -310,7 +315,7 @@ impl Render for ResourceMonitorDialog {
|
||||
cx.theme().primary,
|
||||
cx.theme().primary_foreground,
|
||||
))
|
||||
.child(div().text_base().font_semibold().child(i18n::t("资源监控")))
|
||||
.child(div().text_base().font_semibold().child(i18n::t("titlebar.resmon")))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
@@ -319,7 +324,7 @@ impl Render for ResourceMonitorDialog {
|
||||
.rounded_full()
|
||||
.bg(cx.theme().muted)
|
||||
.text_color(muted)
|
||||
.child(i18n::tf("运行 {t}", &[("t", &uptime_text)])),
|
||||
.child(i18n::tf("resmon.uptime", &[("t", &uptime_text)])),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -334,7 +339,7 @@ impl Render for ResourceMonitorDialog {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(muted)
|
||||
.child(i18n::t("自动刷新")),
|
||||
.child(i18n::t("resmon.auto_refresh")),
|
||||
)
|
||||
.child(
|
||||
Switch::new("resmon-auto")
|
||||
@@ -348,7 +353,7 @@ impl Render for ResourceMonitorDialog {
|
||||
.child(
|
||||
Button::new("resmon-refresh")
|
||||
.icon(IconName::RefreshCw)
|
||||
.label(i18n::t("立即刷新"))
|
||||
.label(i18n::t("resmon.refresh_now"))
|
||||
.outline()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
@@ -367,16 +372,16 @@ impl Render for ResourceMonitorDialog {
|
||||
.child(self.tile(
|
||||
"tile-cpu",
|
||||
IconName::Cpu,
|
||||
i18n::t("CPU 占用"),
|
||||
i18n::t("resmon.cpu"),
|
||||
format!("{cpu:.1}%"),
|
||||
Some(i18n::t("按可用核数归一").into()),
|
||||
Some(i18n::t("resmon.cpu_hint").into()),
|
||||
Some(cpu),
|
||||
cx,
|
||||
))
|
||||
.child(self.tile(
|
||||
"tile-ws",
|
||||
IconName::MemoryStick,
|
||||
i18n::t("工作集内存"),
|
||||
i18n::t("resmon.working_set"),
|
||||
Self::fmt_bytes(ws),
|
||||
Some(i18n::tf(
|
||||
"峰值 {v}",
|
||||
@@ -388,18 +393,18 @@ impl Render for ResourceMonitorDialog {
|
||||
.child(self.tile(
|
||||
"tile-private",
|
||||
IconName::HardDrive,
|
||||
i18n::t("提交内存"),
|
||||
i18n::t("resmon.committed"),
|
||||
Self::fmt_bytes(s.private_bytes),
|
||||
Some(i18n::t("进程独占的物理页承诺").into()),
|
||||
Some(i18n::t("resmon.committed_hint").into()),
|
||||
None,
|
||||
cx,
|
||||
))
|
||||
.child(self.tile(
|
||||
"tile-threads",
|
||||
IconName::Layers,
|
||||
i18n::t("线程 · 句柄"),
|
||||
i18n::t("resmon.threads_handles"),
|
||||
format!("{} / {}", s.threads, s.handles),
|
||||
Some(i18n::t("tokio 引擎 + GPUI 渲染").into()),
|
||||
Some(i18n::t("resmon.threads_hint").into()),
|
||||
None,
|
||||
cx,
|
||||
));
|
||||
@@ -416,7 +421,7 @@ impl Render for ResourceMonitorDialog {
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(Icon::new(IconName::Info).size_4().text_color(muted))
|
||||
.child(div().text_sm().text_color(muted).child(i18n::t("暂无连接"))),
|
||||
.child(div().text_sm().text_color(muted).child(i18n::t("resmon.no_conn"))),
|
||||
);
|
||||
}
|
||||
for (i, c) in self.conns.iter().enumerate() {
|
||||
@@ -431,19 +436,21 @@ impl Render for ResourceMonitorDialog {
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(self.section_header(IconName::Database, i18n::t("消息与连接"), cx))
|
||||
.child(self.section_header(IconName::Database, i18n::t("resmon.section"), cx))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(muted)
|
||||
.child(format!(
|
||||
"{} 个连接(已连接 {})· 打开标签 {} · 内存消息 {} 条(每连接上限 {} 条)· 日志 {} 条",
|
||||
self.conns.len(),
|
||||
connected_count,
|
||||
self.tabs,
|
||||
self.total_messages,
|
||||
self.app.read(cx).settings.max_messages,
|
||||
self.logs
|
||||
.child(i18n::tf(
|
||||
"{n} 个连接(已连接 {m})· 打开标签 {t} · 内存消息 {msgs} 条(每连接上限 {cap} 条)· 日志 {logs} 条",
|
||||
&[
|
||||
("n", &self.conns.len()),
|
||||
("m", &connected_count),
|
||||
("t", &self.tabs),
|
||||
("msgs", &self.total_messages),
|
||||
("cap", &self.app.read(cx).settings.max_messages),
|
||||
("logs", &self.logs),
|
||||
]
|
||||
)),
|
||||
)
|
||||
.child(conns_block),
|
||||
@@ -458,14 +465,14 @@ impl Render for ResourceMonitorDialog {
|
||||
.text_xs()
|
||||
.text_color(muted)
|
||||
.child(if self.auto_refresh {
|
||||
format!(
|
||||
"版本 v{} · 每 1.5 秒自动刷新",
|
||||
crate::update::current_version()
|
||||
i18n::tf(
|
||||
"版本 v{v} · 每 1.5 秒自动刷新",
|
||||
&[("v", &crate::update::current_version())],
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"版本 v{} · 自动刷新已暂停",
|
||||
crate::update::current_version()
|
||||
i18n::tf(
|
||||
"版本 v{v} · 自动刷新已暂停",
|
||||
&[("v", &crate::update::current_version())],
|
||||
)
|
||||
}),
|
||||
),
|
||||
@@ -506,7 +513,7 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
||||
window.open_dialog(cx, move |dialog, _, _| {
|
||||
dialog
|
||||
.w(px(640.))
|
||||
.title(i18n::t("资源监控"))
|
||||
.title(i18n::t("titlebar.resmon"))
|
||||
.child(dialog_view.clone())
|
||||
.footer(
|
||||
h_flex()
|
||||
@@ -514,7 +521,7 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("res-close")
|
||||
.label(i18n::t("关闭"))
|
||||
.label(i18n::t("common.close"))
|
||||
.outline()
|
||||
.on_click(|_, window, cx| window.close_dialog(cx)),
|
||||
),
|
||||
|
||||
@@ -26,10 +26,10 @@ use crate::ui::i18n;
|
||||
use crate::update::{self, UpdateInfo};
|
||||
|
||||
fn theme_labels() -> [&'static str; 3] {
|
||||
[i18n::t("跟随系统"), i18n::t("浅色"), i18n::t("深色")]
|
||||
[i18n::t("theme.system"), i18n::t("theme.light"), i18n::t("theme.dark")]
|
||||
}
|
||||
fn language_labels() -> [&'static str; 3] {
|
||||
[i18n::t("跟随系统 / System"), "中文", "English"]
|
||||
["跟随系统 / System", "中文", "English"]
|
||||
}
|
||||
|
||||
/// 消息缓存条数的合法范围:太少不够回看,太多占内存
|
||||
@@ -179,32 +179,32 @@ impl SettingsDialog {
|
||||
match &self.update {
|
||||
UpdateUi::Idle => row(
|
||||
Button::new("check-update")
|
||||
.label(i18n::t("检查更新"))
|
||||
.label(i18n::t("update.check"))
|
||||
.outline()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, window, cx| this.run_check(window, cx)))
|
||||
.into_any_element(),
|
||||
i18n::t("检查 GitHub Releases 上的新版本").into_any_element(),
|
||||
i18n::t("update.check_hint").into_any_element(),
|
||||
false,
|
||||
),
|
||||
UpdateUi::Checking => row(
|
||||
Button::new("check-update")
|
||||
.label(i18n::t("检查中…"))
|
||||
.label(i18n::t("update.checking"))
|
||||
.outline()
|
||||
.xsmall()
|
||||
.loading(true)
|
||||
.into_any_element(),
|
||||
i18n::t("正在检查更新…").into_any_element(),
|
||||
i18n::t("update.checking_long").into_any_element(),
|
||||
false,
|
||||
),
|
||||
UpdateUi::UpToDate => row(
|
||||
Button::new("check-update")
|
||||
.label(i18n::t("重新检查"))
|
||||
.label(i18n::t("update.recheck"))
|
||||
.outline()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, window, cx| this.run_check(window, cx)))
|
||||
.into_any_element(),
|
||||
i18n::t("已是最新版本").into_any_element(),
|
||||
i18n::t("update.up_to_date").into_any_element(),
|
||||
false,
|
||||
),
|
||||
UpdateUi::Available(info) => {
|
||||
@@ -249,7 +249,7 @@ impl SettingsDialog {
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Button::new("upd-now")
|
||||
.label(i18n::t("立即安装"))
|
||||
.label(i18n::t("update.install_now"))
|
||||
.primary()
|
||||
.xsmall()
|
||||
.on_click(move |_, window, cx| {
|
||||
@@ -267,7 +267,7 @@ impl SettingsDialog {
|
||||
)
|
||||
.child(
|
||||
Button::new("upd-later")
|
||||
.label(i18n::t("下次启动安装"))
|
||||
.label(i18n::t("update.install_later"))
|
||||
.outline()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -288,7 +288,7 @@ impl SettingsDialog {
|
||||
)
|
||||
.child(
|
||||
Button::new("upd-discard")
|
||||
.label(i18n::t("放弃此次更新"))
|
||||
.label(i18n::t("update.discard"))
|
||||
.outline()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -297,7 +297,7 @@ impl SettingsDialog {
|
||||
}
|
||||
this.update = UpdateUi::Idle;
|
||||
window.push_notification(
|
||||
Notification::info(i18n::t("已删除暂存的更新包")),
|
||||
Notification::info(i18n::t("update.discarded")),
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
@@ -341,12 +341,12 @@ impl SettingsDialog {
|
||||
}
|
||||
UpdateUi::Failed(e) => row(
|
||||
Button::new("check-update")
|
||||
.label(i18n::t("重试"))
|
||||
.label(i18n::t("common.retry"))
|
||||
.outline()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, window, cx| this.run_check(window, cx)))
|
||||
.into_any_element(),
|
||||
i18n::tf("更新失败:{e}", &[("e", &e)]).into_any_element(),
|
||||
i18n::tf("update.failed", &[("e", &e)]).into_any_element(),
|
||||
false,
|
||||
),
|
||||
}
|
||||
@@ -401,7 +401,7 @@ fn open_in_file_manager(path: &Path, window: &mut Window, cx: &mut App) {
|
||||
|
||||
cmd.arg(path);
|
||||
if let Err(e) = cmd.spawn() {
|
||||
window.push_notification(Notification::error(i18n::tf("打开目录失败: {e}", &[("e", &e.to_string())])), cx);
|
||||
window.push_notification(Notification::error(i18n::tf("settings.open_dir_failed", &[("e", &e)])), cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,10 +415,10 @@ impl Render for SettingsDialog {
|
||||
v_flex()
|
||||
.gap_4()
|
||||
.w(px(500.))
|
||||
.child(field(i18n::t("界面主题"), Select::new(&self.theme)))
|
||||
.child(field(i18n::t("界面语言"), Select::new(&self.language)))
|
||||
.child(field(i18n::t("settings.theme"), Select::new(&self.theme)))
|
||||
.child(field(i18n::t("settings.language"), Select::new(&self.language)))
|
||||
.child(field(
|
||||
i18n::t("每条连接内存中保留的消息条数(100~100000)"),
|
||||
i18n::t("settings.max_messages"),
|
||||
Input::new(&self.max_messages),
|
||||
))
|
||||
.child(
|
||||
@@ -435,7 +435,7 @@ impl Render for SettingsDialog {
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(div().text_sm().child(i18n::t("时间戳显示毫秒")))
|
||||
.child(div().text_sm().child(i18n::t("settings.show_millis")))
|
||||
.child(
|
||||
Switch::new("show-millis")
|
||||
.checked(self.show_millis)
|
||||
@@ -448,7 +448,7 @@ impl Render for SettingsDialog {
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(div().text_sm().child(i18n::t("自动检查更新(启动时)")))
|
||||
.child(div().text_sm().child(i18n::t("settings.auto_update")))
|
||||
.child(
|
||||
Switch::new("auto-check-update")
|
||||
.checked(self.auto_check_update)
|
||||
@@ -460,7 +460,7 @@ impl Render for SettingsDialog {
|
||||
)
|
||||
.child(div().h(px(1.)).bg(cx.theme().border).w_full())
|
||||
.child(field(
|
||||
i18n::t("数据目录"),
|
||||
i18n::t("settings.data_dir"),
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
@@ -479,7 +479,7 @@ impl Render for SettingsDialog {
|
||||
.child(
|
||||
Button::new("open-data-dir")
|
||||
.icon(IconName::FolderOpen)
|
||||
.label(i18n::t("打开目录"))
|
||||
.label(i18n::t("settings.open_dir"))
|
||||
.outline()
|
||||
.xsmall()
|
||||
.on_click(move |_, window, cx| {
|
||||
@@ -488,7 +488,7 @@ impl Render for SettingsDialog {
|
||||
),
|
||||
))
|
||||
.child(field(
|
||||
i18n::t("日志目录"),
|
||||
i18n::t("settings.log_dir"),
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
@@ -506,14 +506,14 @@ impl Render for SettingsDialog {
|
||||
.child(
|
||||
Button::new("open-log-dir")
|
||||
.icon(IconName::FolderOpen)
|
||||
.label(i18n::t("打开日志目录"))
|
||||
.label(i18n::t("settings.open_log_dir"))
|
||||
.outline()
|
||||
.xsmall()
|
||||
.on_click(move |_, window, cx| {
|
||||
// 日志目录可能尚未创建(从未产生过日志)
|
||||
if let Err(e) = std::fs::create_dir_all(&log_dir_for_open) {
|
||||
window.push_notification(
|
||||
Notification::error(i18n::tf("创建日志目录失败: {e}", &[("e", &e.to_string())])),
|
||||
Notification::error(i18n::tf("settings.mkdir_failed", &[("e", &e)])),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
@@ -527,7 +527,7 @@ impl Render for SettingsDialog {
|
||||
v_flex().gap_1().child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(div().text_sm().font_semibold().child(i18n::t("关于")))
|
||||
.child(div().text_sm().font_semibold().child(i18n::t("settings.about")))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
@@ -536,18 +536,18 @@ impl Render for SettingsDialog {
|
||||
.child(i18n::tf(
|
||||
"版本 {v}(更新源 {src})",
|
||||
&[
|
||||
("v", &update::current_version().to_string()),
|
||||
("src", update::UPDATE_SOURCE),
|
||||
("v", &update::current_version()),
|
||||
("src", &update::UPDATE_SOURCE),
|
||||
],
|
||||
))
|
||||
.child(i18n::t("Apache-2.0 许可证")),
|
||||
.child(i18n::t("settings.license")),
|
||||
)
|
||||
.child(self.render_update_row(window, cx))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(i18n::t("对标 MQTTX 的 GPUI 跨平台 MQTT 调试客户端")),
|
||||
.child(i18n::t("settings.tagline")),
|
||||
)
|
||||
.child(
|
||||
h_flex().gap_1().child(
|
||||
@@ -605,13 +605,13 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("settings-cancel")
|
||||
.label(i18n::t("取消"))
|
||||
.label(i18n::t("common.cancel"))
|
||||
.outline()
|
||||
.on_click(|_, window, cx| window.close_dialog(cx)),
|
||||
)
|
||||
.child(
|
||||
Button::new("settings-ok")
|
||||
.label(i18n::t("保存"))
|
||||
.label(i18n::t("common.save"))
|
||||
.primary()
|
||||
.on_click(move |_, window, cx| {
|
||||
let result = for_collect.read(cx).collect(cx);
|
||||
|
||||
@@ -33,7 +33,7 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
||||
let app_ok = app.clone();
|
||||
dialog
|
||||
.w(px(560.))
|
||||
.title(i18n::t("全局变量"))
|
||||
.title(i18n::t("titlebar.variables"))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
@@ -41,8 +41,8 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
||||
v_flex()
|
||||
.text_sm()
|
||||
.gap_1()
|
||||
.child(i18n::t("在发布主题、消息负载中使用 {{变量名}} 引用。"))
|
||||
.child(i18n::t("内置变量:{{$ts}}(秒)、{{$ts_ms}}(毫秒)、{{$uuid}}")),
|
||||
.child(i18n::t("var.dialog_hint"))
|
||||
.child(i18n::t("var.dialog_builtin")),
|
||||
)
|
||||
.child(editor_body),
|
||||
)
|
||||
@@ -53,13 +53,13 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("vars-cancel")
|
||||
.label(i18n::t("取消"))
|
||||
.label(i18n::t("common.cancel"))
|
||||
.outline()
|
||||
.on_click(|_, window, cx| window.close_dialog(cx)),
|
||||
)
|
||||
.child(
|
||||
Button::new("vars-ok")
|
||||
.label(i18n::t("保存"))
|
||||
.label(i18n::t("common.save"))
|
||||
.primary()
|
||||
.on_click(move |_, window, cx| {
|
||||
let vars = for_ok
|
||||
|
||||
+261
-376
@@ -1,12 +1,29 @@
|
||||
//! 极简 i18n:以中文原文为 key 的双语文案表。
|
||||
//! i18n 门面:语义键(`ns.key`)+ rust-i18n(gpui-kit 官方方案)。
|
||||
//!
|
||||
//! 设计取舍:
|
||||
//! - 中文是基准语言,`t("订阅")` 直接返回原文,代码里不引入间接 key,
|
||||
//! 漏翻时回落中文而不是出现裸 key;
|
||||
//! - 英文表集中在本文件一个 `match`,新增文案先写中文、补翻英文即可;
|
||||
//! - 带参数的文案用 [`tf`],占位符 `{name}` 在中英模式中同名替换;
|
||||
//! - 语言是进程级全局(UI 渲染热路径每帧取用,用原子量免锁)。
|
||||
//! # 架构
|
||||
//! - 语言包在 `locales/ui.yml`(en / zh-CN 双语并排,`_version: 2`),
|
||||
//! 由 crate 根的 `rust_i18n::i18n!("locales", fallback = "en")` 编译期内嵌;
|
||||
//! gpui-kit 组件内置文案经 main.rs 的 `rust_i18n::extend!` 随语言切换。
|
||||
//! - 调用点写 `t("common.save")` / `tf("msg.count", &[("n", &count)])`,
|
||||
//! key 是**稳定标识符**,与任何自然语言解耦——改文案措辞不动代码,
|
||||
//! 也不存在「一词多义撞 key」。
|
||||
//! - [`t`]/[`tf`] 直接查 [`crate::_rust_i18n_translate`](rust-i18n 生成、
|
||||
//! 接受运行时 key 的公开函数),不经 `t!` 宏——`label()` 这类运行时
|
||||
//! 值同样可查。
|
||||
//!
|
||||
//! # 规则(单元测试强制,违反即测试失败)
|
||||
//! 1. 新增文案:调用点 `t("ns.key")`,`locales/ui.yml` 加 key 块(en/zh-CN 必填)。
|
||||
//! 2. 源码每个 `t("…")` / `tf("…")` 字面量必须在语言包中存在。
|
||||
//! 3. 模型 `label()` 返回的 key 必须有**非自映射**的 en 词条。
|
||||
//! 4. 同一 key 的 zh-CN 与 en 占位符 `{name}` 集合必须一致
|
||||
//! (`tf` 按名替换,两侧不一致即静默漏参)。
|
||||
//!
|
||||
//! # 已知限制
|
||||
//! 语言切换对**已打开页签里的下拉框**(QoS / Retain Handling 等)不即时生效——
|
||||
//! 这些 SelectState 在构造时固化了标签列表;重开页签即刷新。其余界面即时生效
|
||||
//! (save_settings 内 `window.refresh()` 兜底整窗重绘)。
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -15,11 +32,20 @@ pub enum Lang {
|
||||
En,
|
||||
}
|
||||
|
||||
/// 0 = 中文,1 = English。默认中文(基准语言)。
|
||||
const LOCALE_ZH: &str = "zh-CN";
|
||||
const LOCALE_EN: &str = "en";
|
||||
|
||||
/// 0 = 中文,1 = English。默认中文。
|
||||
static LANG: AtomicU8 = AtomicU8::new(0);
|
||||
|
||||
/// 切换语言:同时更新 rust-i18n 全局 locale(gpui-kit 组件内置文案随之切换)。
|
||||
/// 已渲染界面的刷新由调用方负责(settings 保存路径带整窗 refresh)。
|
||||
pub fn set_lang(lang: Lang) {
|
||||
LANG.store(lang as u8, Ordering::Relaxed);
|
||||
gpui_kit::component::set_locale(match lang {
|
||||
Lang::Zh => LOCALE_ZH,
|
||||
Lang::En => LOCALE_EN,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn lang() -> Lang {
|
||||
@@ -38,394 +64,253 @@ pub fn from_system() -> Lang {
|
||||
}
|
||||
}
|
||||
|
||||
/// 取当前语言的界面文案。中文原文即 key,英文缺失时回退中文。
|
||||
pub fn t(zh: &'static str) -> &'static str {
|
||||
if lang() == Lang::Zh {
|
||||
return zh;
|
||||
}
|
||||
en(zh)
|
||||
/// 查指定 locale 的词条。
|
||||
fn translate<'k>(locale: &str, key: &'k str) -> Cow<'k, str> {
|
||||
crate::_rust_i18n_translate(locale, key)
|
||||
}
|
||||
|
||||
/// 带参数的界面文案:中英模式均为 `{name}` 占位符,按同名替换。
|
||||
pub fn tf(zh: &'static str, args: &[(&str, &str)]) -> String {
|
||||
let pattern = if lang() == Lang::Zh {
|
||||
zh.to_string()
|
||||
} else {
|
||||
en(zh).to_string()
|
||||
/// 取当前语言的界面文案。miss 时 rust-i18n 原样返回 key(测试会拦截,
|
||||
/// 运行期裸 key 只是最后的安全网)。
|
||||
pub fn t(key: &'static str) -> &'static str {
|
||||
let locale = match lang() {
|
||||
Lang::Zh => LOCALE_ZH,
|
||||
Lang::En => LOCALE_EN,
|
||||
};
|
||||
let mut out = pattern;
|
||||
match translate(locale, key) {
|
||||
// 无参查表只返回 Borrowed(静态表值或原 key);Owned 视为异常回退 key
|
||||
Cow::Borrowed(s) => s,
|
||||
Cow::Owned(_) => key,
|
||||
}
|
||||
}
|
||||
|
||||
/// 带参数的界面文案:语言包内占位符写作 `{name}`(非 rust-i18n 的 `%{name}`,
|
||||
/// 由本函数按名替换),参数值只需 `Display`。
|
||||
pub fn tf(key: &'static str, args: &[(&str, &dyn std::fmt::Display)]) -> String {
|
||||
let mut out = t(key).to_string();
|
||||
for (name, value) in args {
|
||||
out = out.replace(&format!("{{{name}}}"), value);
|
||||
out = out.replace(&format!("{{{name}}}"), &value.to_string());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// 英文文案表。key = 中文原文(与调用处字面量一一对应)。
|
||||
fn en(zh: &'static str) -> &'static str {
|
||||
match zh {
|
||||
// ── 通用 ──
|
||||
"保存" => "Save",
|
||||
"取消" => "Cancel",
|
||||
"删除" => "Delete",
|
||||
"确定" => "OK",
|
||||
"全部" => "All",
|
||||
"清空" => "Clear",
|
||||
"发送" => "Send",
|
||||
"连接" => "Connect",
|
||||
"断开" => "Disconnect",
|
||||
"订阅" => "Subscriptions",
|
||||
"编辑" => "Edit",
|
||||
"高级" => "Advanced",
|
||||
"错误" => "Error",
|
||||
"未连接" => "Disconnected",
|
||||
"连接中" => "Connecting",
|
||||
"连接中…" => "Connecting…",
|
||||
"已连接" => "Connected",
|
||||
"暂停" => "Pause",
|
||||
"恢复" => "Resume",
|
||||
"恢复实时滚动" => "Resume live scrolling",
|
||||
"暂停消息流滚动(消息仍在后台接收)" => {
|
||||
"Pause message stream (messages keep arriving in background)"
|
||||
}
|
||||
"MQTT 调试客户端" => "MQTT debug client",
|
||||
"新建连接" => "New Connection",
|
||||
// ─── 单元测试:语言包完整性由测试强制,而不是靠人工纪律 ──────────────────────
|
||||
|
||||
// ── 标题栏 ──
|
||||
"置顶窗口" => "Pin window on top",
|
||||
"取消窗口置顶" => "Unpin window",
|
||||
"置顶操作失败" => "Failed to set always-on-top",
|
||||
"切换为浅色" => "Switch to light theme",
|
||||
"切换为深色" => "Switch to dark theme",
|
||||
"阿里云设备" => "Alibaba Cloud Devices",
|
||||
"全局变量" => "Global Variables",
|
||||
"资源监控" => "Resource Monitor",
|
||||
"导出连接" => "Export Connections",
|
||||
"导入连接" => "Import Connections",
|
||||
"设置" => "Settings",
|
||||
/// 提取 `{name}` 形式的占位符(ASCII 标识符;`{{x}}` 双括号、`{$builtin}`
|
||||
/// 与中文花括号说明文不算),排序后比较集合。
|
||||
#[cfg(test)]
|
||||
fn placeholders(s: &str) -> Vec<String> {
|
||||
let b = s.as_bytes();
|
||||
let mut out = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < b.len() {
|
||||
if b[i] == b'{' {
|
||||
let before_ok = i == 0 || b[i - 1] != b'{';
|
||||
let mut j = i + 1;
|
||||
while j < b.len() && (b[j].is_ascii_alphanumeric() || b[j] == b'_') {
|
||||
j += 1;
|
||||
}
|
||||
let after_ok = j < b.len() && b[j] == b'}' && (j + 1 == b.len() || b[j + 1] != b'}');
|
||||
let name = &s[i + 1..j];
|
||||
let name_ok = name
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
|
||||
if before_ok && after_ok && name_ok {
|
||||
out.push(name.to_string());
|
||||
i = j + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
// ── 侧边栏 ──
|
||||
"搜索连接…" => "Search connections…",
|
||||
"还没有连接,点击上方 + 新建" => "No connections yet. Click + above to create one",
|
||||
"没有匹配的连接" => "No matching connections",
|
||||
"收起侧栏(Ctrl+B)" => "Collapse sidebar (Ctrl+B)",
|
||||
"展开连接侧栏(Ctrl+B)" => "Expand sidebar (Ctrl+B)",
|
||||
"复制连接" => "Duplicate",
|
||||
"删除连接" => "Delete Connection",
|
||||
"未分组" => "Ungrouped",
|
||||
/// 解析生成器产出的规整 YAML(key 行 + 缩进的 en/zh-CN 值行)。
|
||||
/// 格式意外时 panic——语言包是构建产物,格式受控。
|
||||
#[cfg(test)]
|
||||
fn load_ui_yml() -> std::collections::BTreeMap<String, (String, String)> {
|
||||
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("\\\\", "\\")
|
||||
};
|
||||
let mut map = std::collections::BTreeMap::new();
|
||||
let mut key: Option<String> = None;
|
||||
let mut en: Option<String> = None;
|
||||
let mut zh: Option<String> = None;
|
||||
for line in text.lines() {
|
||||
if line.starts_with('#') || line.starts_with("_version") || line.trim().is_empty() {
|
||||
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));
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
// ── 主区 / 标签页 ──
|
||||
"开始使用 MQTTX" => "Get started with MQTTX",
|
||||
"从左侧选择一个连接,或新建连接开始调试" => {
|
||||
"Pick a connection on the left, or create a new one to start debugging"
|
||||
}
|
||||
"消息流" => "Messages",
|
||||
"日志" => "Logs",
|
||||
"关闭标签页(Ctrl+W)" => "Close tab (Ctrl+W)",
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ── 连接工作区 ──
|
||||
"收起侧栏" => "Collapse sidebar",
|
||||
"收起订阅面板" => "Collapse subscriptions panel",
|
||||
"展开订阅面板" => "Expand subscriptions panel",
|
||||
"订阅主题后,消息会显示在右侧" => "Subscribe to a topic and messages will show up here",
|
||||
"订阅颜色" => "Subscription color",
|
||||
"随机颜色" => "Random color",
|
||||
"清除颜色" => "Clear color",
|
||||
"停用订阅" => "Disable subscription",
|
||||
"启用订阅" => "Enable subscription",
|
||||
"编辑订阅" => "Edit subscription",
|
||||
"取消订阅" => "Unsubscribe",
|
||||
"订阅主题,支持 # +,多个用逗号/空格分隔" => {
|
||||
"Topic filter, supports # +; separate multiple with commas/spaces"
|
||||
/// 每个 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:?}");
|
||||
assert_eq!(
|
||||
placeholders(zh),
|
||||
placeholders(en),
|
||||
"占位符两侧不一致: {k:?}"
|
||||
);
|
||||
}
|
||||
"别名(可选)" => "Alias (optional)",
|
||||
"订阅标识符" => "Subscription Identifier",
|
||||
"No Local" => "No Local",
|
||||
"Retain As Published" => "Retain As Published",
|
||||
"Retain Handling" => "Retain Handling",
|
||||
"0 每次发送" => "0 Send at every subscribe",
|
||||
"1 仅新订阅" => "1 Send only for new subscription",
|
||||
"2 不发送" => "2 Do not send",
|
||||
"正在编辑:{topic}" => "Editing: {topic}",
|
||||
"订阅主题不能为空" => "Topic filter cannot be empty",
|
||||
"订阅标识符须为正整数" => "Subscription identifier must be a positive integer",
|
||||
"未连接:订阅已保存,连接后自动恢复" => {
|
||||
"Offline: subscription saved, restored on connect"
|
||||
}
|
||||
"未连接:订阅已保存" => "Offline: subscription saved",
|
||||
"QoS 0" => "QoS 0",
|
||||
"QoS 1" => "QoS 1",
|
||||
"QoS 2" => "QoS 2",
|
||||
"更新" => "Update",
|
||||
}
|
||||
|
||||
// ── 消息流 ──
|
||||
"过滤主题或内容…" => "Filter by topic or content…",
|
||||
"{n} 条" => "{n} msgs",
|
||||
"仅显示最新 {n} 条" => "Showing latest {n} only",
|
||||
"清空当前连接的消息" => "Clear messages of this connection",
|
||||
"清空消息" => "Clear Messages",
|
||||
"确定清空当前连接的全部消息吗?此操作不可撤销。" => {
|
||||
"Clear all messages of this connection? This cannot be undone."
|
||||
/// 扫描 src/ 全部源码里的 `t("…")` / `tf("…")` 字面量,
|
||||
/// 逐一断言语言包有词条——漏翻是测试失败,不再是静默回退。
|
||||
#[test]
|
||||
fn call_sites_covered_by_yml() {
|
||||
let map = load_ui_yml();
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let mut files = Vec::new();
|
||||
collect_rs(&root, &mut files);
|
||||
assert!(!files.is_empty(), "应扫描到源码文件");
|
||||
let mut missing = Vec::new();
|
||||
for f in files {
|
||||
if f.ends_with("i18n.rs") {
|
||||
continue;
|
||||
}
|
||||
let src = std::fs::read_to_string(&f).unwrap();
|
||||
for key in extract_t_literals(&src) {
|
||||
if !map.contains_key(&key) {
|
||||
missing.push(format!("{}: {key:?}", f.display()));
|
||||
}
|
||||
}
|
||||
}
|
||||
"接收" => "Received",
|
||||
"发布" => "Published",
|
||||
"没有匹配的消息" => "No matching messages",
|
||||
"暂无消息,订阅主题后消息会显示在这里" => {
|
||||
"No messages yet — they will appear here once you subscribe"
|
||||
}
|
||||
"retain" => "retain",
|
||||
"(非文本,详情可切 Hex)" => " (binary — switch to Hex in details)",
|
||||
"用户属性" => "User Properties",
|
||||
"Content-Type" => "Content-Type",
|
||||
"Response Topic" => "Response Topic",
|
||||
"Correlation Data" => "Correlation Data",
|
||||
"消息过期" => "Message Expiry",
|
||||
"{n} 秒" => "{n}s",
|
||||
"复制主题" => "Copy topic",
|
||||
"复制负载原文" => "Copy raw payload",
|
||||
"复制完整详情(含 v5 属性)" => "Copy full details (incl. MQTT 5 props)",
|
||||
"已复制主题" => "Topic copied",
|
||||
"已复制负载" => "Payload copied",
|
||||
"已复制详情" => "Details copied",
|
||||
"主题" => "Topic",
|
||||
"界面主题" => "Theme",
|
||||
"负载" => "Payload",
|
||||
"详情" => "Details",
|
||||
"自动" => "Auto",
|
||||
"文本" => "Text",
|
||||
"连接日志" => "Connection Logs",
|
||||
"暂无日志" => "No logs yet",
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"以下 t()/tf() 字面量没有语言包词条:\n{}",
|
||||
missing.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
// ── 发布面板 ──
|
||||
"发布主题" => "Publish topic",
|
||||
"输入消息负载,支持 {{变量名}} 与 {{$ts}} {{$uuid}}" => {
|
||||
"Message payload; supports {{variables}} and {{$ts}} {{$uuid}}"
|
||||
}
|
||||
"消息预设" => "Message Presets",
|
||||
"保存当前为预设…" => "Save current as preset…",
|
||||
"管理预设…" => "Manage presets…",
|
||||
"删除预设" => "Delete Preset",
|
||||
"确定删除预设「{name}」吗?此操作不可撤销。" => {
|
||||
"Delete preset \"{name}\"? This cannot be undone."
|
||||
}
|
||||
"预设已保存" => "Preset saved",
|
||||
"已覆盖预设「{name}」" => "Preset \"{name}\" overwritten",
|
||||
"请填写预设名称" => "Preset name is required",
|
||||
"预设名称,如 温度上报" => "Preset name, e.g. temperature-report",
|
||||
"保存消息预设" => "Save Message Preset",
|
||||
"预设名称" => "Preset name",
|
||||
"变量注入" => "Variable injection",
|
||||
"变量注入({n} 个占位符)" => "Variable injection ({n} placeholders)",
|
||||
"MQTT 5 属性" => "MQTT 5 Properties",
|
||||
"MQTT 5 订阅选项" => "MQTT 5 Subscription Options",
|
||||
"未连接:消息未发送" => "Offline: message not sent",
|
||||
"发布主题不能为空" => "Publish topic cannot be empty",
|
||||
"消息过期须为非负整数(秒)" => "Message expiry must be a non-negative integer (seconds)",
|
||||
"JSON 格式无效: {e}" => "Invalid JSON: {e}",
|
||||
"订阅失败: {e}" => "Subscribe failed: {e}",
|
||||
"Keep Alive" => "Keep Alive",
|
||||
"接收上限" => "Receive Maximum",
|
||||
"主题别名" => "Topic Alias",
|
||||
"需要是 0~65535 的整数" => "must be an integer 0-65535",
|
||||
"必须大于 0(0 是 MQTT 5 协议错误)" => "must be > 0 (0 is an MQTT 5 protocol error)",
|
||||
"需要是非负整数" => "must be a non-negative integer",
|
||||
"已启用" => "Enabled",
|
||||
"添加属性" => "Add property",
|
||||
"删除此属性" => "Remove this property",
|
||||
"收起" => "Collapse",
|
||||
"变量值" => "Value",
|
||||
"主题/负载中没有 {{变量}} 引用" => "No {{variable}} reference in topic/payload",
|
||||
"重新提取占位符" => "Re-extract placeholders",
|
||||
"内置:{{$ts}}(秒) {{$ts_ms}}(毫秒) {{$uuid}} · 发布时注入" => "Built-in: {{$ts}} (s), {{$ts_ms}} (ms), {{$uuid}} · injected on publish",
|
||||
"预览:{tp} | {pl}" => "Preview: {tp} | {pl}",
|
||||
"变量({n})" => "Variables ({n})",
|
||||
"清除订阅过滤" => "Clear subscription filter",
|
||||
"取消编辑" => "Cancel editing",
|
||||
"鉴权方式" => "Auth mode",
|
||||
"地域" => "Region",
|
||||
"实例 ID" => "Instance ID",
|
||||
"已保存预设" => "Saved presets",
|
||||
"保存预设" => "Save preset",
|
||||
"生成并连接" => "Generate & connect",
|
||||
"无法生成连接: {e}" => "Failed to build connection: {e}",
|
||||
"分组(可选)" => "Group (optional)",
|
||||
"如 cn-shanghai(可留空)" => "e.g. cn-shanghai (optional)",
|
||||
"设备 ID" => "Device ID",
|
||||
"一机一密:ProductKey" => "Triple: ProductKey",
|
||||
"一机一密:DeviceSecret" => "Triple: DeviceSecret",
|
||||
"阿里云 IoT 设备" => "Alibaba Cloud IoT",
|
||||
"按可用核数归一" => "Normalized by core count",
|
||||
"峰值 {v}" => "Peak {v}",
|
||||
"进程独占的物理页承诺" => "Physical pages committed by the process",
|
||||
"tokio 引擎 + GPUI 渲染" => "tokio engine + GPUI renderer",
|
||||
"暂无连接" => "No connections",
|
||||
"消息与连接" => "Messages & Connections",
|
||||
"{n} 条消息" => "{n} msgs",
|
||||
"运行 {t}" => "Up {t}",
|
||||
"立即刷新" => "Refresh now",
|
||||
"发布主题,支持 {{变量名}}" => "Publish topic; supports {{variables}}",
|
||||
"消息负载,支持 {{变量名}}" => "Message payload; supports {{variables}}",
|
||||
"Payload(原文保存,发送时渲染变量)" => "Payload (stored raw; variables rendered on send)",
|
||||
"Payload 格式" => "Payload format",
|
||||
"暂无发布预设,点击上方「新建」创建" => "No presets yet — click \"New\" above to create one",
|
||||
"请先选择或新建预设" => "Select or create a preset first",
|
||||
"请先选择要删除的预设" => "Select a preset to delete first",
|
||||
"预设「{name}」已保存" => "Preset \"{name}\" saved",
|
||||
"预设名称「{name}」已存在" => "Preset name \"{name}\" already exists",
|
||||
"新预设 {n}" => "New preset {n}",
|
||||
"新预设" => "New preset",
|
||||
"管理发布预设" => "Manage Publish Presets",
|
||||
"在发布主题、消息负载中使用 {{变量名}} 引用。" => "Reference {{variables}} in publish topics and payloads.",
|
||||
"内置变量:{{$ts}}(秒)、{{$ts_ms}}(毫秒)、{{$uuid}}" => "Built-in: {{$ts}} (s), {{$ts_ms}} (ms), {{$uuid}}",
|
||||
"测试任务丢失" => "Test task lost",
|
||||
"Hex 长度必须为偶数(每两个字符一个字节)" => "Hex length must be even (two chars per byte)",
|
||||
"Hex 解码失败: 含非十六进制字符" => "Hex decode failed: non-hex character present",
|
||||
"Hex 解码失败: {e}" => "Hex decode failed: {e}",
|
||||
"Base64 解码失败: {e}" => "Base64 decode failed: {e}",
|
||||
"已发送(被当前过滤条件隐藏,消息流中不显示)" => {
|
||||
"Sent (hidden by current filter — not shown in the stream)"
|
||||
}
|
||||
"收起发布面板" => "Collapse publish panel",
|
||||
"展开发布面板" => "Expand publish panel",
|
||||
"发布面板已收起" => "Publish panel collapsed",
|
||||
"Content-Type(可选)" => "Content-Type (optional)",
|
||||
"秒,如 60" => "seconds, e.g. 60",
|
||||
"Response Topic(可选)" => "Response Topic (optional)",
|
||||
"Correlation Data(可选)" => "Correlation Data (optional)",
|
||||
"消息过期(秒)" => "Expiry (s)",
|
||||
/// 模型 `label()` 返回的 key 必须有真实(非自映射)的 en 词条。
|
||||
#[test]
|
||||
fn model_labels_have_real_translations() {
|
||||
let labels = [
|
||||
crate::model::ConnectionStatus::Disconnected.label(),
|
||||
crate::model::ConnectionStatus::Connecting.label(),
|
||||
crate::model::ConnectionStatus::Connected.label(),
|
||||
crate::model::ConnectionStatus::Error.label(),
|
||||
crate::model::Direction::Received.label(),
|
||||
crate::model::Direction::Published.label(),
|
||||
crate::model::ThemeModePref::System.label(),
|
||||
crate::model::ThemeModePref::Light.label(),
|
||||
crate::model::ThemeModePref::Dark.label(),
|
||||
];
|
||||
let missing: Vec<&str> = labels
|
||||
.iter()
|
||||
.filter(|l| translate(LOCALE_EN, l) == Cow::Borrowed(**l))
|
||||
.copied()
|
||||
.collect();
|
||||
assert!(missing.is_empty(), "模型 label 缺 en 词条: {missing:?}");
|
||||
}
|
||||
|
||||
// ── 变量面板 ──
|
||||
"检测到模板变量,发布时注入全局变量值" => {
|
||||
"Template variables detected — global values injected on publish"
|
||||
fn collect_rs(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
|
||||
let Ok(rd) = std::fs::read_dir(dir) else { return };
|
||||
for e in rd.flatten() {
|
||||
let p = e.path();
|
||||
if p.is_dir() {
|
||||
collect_rs(&p, out);
|
||||
} else if p.extension().is_some_and(|x| x == "rs") {
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 连接表单 ──
|
||||
"连接配置" => "Connection Settings",
|
||||
"基本信息" => "Basics",
|
||||
"主机" => "Host",
|
||||
"端口" => "Port",
|
||||
"名称" => "Name",
|
||||
"协议" => "Protocol",
|
||||
"传输" => "Transport",
|
||||
"分组" => "Group",
|
||||
"新建分组" => "New group",
|
||||
"填写后优先于左侧所选分组" => "Overrides the selected group when filled",
|
||||
"Client ID" => "Client ID",
|
||||
"重新生成 Client ID" => "Regenerate Client ID",
|
||||
"WS 路径" => "WS Path",
|
||||
"broker.example.com" => "broker.example.com",
|
||||
"连接名称" => "Connection name",
|
||||
"无分组" => "No group",
|
||||
"认证与心跳" => "Auth & Keep-alive",
|
||||
"用户名" => "Username",
|
||||
"密码" => "Password",
|
||||
"用户名(可选)" => "Username (optional)",
|
||||
"密码(可选)" => "Password (optional)",
|
||||
"Keep Alive (秒)" => "Keep Alive (s)",
|
||||
"Keep Alive (秒) 需要是 0~65535 的整数" => "Keep Alive must be an integer 0-65535",
|
||||
"连接超时 (秒)" => "Connect timeout (s)",
|
||||
"最大重连次数 (0=无限)" => "Max reconnects (0 = unlimited)",
|
||||
"Clean Start / Clean Session" => "Clean Start / Clean Session",
|
||||
"断线自动重连" => "Auto reconnect on disconnect",
|
||||
"连接后自动恢复订阅" => "Auto-resubscribe on connect",
|
||||
"启动时自动连接" => "Connect on app start",
|
||||
"SSL/TLS" => "SSL/TLS",
|
||||
"CA 证书 (PEM)" => "CA certificate (PEM)",
|
||||
"客户端证书 (PEM)" => "Client certificate (PEM)",
|
||||
"客户端密钥 (PEM)" => "Client key (PEM)",
|
||||
"浏览…" => "Browse…",
|
||||
"选择证书 / 密钥文件" => "Select certificate / key file",
|
||||
"忽略 CA 校验(不验证服务器证书)" => "Skip CA verification (trust any server cert)",
|
||||
"已配置 {n} 项" => "{n} configured",
|
||||
"MQTT 5 属性已配置" => "MQTT 5 properties configured",
|
||||
"遗嘱消息 (Last Will)" => "Last Will",
|
||||
"启用遗嘱消息" => "Enable last will",
|
||||
"遗嘱主题" => "Will topic",
|
||||
"遗嘱内容" => "Will payload",
|
||||
"遗嘱主题不能为空" => "Will topic cannot be empty",
|
||||
"遗嘱主题不能包含通配符 + 或 #" => "Will topic cannot contain wildcards + or #",
|
||||
"遗嘱消息已启用" => "Last will enabled",
|
||||
"Retain" => "Retain",
|
||||
"内容类型 (Content Type)" => "Content Type",
|
||||
"响应主题 (Response Topic)" => "Response Topic",
|
||||
"连接名称不能为空" => "Connection name cannot be empty",
|
||||
"主机地址不能为空" => "Host cannot be empty",
|
||||
"Client ID 不能为空" => "Client ID cannot be empty",
|
||||
"会话过期间隔(秒)" => "Session expiry (s)",
|
||||
"接收最大值" => "Receive Maximum",
|
||||
"最大报文长度" => "Maximum Packet Size",
|
||||
"主题别名上限" => "Topic Alias Maximum",
|
||||
"测试连接" => "Test Connection",
|
||||
"测试中…" => "Testing…",
|
||||
"连接测试成功" => "Connection test succeeded",
|
||||
"配置已保存,重连后生效" => "Saved. Takes effect after reconnect",
|
||||
/// 手写扫描(避免引入 regex 依赖):找 `::t("` / `::tf("` 后的字面量。
|
||||
/// 两个针互不误伤:`::t("` 要求引号紧跟 t,不会命中 `::tf("`。
|
||||
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()] {
|
||||
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 {
|
||||
out.push(lit);
|
||||
}
|
||||
i = end.max(i + 1);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ── 设置 ──
|
||||
"跟随系统" => "Follow system",
|
||||
"浅色" => "Light",
|
||||
"深色" => "Dark",
|
||||
"界面语言" => "Language",
|
||||
"每条连接内存中保留的消息条数(100~100000)" => {
|
||||
"Messages kept in memory per connection (100-100000)"
|
||||
}
|
||||
"另有每连接 {n}MB 的字节预算兜底:大报文流会优先按字节从最旧开始驱逐" => {
|
||||
"Plus a per-connection byte budget of {n} MB: under heavy traffic, oldest messages are evicted by size first"
|
||||
}
|
||||
"时间戳显示毫秒" => "Show milliseconds in timestamps",
|
||||
"自动检查更新(启动时)" => "Check for updates on start",
|
||||
"数据目录" => "Data directory",
|
||||
"日志目录" => "Log directory",
|
||||
"打开目录" => "Open folder",
|
||||
"打开日志目录" => "Open log folder",
|
||||
"打开目录失败: {e}" => "Failed to open folder: {e}",
|
||||
"创建日志目录失败: {e}" => "Failed to create log folder: {e}",
|
||||
"关于" => "About",
|
||||
"版本 {v}(更新源 {src})" => "Version {v} (update source: {src})",
|
||||
"Apache-2.0 许可证" => "Apache-2.0 license",
|
||||
"对标 MQTTX 的 GPUI 跨平台 MQTT 调试客户端" => {
|
||||
"A cross-platform MQTT debug client built with GPUI, aiming at MQTTX"
|
||||
}
|
||||
"检查更新" => "Check for updates",
|
||||
"检查 GitHub Releases 上的新版本" => "Check GitHub Releases for a new version",
|
||||
"检查中…" => "Checking…",
|
||||
"正在检查更新…" => "Checking for updates…",
|
||||
"重新检查" => "Check again",
|
||||
"已是最新版本" => "You are up to date",
|
||||
"立即更新到 v{v}" => "Update to v{v} now",
|
||||
"发现新版本 v{v},下载后自动替换重启" => {
|
||||
"New version v{v} found — downloads, replaces itself and restarts"
|
||||
}
|
||||
"新版本 v{v} 已下载并通过校验,等待安装" => {
|
||||
"Version v{v} downloaded and verified, ready to install"
|
||||
}
|
||||
"立即安装" => "Install now",
|
||||
"下次启动安装" => "Install on next start",
|
||||
"已暂存,下次启动时将自动安装" => "Staged — it will install on next start",
|
||||
"放弃此次更新" => "Discard this update",
|
||||
"已删除暂存的更新包" => "Staged update discarded",
|
||||
"下载中 {pct}%({done} / {total})" => "Downloading {pct}% ({done} / {total})",
|
||||
"下载中 {done}" => "Downloading {done}",
|
||||
"重试" => "Retry",
|
||||
"更新失败:{e}" => "Update failed: {e}",
|
||||
"更新就绪" => "Update Ready",
|
||||
"新版本 v{v} 已下载完成(已通过 sha256 校验)。" => {
|
||||
"Version v{v} has been downloaded (sha256 verified)."
|
||||
}
|
||||
"暂不更新" => "Not now",
|
||||
"安装失败:{e}" => "Install failed: {e}",
|
||||
fn find(hay: &[u8], from: usize, needle: &[u8]) -> Option<usize> {
|
||||
hay[from..]
|
||||
.windows(needle.len())
|
||||
.position(|w| w == needle)
|
||||
.map(|p| p + from)
|
||||
}
|
||||
|
||||
// ── 导入 / 导出 / 通知 ──
|
||||
"导出 {n} 条连接到 {path}" => "Exported {n} connections to {path}",
|
||||
"导入 {n} 条,跳过 {n2} 条" => "Imported {n}, skipped {n2}",
|
||||
"已保存" => "Saved",
|
||||
|
||||
// ── 预设管理 ──
|
||||
"发布预设" => "Publish Presets",
|
||||
"新建预设" => "New Preset",
|
||||
"预设列表" => "Presets",
|
||||
|
||||
// ── 资源监控 / 其他对话框 ──
|
||||
"自身进程资源采样" => "Self-process resource sampling",
|
||||
|
||||
_ => zh,
|
||||
/// 从 `start`(首字节应为 `"`)读一个 Rust 字符串字面量,
|
||||
/// 处理转义,返回 (解码内容, 结束位置)。
|
||||
fn read_string(b: &[u8], start: usize) -> (Option<String>, usize) {
|
||||
if start >= b.len() || b[start] != b'"' {
|
||||
return (None, start + 1);
|
||||
}
|
||||
let mut out = String::new();
|
||||
let mut i = start + 1;
|
||||
while i < b.len() {
|
||||
match b[i] {
|
||||
b'\\' => {
|
||||
i += 1;
|
||||
if i < b.len() {
|
||||
match b[i] {
|
||||
b'"' => out.push('"'),
|
||||
b'\\' => out.push('\\'),
|
||||
other => {
|
||||
out.push('\\');
|
||||
out.push(other as char);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
b'"' => return (Some(out), i + 1),
|
||||
_ => {
|
||||
let mut end = i + 1;
|
||||
while end < b.len() && (b[end] & 0xC0) == 0x80 {
|
||||
end += 1;
|
||||
}
|
||||
out.push_str(&String::from_utf8_lossy(&b[i..end]));
|
||||
i = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, i)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ impl Render for KvEditor {
|
||||
.child(
|
||||
Button::new(SharedString::from(format!("kv-add-{prefix}")))
|
||||
.icon(IconName::Plus)
|
||||
.label(crate::ui::i18n::t("添加属性"))
|
||||
.label(crate::ui::i18n::t("kv.add"))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
|
||||
Reference in New Issue
Block a user