i18n: shipped components read their copy from the catalog
Select's default placeholder, the clear, close, remove and unpin tooltips, the completion menu's empty line and the size sample showed English under zh-CN. A source test now fails on any fixed English literal in them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1b5fb07cda
commit
d05b1b78ca
11 files changed
+86
-15
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. 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 (app catalog, then the builtin English and Chinese); 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.
|
||||
- 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`).
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ use gpui::{
|
||||
div, prelude::*,
|
||||
};
|
||||
|
||||
use crate::i18n;
|
||||
use crate::{
|
||||
buttons::{Button, ButtonVariant, IconButton, ToggleButton, ToggleItem},
|
||||
forms::{Input, TextInput},
|
||||
@@ -295,7 +296,7 @@ impl RenderOnce for FindWidget {
|
||||
IconButton::new((self.id.clone(), "close"), IconName::X)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip("Close")
|
||||
.tooltip(i18n::text(cx, "dialog.close", &[]))
|
||||
.on_click(move |_, window, cx| close(window, cx))
|
||||
})),
|
||||
)
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ use gpui::{
|
||||
};
|
||||
|
||||
use super::{symbols::SymbolKind, syntax};
|
||||
use crate::i18n;
|
||||
use crate::{
|
||||
buttons::{ButtonVariant, IconButton},
|
||||
navigation::{fuzzy, marked},
|
||||
@@ -185,7 +186,7 @@ impl RenderOnce for CompletionMenu {
|
||||
.px_2()
|
||||
.py_1()
|
||||
.text_color(colors.fg_subtle)
|
||||
.child("No suggestions"),
|
||||
.child(i18n::text(cx, "completion.empty", &[])),
|
||||
)
|
||||
})
|
||||
.children(rows),
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ use gpui::{
|
||||
};
|
||||
|
||||
use super::TextInput;
|
||||
use crate::i18n;
|
||||
use crate::{
|
||||
buttons::IconButton,
|
||||
primitives::{Icon, IconName},
|
||||
@@ -148,7 +149,7 @@ impl RenderOnce for Input {
|
||||
frame.child(
|
||||
IconButton::new(("input-clear", clear_id), IconName::CircleX)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip("Clear")
|
||||
.tooltip(i18n::text(cx, "input.clear", &[]))
|
||||
.on_click(move |_, window, cx| {
|
||||
state.update(cx, |input, cx| input.set_text("", cx));
|
||||
window.focus(&state.read(cx).focus().clone(), cx);
|
||||
|
||||
+7
-5
@@ -251,7 +251,7 @@ pub struct Select {
|
||||
id: ElementId,
|
||||
choices: Vec<Choice>,
|
||||
selected: Option<SharedString>,
|
||||
placeholder: SharedString,
|
||||
placeholder: Option<SharedString>,
|
||||
label: Option<SharedString>,
|
||||
size: ControlSize,
|
||||
disabled: bool,
|
||||
@@ -264,7 +264,7 @@ impl Select {
|
||||
id: id.into(),
|
||||
choices: choices.into_iter().collect(),
|
||||
selected: None,
|
||||
placeholder: SharedString::from("Choose…"),
|
||||
placeholder: None,
|
||||
label: None,
|
||||
size: ControlSize::default(),
|
||||
disabled: false,
|
||||
@@ -284,7 +284,7 @@ impl Select {
|
||||
}
|
||||
|
||||
pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
|
||||
self.placeholder = text.into();
|
||||
self.placeholder = Some(text.into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -326,6 +326,8 @@ impl RenderOnce for Select {
|
||||
.as_ref()
|
||||
.and_then(|value| choices.iter().find(|choice| choice.value == *value))
|
||||
.cloned();
|
||||
let placeholder =
|
||||
(self.placeholder.clone()).unwrap_or_else(|| i18n::text(cx, "select.placeholder", &[]));
|
||||
let colors = &cx.theme().colors;
|
||||
let trigger = field_button(
|
||||
self.id.clone(),
|
||||
@@ -336,7 +338,7 @@ impl RenderOnce for Select {
|
||||
cx,
|
||||
)
|
||||
.role(Role::ComboBox)
|
||||
.aria_placeholder(self.placeholder.clone())
|
||||
.aria_placeholder(placeholder.clone())
|
||||
.when_some(self.label.clone(), |trigger, label| {
|
||||
trigger.aria_label(label)
|
||||
})
|
||||
@@ -361,7 +363,7 @@ impl RenderOnce for Select {
|
||||
)
|
||||
.child(field_text(
|
||||
shown.map(|choice| choice.label),
|
||||
self.placeholder,
|
||||
placeholder,
|
||||
self.disabled,
|
||||
cx,
|
||||
))
|
||||
|
||||
@@ -115,6 +115,16 @@ const EN: &[(&str, &str)] = &[
|
||||
("theme.light", "Light"),
|
||||
("theme.dark", "Dark"),
|
||||
("theme.system", "System"),
|
||||
("select.placeholder", "Choose…"),
|
||||
("input.clear", "Clear"),
|
||||
("completion.empty", "No suggestions"),
|
||||
("builder.remove", "Remove"),
|
||||
("tabs.unpin", "Unpin"),
|
||||
("theme.size.reset", "Back to the default size"),
|
||||
(
|
||||
"theme.size.sample",
|
||||
"The quick brown fox jumps over the lazy dog.",
|
||||
),
|
||||
];
|
||||
|
||||
const ZH_CN: &[(&str, &str)] = &[
|
||||
@@ -228,6 +238,16 @@ const ZH_CN: &[(&str, &str)] = &[
|
||||
("theme.light", "浅色"),
|
||||
("theme.dark", "深色"),
|
||||
("theme.system", "跟随系统"),
|
||||
("select.placeholder", "请选择…"),
|
||||
("input.clear", "清除"),
|
||||
("completion.empty", "没有建议"),
|
||||
("builder.remove", "移除"),
|
||||
("tabs.unpin", "取消固定"),
|
||||
("theme.size.reset", "恢复默认大小"),
|
||||
(
|
||||
"theme.size.sample",
|
||||
"天地玄黄,宇宙洪荒。The quick brown fox.",
|
||||
),
|
||||
];
|
||||
|
||||
/// Ely's message for `key`; English if unshipped, fails if unknown.
|
||||
|
||||
@@ -2,8 +2,9 @@ use jiff::{Timestamp, tz::TimeZone};
|
||||
|
||||
use super::{Direction, I18n, LOCALES, Locale, fill};
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
mod a11y;
|
||||
#[cfg(feature = "test-support")]
|
||||
mod shipped;
|
||||
|
||||
fn i18n() -> I18n {
|
||||
I18n::new("en-US")
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//! Components Ely ships say nothing in fixed English.
|
||||
|
||||
/// Sources of components the Ely apps draw.
|
||||
const SHIPPED: &[(&str, &str)] = &[
|
||||
("editor/find.rs", include_str!("../../editor/find.rs")),
|
||||
("editor/hints.rs", include_str!("../../editor/hints.rs")),
|
||||
("forms/input.rs", include_str!("../../forms/input.rs")),
|
||||
("forms/select.rs", include_str!("../../forms/select.rs")),
|
||||
(
|
||||
"tables/builders.rs",
|
||||
include_str!("../../tables/builders.rs"),
|
||||
),
|
||||
(
|
||||
"navigation/editor_tabs.rs",
|
||||
include_str!("../../navigation/editor_tabs.rs"),
|
||||
),
|
||||
(
|
||||
"settings/appearance.rs",
|
||||
include_str!("../../settings/appearance.rs"),
|
||||
),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn shipped_components_show_no_fixed_english() {
|
||||
let shown = [".tooltip(\"", ".child(\"", "SharedString::from(\""];
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
found.is_empty(),
|
||||
"English outside the catalog:\n{}",
|
||||
found.join("\n")
|
||||
);
|
||||
}
|
||||
@@ -195,7 +195,7 @@ impl RenderOnce for EditorTabs {
|
||||
IconButton::new(("pin", ix), IconName::Pin)
|
||||
.size(ControlSize::Sm)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.tooltip("Unpin")
|
||||
.tooltip(i18n::text(cx, "tabs.unpin", &[]))
|
||||
.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
run(&pin, "unpin", &slot_id, window, cx);
|
||||
|
||||
@@ -223,7 +223,7 @@ impl RenderOnce for FontSizeControl {
|
||||
)
|
||||
.child(
|
||||
IconButton::new((self.id, "reset"), IconName::RotateCcw)
|
||||
.tooltip("Back to the default size")
|
||||
.tooltip(i18n::text(cx, "theme.size.reset", &[]))
|
||||
.disabled(self.size == default)
|
||||
.on_click(move |_, window, cx| {
|
||||
reset(default, window, cx);
|
||||
@@ -237,7 +237,7 @@ impl RenderOnce for FontSizeControl {
|
||||
.min_w_0()
|
||||
.text_size(Pixels::from(self.size))
|
||||
.text_color(theme.colors.fg)
|
||||
.child("The quick brown fox jumps over the lazy dog."),
|
||||
.child(i18n::text(cx, "theme.size.sample", &[])),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use gpui::{
|
||||
};
|
||||
|
||||
use super::{FilterRule, SortKey, Test};
|
||||
use crate::i18n;
|
||||
use crate::{
|
||||
buttons::{Button, ButtonVariant, IconButton, SegmentedControl},
|
||||
forms::{Choice, Input, InputEvent, Select, TextInput},
|
||||
@@ -205,7 +206,7 @@ impl RenderOnce for FilterBuilder {
|
||||
IconButton::new((id.clone(), format!("remove-{ix}")), IconName::X)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip("Remove")
|
||||
.tooltip(i18n::text(cx, "builder.remove", &[]))
|
||||
.on_click(move |_, window, cx| {
|
||||
let mut next = for_remove.clone();
|
||||
next.remove(ix);
|
||||
@@ -377,7 +378,7 @@ impl RenderOnce for SortBuilder {
|
||||
IconButton::new((id.clone(), format!("sort-remove-{ix}")), IconName::X)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip("Remove")
|
||||
.tooltip(i18n::text(cx, "builder.remove", &[]))
|
||||
.on_click(move |_, window, cx| {
|
||||
let mut next = for_remove.clone();
|
||||
next.remove(ix);
|
||||
|
||||
Reference in new issue
Block a user