旧实现的问题(按用户反馈):
- 中文原文作 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。
123 lines
4.4 KiB
Rust
123 lines
4.4 KiB
Rust
//! key/value 列表编辑器(MQTT 5 用户属性等)。
|
|
|
|
use gpui_kit::component::button::{ Button, ButtonVariants as _ };
|
|
use gpui_kit::component::input::{ Input, InputState };
|
|
use gpui_kit::component::{ h_flex, v_flex, Disableable as _, Sizable as _ };
|
|
use crate::ui::IconName;
|
|
use gpui_kit::{ App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString, Styled as _, Window, div, px };
|
|
|
|
|
|
/// MQTT 5 用户属性等 key/value 列表编辑器。
|
|
pub struct KvEditor {
|
|
id_prefix: SharedString,
|
|
pub rows: Vec<(Entity<InputState>, Entity<InputState>)>,
|
|
}
|
|
|
|
impl KvEditor {
|
|
pub fn new(
|
|
id_prefix: impl Into<SharedString>,
|
|
pairs: &[(String, String)],
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let mut editor = Self {
|
|
id_prefix: id_prefix.into(),
|
|
rows: Vec::new(),
|
|
};
|
|
for (k, v) in pairs {
|
|
editor.push(Some((k.as_str(), v.as_str())), window, cx);
|
|
}
|
|
if editor.rows.is_empty() {
|
|
editor.push(None, window, cx);
|
|
}
|
|
editor
|
|
}
|
|
|
|
fn push(&mut self, initial: Option<(&str, &str)>, window: &mut Window, cx: &mut Context<Self>) {
|
|
let key = cx.new(|cx| {
|
|
let mut s = InputState::new(window, cx).placeholder("key");
|
|
if let Some((k, _)) = initial {
|
|
s.set_value(k, window, cx);
|
|
}
|
|
s
|
|
});
|
|
let value = cx.new(|cx| {
|
|
let mut s = InputState::new(window, cx).placeholder("value");
|
|
if let Some((_, v)) = initial {
|
|
s.set_value(v, window, cx);
|
|
}
|
|
s
|
|
});
|
|
self.rows.push((key, value));
|
|
}
|
|
|
|
pub fn pairs(&self, cx: &App) -> Vec<(String, String)> {
|
|
// key 统一 trim 并去重(保留最后一条):否则带首尾空白的 key
|
|
// 永远无法被 {{key}} 模板引用,重复 key 也只有一条能生效。
|
|
let mut out: Vec<(String, String)> = Vec::new();
|
|
for (k, v) in self
|
|
.rows
|
|
.iter()
|
|
.map(|(k, v)| (k.read(cx).value().to_string(), v.read(cx).value().to_string()))
|
|
{
|
|
let k = k.trim().to_string();
|
|
if k.is_empty() {
|
|
continue;
|
|
}
|
|
match out.iter_mut().find(|(ek, _)| *ek == k) {
|
|
Some(slot) => slot.1 = v,
|
|
None => out.push((k, v)),
|
|
}
|
|
}
|
|
out
|
|
}
|
|
}
|
|
|
|
impl Render for KvEditor {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let prefix = self.id_prefix.clone();
|
|
let count = self.rows.len();
|
|
|
|
let mut rows = v_flex().gap_1();
|
|
for (i, (key, value)) in self.rows.iter().cloned().enumerate() {
|
|
rows = rows.child(
|
|
h_flex()
|
|
.id(SharedString::from(format!("kv-row-{prefix}-{i}")))
|
|
.gap_1()
|
|
.child(div().flex_1().min_w(px(0.)).child(Input::new(&key)))
|
|
.child(div().flex_1().min_w(px(0.)).child(Input::new(&value)))
|
|
.child(
|
|
Button::new(SharedString::from(format!("kv-del-{prefix}-{i}")))
|
|
.icon(IconName::Close)
|
|
.ghost()
|
|
.xsmall()
|
|
.disabled(count <= 1)
|
|
.on_click(cx.listener(move |this, _, _, cx| {
|
|
if this.rows.len() > 1 {
|
|
this.rows.remove(i);
|
|
cx.notify();
|
|
}
|
|
})),
|
|
),
|
|
);
|
|
}
|
|
|
|
v_flex()
|
|
.gap_2()
|
|
.child(rows)
|
|
.child(
|
|
Button::new(SharedString::from(format!("kv-add-{prefix}")))
|
|
.icon(IconName::Plus)
|
|
.label(crate::ui::i18n::t("kv.add"))
|
|
.ghost()
|
|
.xsmall()
|
|
.on_click(cx.listener(|this, _, window, cx| {
|
|
this.push(None, window, cx);
|
|
cx.notify();
|
|
})),
|
|
)
|
|
}
|
|
}
|
|
|
|
// ─── 时间格式化 ──────────────────────────────────────────────────────────────
|