i18n: the find box, sort and filter builders and password toggle in Chinese
The find counts, toggles, replace buttons, the builders' add, join and direction labels, the filter tests and the password tooltip read the builtin catalog. Test::label becomes Test::key, the choice's value; its label is the catalog's. The shipped-copy scan checks button, segment and match-arm text on every match, and zh-CN render tests cover both. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
79e94f7e24
commit
a4bf6965f9
11 files changed
+303
-79
No files matched your search
@@ -226,7 +226,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- A pane the window's blur shows through is `shell::Vibrancy`, filled with the palette's `glass`, in a window opened `WindowBackgroundAppearance::Blurred`: vibrancy on macOS, acrylic on Windows. gpui's `MicaBackdrop` and `MicaAltBackdrop` are DWM backdrops from Windows 11 build 22621; older builds get none, in silence, and macOS plain transparency.
|
||||
- The chart's hues go by `theme::HUE_NAMES`, for an event's color and a label's alike.
|
||||
- Text whose every character counts, encoded data and keys, goes through `typography::literal`: JetBrains Mono draws `==` and `->` as ligatures (`devtools::Encoder`).
|
||||
- Messages go through `i18n::I18n`, a global the app sets: `cx.global::<I18n>().text(key, args)` fills each `{name}`. Every catalog holds the first one's keys; a missing key, argument or catalog fails. Copy a component draws, tooltips and default placeholders too, reads `i18n::text` (app catalog, then the builtin English and Chinese); `i18n/tests/shipped.rs` fails on a fixed English literal in a component Ely ships. A right-to-left locale mirrors a stack with `Direction::row` and sets its text with `Direction::align`; CoreText orders mixed Hebrew and Latin runs on macOS, but gpui sets no paragraph direction, so a line takes its first strong character's: `text` leads a right-to-left message with U+200F, so its words suit labels, not a field's value, and `Locale::number` a right-to-left minus with U+200E, as CLDR does. gpui wraps shaped glyphs in the order they are drawn, on the web as on macOS, so a right-to-left line that wraps comes out garbled: right-to-left text stays on one line, an `Ellipsis`, which cuts by characters and sits by its text's alignment.
|
||||
- Messages go through `i18n::I18n`, a global the app sets: `cx.global::<I18n>().text(key, args)` fills each `{name}`. Every catalog holds the first one's keys; a missing key, argument or catalog fails. Copy a component draws, tooltips and default placeholders too, reads `i18n::text` (app catalog, then the builtin English and Chinese); `i18n/tests/shipped.rs` fails on a fixed English literal in a component Ely ships (labels, tooltips, button and segment text, match arms, capitalized literal arguments); a choice's value stays its key, its label the catalog's (`tables::Test::key`). A right-to-left locale mirrors a stack with `Direction::row` and sets its text with `Direction::align`; CoreText orders mixed Hebrew and Latin runs on macOS, but gpui sets no paragraph direction, so a line takes its first strong character's: `text` leads a right-to-left message with U+200F, so its words suit labels, not a field's value, and `Locale::number` a right-to-left minus with U+200E, as CLDR does. gpui wraps shaped glyphs in the order they are drawn, on the web as on macOS, so a right-to-left line that wraps comes out garbled: right-to-left text stays on one line, an `Ellipsis`, which cuts by characters and sits by its text's alignment.
|
||||
- gpui orders Tab stops by `tab_index` before paint order, and registers none for an `invisible()` element. A stop that must come first where it sits, yet draw over what paints after it, stays in flow at zero size and raises what it shows (`i18n::SkipLink`): a lower `tab_index` would sort it before the root scope's own handle, so Tab from the root would pass it.
|
||||
- Color-blind safe charts swap Ely's chart hues for a set fitted to stay apart under Machado's protanopia, deuteranopia and tritanopia; a test holds every pair 0.10 apart in Oklab and each hue 3:1 on its page (`Theme::set_color_blind_safe`, in Ely's own palettes alone).
|
||||
- gpui packs pictures in its atlas without padding and samples them with a linear filter, on the web as on macOS, so a scaled picture's edge takes color from its atlas neighbor, and pictures laid edge to edge show seams. A map tile carries a one-pixel rim copied from its own edge and draws one of its pixels larger each side, clipped to its box (`maps::tile`).
|
||||
|
||||
+49
-28
@@ -149,6 +149,26 @@ impl FindWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// The count shown, and whether it reads as a miss.
|
||||
pub(crate) fn count(
|
||||
matches: &Result<(Option<usize>, usize), SharedString>,
|
||||
cx: &App,
|
||||
) -> (SharedString, bool) {
|
||||
match matches {
|
||||
Ok((_, 0)) => (i18n::text(cx, "find.none", &[]), true),
|
||||
Ok((Some(current), total)) => {
|
||||
let (at, total) = ((current + 1).to_string(), total.to_string());
|
||||
let args = [("at", at.as_str()), ("total", total.as_str())];
|
||||
(i18n::text(cx, "find.at", &args), false)
|
||||
}
|
||||
Ok((None, total)) => {
|
||||
let total = total.to_string();
|
||||
(i18n::text(cx, "find.found", &[("total", &total)]), false)
|
||||
}
|
||||
Err(message) => (message.clone(), true),
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for FindWidget {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
@@ -156,7 +176,7 @@ impl RenderOnce for FindWidget {
|
||||
let options = self.options;
|
||||
let toggle = |key: &'static str,
|
||||
icon: IconName,
|
||||
words: &'static str,
|
||||
words: SharedString,
|
||||
on: bool,
|
||||
set: fn(&mut FindOptions, bool)| {
|
||||
let on_options = self.on_options.clone();
|
||||
@@ -175,16 +195,11 @@ impl RenderOnce for FindWidget {
|
||||
}
|
||||
})
|
||||
};
|
||||
let (count, missing) = match &self.matches {
|
||||
Ok((_, 0)) => ("No results".to_string(), true),
|
||||
Ok((Some(current), total)) => (format!("{} of {total}", current + 1), false),
|
||||
Ok((None, total)) => (format!("{total} found"), false),
|
||||
Err(message) => (message.to_string(), true),
|
||||
};
|
||||
let (count, missing) = count(&self.matches, cx);
|
||||
let found = matches!(self.matches, Ok((_, total)) if total > 0);
|
||||
let icon_button = |key: &'static str,
|
||||
icon: IconName,
|
||||
words: &'static str,
|
||||
words: SharedString,
|
||||
enabled: bool,
|
||||
action: Option<OnBool>,
|
||||
value: bool| {
|
||||
@@ -230,7 +245,7 @@ impl RenderOnce for FindWidget {
|
||||
} else {
|
||||
IconName::ChevronRight
|
||||
},
|
||||
"Toggle replace",
|
||||
i18n::text(cx, "find.fold", &[]),
|
||||
true,
|
||||
Some(fold),
|
||||
!showing,
|
||||
@@ -246,21 +261,21 @@ impl RenderOnce for FindWidget {
|
||||
row.child(toggle(
|
||||
"case",
|
||||
IconName::CaseSensitive,
|
||||
"Match case",
|
||||
i18n::text(cx, "find.case", &[]),
|
||||
options.case,
|
||||
|next, on| next.case = on,
|
||||
))
|
||||
.child(toggle(
|
||||
"word",
|
||||
IconName::WholeWord,
|
||||
"Whole word",
|
||||
i18n::text(cx, "find.word", &[]),
|
||||
options.word,
|
||||
|next, on| next.word = on,
|
||||
))
|
||||
.child(toggle(
|
||||
"regex",
|
||||
IconName::Regex,
|
||||
"Regular expression",
|
||||
i18n::text(cx, "find.regex", &[]),
|
||||
options.regex,
|
||||
|next, on| next.regex = on,
|
||||
))
|
||||
@@ -279,7 +294,7 @@ impl RenderOnce for FindWidget {
|
||||
.child(icon_button(
|
||||
"previous",
|
||||
IconName::ArrowUp,
|
||||
"Previous match",
|
||||
i18n::text(cx, "find.previous", &[]),
|
||||
found,
|
||||
self.on_step.clone(),
|
||||
false,
|
||||
@@ -287,7 +302,7 @@ impl RenderOnce for FindWidget {
|
||||
.child(icon_button(
|
||||
"next",
|
||||
IconName::ArrowDown,
|
||||
"Next match",
|
||||
i18n::text(cx, "find.next", &[]),
|
||||
found,
|
||||
self.on_step.clone(),
|
||||
true,
|
||||
@@ -313,22 +328,28 @@ impl RenderOnce for FindWidget {
|
||||
.child(Input::new(field).size(ControlSize::Sm)),
|
||||
)
|
||||
.child(
|
||||
Button::new((self.id.clone(), "replace"), "Replace")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.disabled(!found)
|
||||
.when_some(one, |button, one| {
|
||||
button.on_click(move |_, window, cx| one(false, window, cx))
|
||||
}),
|
||||
Button::new(
|
||||
(self.id.clone(), "replace"),
|
||||
i18n::text(cx, "find.replace", &[]),
|
||||
)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.disabled(!found)
|
||||
.when_some(one, |button, one| {
|
||||
button.on_click(move |_, window, cx| one(false, window, cx))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new((self.id.clone(), "all"), "Replace all")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.disabled(!found)
|
||||
.when_some(all, |button, all| {
|
||||
button.on_click(move |_, window, cx| all(true, window, cx))
|
||||
}),
|
||||
Button::new(
|
||||
(self.id.clone(), "all"),
|
||||
i18n::text(cx, "find.replace_all", &[]),
|
||||
)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.disabled(!found)
|
||||
.when_some(all, |button, all| {
|
||||
button.on_click(move |_, window, cx| all(true, window, cx))
|
||||
}),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
//! The find box reads in Chinese under zh-CN.
|
||||
|
||||
use gpui::{Entity, IntoElement, Render, TestAppContext, Window, div, prelude::*};
|
||||
|
||||
use crate::{forms::TextInput, i18n::I18n, theme::Theme};
|
||||
|
||||
struct Host(Entity<TextInput>, Entity<TextInput>);
|
||||
|
||||
impl Render for Host {
|
||||
fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
|
||||
let find = super::super::FindWidget::new("find", &self.0, Some(0), 2)
|
||||
.replace(&self.1)
|
||||
.on_options(|_, _, _| {})
|
||||
.on_step(|_, _, _| {})
|
||||
.on_toggle_replace(|_, _, _| {})
|
||||
.on_close(|_, _| {});
|
||||
div().size_full().child(find)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_find_box_reads_in_chinese(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
Theme::init(cx);
|
||||
cx.set_global(I18n::new("zh-CN").catalog("zh-CN", &[]));
|
||||
});
|
||||
cx.update(|cx| {
|
||||
let count = |matches| super::super::find::count(&matches, cx);
|
||||
assert_eq!(count(Ok((None, 0))), ("无结果".into(), true));
|
||||
assert_eq!(count(Ok((Some(0), 2))), ("第 1 项,共 2 项".into(), false));
|
||||
assert_eq!(count(Ok((None, 3))), ("找到 3 项".into(), false));
|
||||
});
|
||||
let (_, cx) = cx.add_window_view(|window, cx| {
|
||||
Host(
|
||||
cx.new(|cx| TextInput::new(window, cx)),
|
||||
cx.new(|cx| TextInput::new(window, cx)),
|
||||
)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod chinese;
|
||||
mod history;
|
||||
mod stale;
|
||||
|
||||
|
||||
+9
-5
@@ -199,11 +199,15 @@ impl RenderOnce for PasswordInput {
|
||||
},
|
||||
)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip(if masked {
|
||||
"Show password"
|
||||
} else {
|
||||
"Hide password"
|
||||
})
|
||||
.tooltip(i18n::text(
|
||||
cx,
|
||||
if masked {
|
||||
"input.show_password"
|
||||
} else {
|
||||
"input.hide_password"
|
||||
},
|
||||
&[],
|
||||
))
|
||||
.on_click(move |_, _, cx| {
|
||||
toggle.update(cx, |input, cx| {
|
||||
let masked = input.is_masked();
|
||||
|
||||
@@ -117,9 +117,34 @@ const EN: &[(&str, &str)] = &[
|
||||
("theme.system", "System"),
|
||||
("select.placeholder", "Choose…"),
|
||||
("input.clear", "Clear"),
|
||||
("input.show_password", "Show password"),
|
||||
("input.hide_password", "Hide password"),
|
||||
("completion.empty", "No suggestions"),
|
||||
("builder.remove", "Remove"),
|
||||
("tabs.unpin", "Unpin"),
|
||||
("find.none", "No results"),
|
||||
("find.at", "{at} of {total}"),
|
||||
("find.found", "{total} found"),
|
||||
("find.fold", "Toggle replace"),
|
||||
("find.case", "Match case"),
|
||||
("find.word", "Whole word"),
|
||||
("find.regex", "Regular expression"),
|
||||
("find.previous", "Previous match"),
|
||||
("find.next", "Next match"),
|
||||
("find.replace", "Replace"),
|
||||
("find.replace_all", "Replace all"),
|
||||
("builder.add_rule", "Add rule"),
|
||||
("builder.all", "Match all"),
|
||||
("builder.any", "Match any"),
|
||||
("builder.add_sort", "Add sort"),
|
||||
("builder.up", "Ascending"),
|
||||
("builder.down", "Descending"),
|
||||
("builder.test.contains", "contains"),
|
||||
("builder.test.equals", "is"),
|
||||
("builder.test.not_equals", "is not"),
|
||||
("builder.test.above", "is above"),
|
||||
("builder.test.below", "is below"),
|
||||
("builder.test.empty", "is empty"),
|
||||
("theme.size.reset", "Back to the default size"),
|
||||
(
|
||||
"theme.size.sample",
|
||||
@@ -240,9 +265,34 @@ const ZH_CN: &[(&str, &str)] = &[
|
||||
("theme.system", "跟随系统"),
|
||||
("select.placeholder", "请选择…"),
|
||||
("input.clear", "清除"),
|
||||
("input.show_password", "显示密码"),
|
||||
("input.hide_password", "隐藏密码"),
|
||||
("completion.empty", "没有建议"),
|
||||
("builder.remove", "移除"),
|
||||
("tabs.unpin", "取消固定"),
|
||||
("find.none", "无结果"),
|
||||
("find.at", "第 {at} 项,共 {total} 项"),
|
||||
("find.found", "找到 {total} 项"),
|
||||
("find.fold", "切换替换"),
|
||||
("find.case", "区分大小写"),
|
||||
("find.word", "全字匹配"),
|
||||
("find.regex", "使用正则表达式"),
|
||||
("find.previous", "上一个匹配项"),
|
||||
("find.next", "下一个匹配项"),
|
||||
("find.replace", "替换"),
|
||||
("find.replace_all", "全部替换"),
|
||||
("builder.add_rule", "添加规则"),
|
||||
("builder.all", "满足全部"),
|
||||
("builder.any", "满足任一"),
|
||||
("builder.add_sort", "添加排序"),
|
||||
("builder.up", "升序"),
|
||||
("builder.down", "降序"),
|
||||
("builder.test.contains", "包含"),
|
||||
("builder.test.equals", "等于"),
|
||||
("builder.test.not_equals", "不等于"),
|
||||
("builder.test.above", "大于"),
|
||||
("builder.test.below", "小于"),
|
||||
("builder.test.empty", "为空"),
|
||||
("theme.size.reset", "恢复默认大小"),
|
||||
(
|
||||
"theme.size.sample",
|
||||
|
||||
@@ -10,6 +10,7 @@ const SHIPPED: &[(&str, &str)] = &[
|
||||
"tables/builders.rs",
|
||||
include_str!("../../tables/builders.rs"),
|
||||
),
|
||||
("tables/rules.rs", include_str!("../../tables/rules.rs")),
|
||||
(
|
||||
"navigation/editor_tabs.rs",
|
||||
include_str!("../../navigation/editor_tabs.rs"),
|
||||
@@ -22,17 +23,46 @@ const SHIPPED: &[(&str, &str)] = &[
|
||||
|
||||
#[test]
|
||||
fn shipped_components_show_no_fixed_english() {
|
||||
let shown = [".tooltip(\"", ".child(\"", "SharedString::from(\""];
|
||||
let shown = [".tooltip(\"", ".child(\"", "SharedString::from(\"", "=> \""];
|
||||
// After an id: words, not a lowercase id.
|
||||
let labels = ["), \"", "\", \"", "(\""];
|
||||
let mut found = Vec::new();
|
||||
for (path, source) in SHIPPED {
|
||||
let code = source.split("#[cfg(test)]").next().unwrap_or(source);
|
||||
for (no, line) in code.lines().enumerate() {
|
||||
for call in shown {
|
||||
let Some(at) = line.find(call) else { continue };
|
||||
let rest = &line[at + call.len()..];
|
||||
if rest.starts_with(|ch: char| ch.is_ascii_alphabetic()) {
|
||||
found.push(format!("{path}:{}: {}", no + 1, line.trim()));
|
||||
}
|
||||
let checks = ["assert", "panic!", "log::", ".expect("];
|
||||
if checks.iter().any(|check| line.contains(check)) {
|
||||
continue;
|
||||
}
|
||||
let literals = |calls: &[&str]| -> Vec<&str> {
|
||||
(calls.iter())
|
||||
.flat_map(|call| {
|
||||
line.match_indices(call)
|
||||
.map(|(at, _)| &line[at + call.len()..])
|
||||
})
|
||||
.map(|rest| rest.split('"').next().unwrap_or(rest))
|
||||
.filter(|literal| literal.starts_with(|ch: char| ch.is_ascii_alphabetic()))
|
||||
.collect()
|
||||
};
|
||||
let key = |literal: &str| literal.contains('.') && !literal.contains(' ');
|
||||
let words =
|
||||
|literal: &str| literal.contains(' ') || literal.starts_with(char::is_uppercase);
|
||||
let english = (literals(&shown).into_iter().filter(|literal| !key(literal)))
|
||||
.chain(
|
||||
literals(&labels)
|
||||
.into_iter()
|
||||
.filter(|literal| words(literal)),
|
||||
)
|
||||
.chain(
|
||||
line.trim()
|
||||
.strip_prefix('"')
|
||||
.and_then(|rest| rest.split('"').next()),
|
||||
)
|
||||
.filter(|literal| {
|
||||
literal.starts_with(char::is_uppercase) || !line.trim().starts_with('"')
|
||||
});
|
||||
if english.count() > 0 {
|
||||
found.push(format!("{path}:{}: {}", no + 1, line.trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-31
@@ -152,7 +152,7 @@ impl RenderOnce for FilterBuilder {
|
||||
let emit = Rc::new(emit);
|
||||
let tests: Vec<Choice> = Test::ALL
|
||||
.iter()
|
||||
.map(|test| Choice::new(test.label(), test.label()))
|
||||
.map(|test| Choice::new(test.key(), i18n::text(cx, test.key(), &[])))
|
||||
.collect();
|
||||
let theme = cx.theme();
|
||||
let (rules, any, id) = (self.rules.clone(), self.any, self.id.clone());
|
||||
@@ -187,13 +187,13 @@ impl RenderOnce for FilterBuilder {
|
||||
.child(
|
||||
div().w(theme.label_width() * 0.75).child(
|
||||
Select::new((id.clone(), format!("test-{ix}")), tests.clone())
|
||||
.selected(rule.test.label())
|
||||
.selected(rule.test.key())
|
||||
.size(ControlSize::Sm)
|
||||
.on_change(move |label, window, cx| {
|
||||
let mut next = for_test.clone();
|
||||
next[ix].test = *Test::ALL
|
||||
.iter()
|
||||
.find(|test| test.label() == label.as_ref())
|
||||
.find(|test| test.key() == label.as_ref())
|
||||
.expect("a listed test");
|
||||
test(next, any, window, cx)
|
||||
}),
|
||||
@@ -224,18 +224,21 @@ impl RenderOnce for FilterBuilder {
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
Button::new((self.id.clone(), "add"), "Add rule")
|
||||
.icon(IconName::Plus)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(move |_, window, cx| {
|
||||
let mut next = to_add.clone();
|
||||
next.push(FilterRule {
|
||||
column: first.clone(),
|
||||
..FilterRule::default()
|
||||
});
|
||||
add(next, any, window, cx)
|
||||
}),
|
||||
Button::new(
|
||||
(self.id.clone(), "add"),
|
||||
i18n::text(cx, "builder.add_rule", &[]),
|
||||
)
|
||||
.icon(IconName::Plus)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(move |_, window, cx| {
|
||||
let mut next = to_add.clone();
|
||||
next.push(FilterRule {
|
||||
column: first.clone(),
|
||||
..FilterRule::default()
|
||||
});
|
||||
add(next, any, window, cx)
|
||||
}),
|
||||
)
|
||||
.when(self.rules.len() > 1, |footer| {
|
||||
footer.child(
|
||||
@@ -243,8 +246,8 @@ impl RenderOnce for FilterBuilder {
|
||||
(self.id.clone(), "join"),
|
||||
if any { "any" } else { "all" },
|
||||
)
|
||||
.segment("all", "Match all", None)
|
||||
.segment("any", "Match any", None)
|
||||
.segment("all", i18n::text(cx, "builder.all", &[]), None)
|
||||
.segment("any", i18n::text(cx, "builder.any", &[]), None)
|
||||
.size(ControlSize::Sm)
|
||||
.on_change(move |value, window, cx| {
|
||||
join(to_join.clone(), value.as_ref() == "any", window, cx)
|
||||
@@ -365,8 +368,16 @@ impl RenderOnce for SortBuilder {
|
||||
(id.clone(), format!("sort-way-{ix}")),
|
||||
if sort.rising { "up" } else { "down" },
|
||||
)
|
||||
.segment("up", "Ascending", Some(IconName::ArrowUp))
|
||||
.segment("down", "Descending", Some(IconName::ArrowDown))
|
||||
.segment(
|
||||
"up",
|
||||
i18n::text(cx, "builder.up", &[]),
|
||||
Some(IconName::ArrowUp),
|
||||
)
|
||||
.segment(
|
||||
"down",
|
||||
i18n::text(cx, "builder.down", &[]),
|
||||
Some(IconName::ArrowDown),
|
||||
)
|
||||
.size(ControlSize::Sm)
|
||||
.on_change(move |way, window, cx| {
|
||||
let mut next = for_flip.clone();
|
||||
@@ -395,18 +406,21 @@ impl RenderOnce for SortBuilder {
|
||||
.gap_1()
|
||||
.child(list)
|
||||
.children(unused.map(|column| {
|
||||
Button::new((self.id.clone(), "add"), "Add sort")
|
||||
.icon(IconName::Plus)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(move |_, window, cx| {
|
||||
let mut next = add.1.clone();
|
||||
next.push(SortKey {
|
||||
column: column.clone(),
|
||||
rising: true,
|
||||
});
|
||||
(add.0)(&next, window, cx)
|
||||
})
|
||||
Button::new(
|
||||
(self.id.clone(), "add"),
|
||||
i18n::text(cx, "builder.add_sort", &[]),
|
||||
)
|
||||
.icon(IconName::Plus)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(move |_, window, cx| {
|
||||
let mut next = add.1.clone();
|
||||
next.push(SortKey {
|
||||
column: column.clone(),
|
||||
rising: true,
|
||||
});
|
||||
(add.0)(&next, window, cx)
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -26,14 +26,15 @@ impl Test {
|
||||
Test::Empty,
|
||||
];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
/// Its catalog key, which also names it.
|
||||
pub fn key(self) -> &'static str {
|
||||
match self {
|
||||
Self::Contains => "contains",
|
||||
Self::Equals => "is",
|
||||
Self::NotEquals => "is not",
|
||||
Self::Above => "is above",
|
||||
Self::Below => "is below",
|
||||
Self::Empty => "is empty",
|
||||
Self::Contains => "builder.test.contains",
|
||||
Self::Equals => "builder.test.equals",
|
||||
Self::NotEquals => "builder.test.not_equals",
|
||||
Self::Above => "builder.test.above",
|
||||
Self::Below => "builder.test.below",
|
||||
Self::Empty => "builder.test.empty",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//! The sort and filter builders read in Chinese under zh-CN.
|
||||
|
||||
use gpui::{IntoElement, Render, TestAppContext, Window, div, prelude::*};
|
||||
|
||||
use crate::{
|
||||
i18n::{self, I18n},
|
||||
tables::{FilterBuilder, FilterRule, SortBuilder, SortKey, Test},
|
||||
theme::Theme,
|
||||
};
|
||||
|
||||
struct Builders;
|
||||
|
||||
impl Render for Builders {
|
||||
fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
|
||||
let columns = [("a", "第一列"), ("b", "第二列")];
|
||||
let rule = FilterRule {
|
||||
column: "a".into(),
|
||||
..FilterRule::default()
|
||||
};
|
||||
let key = SortKey {
|
||||
column: "a".into(),
|
||||
rising: true,
|
||||
};
|
||||
div()
|
||||
.size_full()
|
||||
.child(FilterBuilder::new("filter", columns).rules([rule.clone(), rule], true))
|
||||
.child(SortBuilder::new("sort", columns).keys([key]))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_builders_read_in_chinese(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
Theme::init(cx);
|
||||
cx.set_global(I18n::new("zh-CN").catalog("zh-CN", &[]));
|
||||
});
|
||||
cx.update(|cx| {
|
||||
for test in Test::ALL {
|
||||
let shown = i18n::text(cx, test.key(), &[]);
|
||||
assert!(
|
||||
!shown.chars().any(|ch| ch.is_ascii_alphabetic()),
|
||||
"{test:?}: {shown}"
|
||||
);
|
||||
}
|
||||
for key in [
|
||||
"builder.add_rule",
|
||||
"builder.all",
|
||||
"builder.any",
|
||||
"builder.add_sort",
|
||||
"builder.up",
|
||||
"builder.down",
|
||||
] {
|
||||
let shown = i18n::text(cx, key, &[]);
|
||||
assert!(
|
||||
!shown.chars().any(|ch| ch.is_ascii_alphabetic()),
|
||||
"{key}: {shown}"
|
||||
);
|
||||
}
|
||||
});
|
||||
let (_, cx) = cx.add_window_view(|_, _| Builders);
|
||||
cx.run_until_parked();
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use gpui::{
|
||||
Styled, TestAppContext, VisualTestContext, Window, div, point, px,
|
||||
};
|
||||
|
||||
mod chinese;
|
||||
mod clipboard;
|
||||
mod columns;
|
||||
mod stale;
|
||||
|
||||
Reference in new issue
Block a user