a11y: a skip link, color-blind safe charts
i18n::SkipLink holds a Tab stop in flow at zero size, first where it sits, and while focused raises a pill over the page; Enter, Space or a press hands focus to its target. gpui orders stops by tab_index before paint and registers none for an invisible element, so the stop stays in flow and only the pill is raised. Theme::set_color_blind_safe swaps 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, and red against green shows the measure bites. KeyboardNavigationHelper points to interaction::RovingFocus. The announcer, labels and screen-reader text stay blocked: gpui 0.2.2 has no accessibility tree, and nothing here is trusted to observe VoiceOver. The i18n tests move under src/i18n/tests.
This commit is contained in:
1 parent
aa39fba87f
commit
1ac5c04fb0
10 files changed
+463
-7
No files matched your search
@@ -0,0 +1,220 @@
|
||||
use jiff::{Timestamp, tz::TimeZone};
|
||||
|
||||
use super::{Direction, I18n, LOCALES, Locale, fill};
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
mod a11y;
|
||||
|
||||
fn i18n() -> I18n {
|
||||
I18n::new("en-US")
|
||||
.catalog(
|
||||
"en-US",
|
||||
&[("files", "{count} files in {folder}"), ("hi", "Hello")],
|
||||
)
|
||||
.catalog(
|
||||
"de-DE",
|
||||
&[("files", "{count} Dateien in {folder}"), ("hi", "Hallo")],
|
||||
)
|
||||
}
|
||||
|
||||
/// Each `{name}` takes its argument, in the order the message puts them.
|
||||
#[test]
|
||||
fn a_message_fills_its_arguments() {
|
||||
let args = [("folder", "Docs"), ("count", "1,204")];
|
||||
assert_eq!(i18n().text("files", &args), "1,204 files in Docs");
|
||||
assert_eq!(i18n().text("hi", &[]), "Hello");
|
||||
assert_eq!(fill("{a}{a}", &[("a", "x")]), Ok("xx".into()));
|
||||
assert_eq!(fill("{a", &[("a", "x")]), Err("a { without }".into()));
|
||||
assert_eq!(fill("{b}", &[("a", "x")]), Err("no argument \"b\"".into()));
|
||||
}
|
||||
|
||||
/// A right-to-left message leads with its mark, so one that starts with Latin letters or a signed number still reads right to left.
|
||||
#[test]
|
||||
fn a_right_to_left_message_leads_with_its_mark() {
|
||||
let he = I18n::new("he-IL").catalog(
|
||||
"he-IL",
|
||||
&[("change", "{change}% מאז אתמול"), ("sent", "{name} שלח")],
|
||||
);
|
||||
let change = Locale::of("he-IL").number(-3.5, 1);
|
||||
assert_eq!(
|
||||
he.text("change", &[("change", &change)]),
|
||||
"\u{200f}\u{200e}−3.5% מאז אתמול"
|
||||
);
|
||||
assert_eq!(he.text("sent", &[("name", "Ada")]), "\u{200f}Ada שלח");
|
||||
assert_eq!(
|
||||
i18n().text("hi", &[]),
|
||||
"Hello",
|
||||
"a left-to-right message takes no mark"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "\"files\" in en-US: no argument \"folder\"")]
|
||||
fn a_missing_argument_fails() {
|
||||
i18n().text("files", &[("count", "3")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "no message \"bye\" in en-US")]
|
||||
fn a_missing_key_fails() {
|
||||
i18n().text("bye", &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "de-DE misses [\"hi\"] from en-US")]
|
||||
fn a_catalog_missing_a_key_fails() {
|
||||
I18n::new("en-US")
|
||||
.catalog("en-US", &[("hi", "Hello"), ("files", "{count} files")])
|
||||
.catalog("de-DE", &[("files", "{count} Dateien")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "de-DE has [\"bye\"], which en-US lacks")]
|
||||
fn a_catalog_with_a_stray_key_fails() {
|
||||
I18n::new("en-US")
|
||||
.catalog("en-US", &[("hi", "Hello")])
|
||||
.catalog("de-DE", &[("hi", "Hallo"), ("bye", "Tschüss")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "en-US repeats a key")]
|
||||
fn a_repeated_key_fails() {
|
||||
I18n::new("en-US").catalog("en-US", &[("hi", "Hello"), ("hi", "Hey")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "en-US has a catalog")]
|
||||
fn a_second_catalog_for_a_locale_fails() {
|
||||
I18n::new("en-US")
|
||||
.catalog("en-US", &[("hi", "Hello")])
|
||||
.catalog("en-US", &[("hi", "Hey")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "no locale \"xx-XX\"")]
|
||||
fn an_unlisted_locale_fails() {
|
||||
Locale::of("xx-XX");
|
||||
}
|
||||
|
||||
/// Separators and the date's order follow each locale.
|
||||
#[test]
|
||||
fn locales_write_their_own_numbers_and_dates() {
|
||||
let at = Timestamp::from_second(1_788_220_800).unwrap();
|
||||
let cases: Vec<_> = LOCALES
|
||||
.iter()
|
||||
.map(|locale| {
|
||||
(
|
||||
locale.tag,
|
||||
locale.number(-1234567.891, 2),
|
||||
locale.date(at, &TimeZone::UTC),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
cases,
|
||||
[
|
||||
("en-US", "−1,234,567.89".into(), "9/1/26".into()),
|
||||
("en-GB", "−1,234,567.89".into(), "01/09/2026".into()),
|
||||
("de-DE", "−1.234.567,89".into(), "01.09.26".into()),
|
||||
(
|
||||
"fr-FR",
|
||||
"−1\u{202f}234\u{202f}567,89".into(),
|
||||
"01/09/2026".into()
|
||||
),
|
||||
("ja-JP", "−1,234,567.89".into(), "2026/09/01".into()),
|
||||
("zh-CN", "−1,234,567.89".into(), "2026/9/1".into()),
|
||||
("he-IL", "\u{200e}−1,234,567.89".into(), "1.9.2026".into()),
|
||||
]
|
||||
);
|
||||
let rtl: Vec<_> = LOCALES
|
||||
.iter()
|
||||
.filter(|locale| locale.direction == Direction::Rtl)
|
||||
.collect();
|
||||
assert_eq!(rtl, [&Locale::of("he-IL")]);
|
||||
assert_eq!(
|
||||
Locale::of("he-IL").number(3.5, 1),
|
||||
"3.5",
|
||||
"a gain takes no mark"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
mod shown {
|
||||
use gpui::{
|
||||
Context, InteractiveElement, IntoElement, ParentElement, Render, Styled, TestAppContext,
|
||||
Window, div, px,
|
||||
};
|
||||
|
||||
use super::super::{Direction, I18n};
|
||||
|
||||
/// A row of two marks, laid out the way `I18n`'s locale reads.
|
||||
struct Row;
|
||||
|
||||
impl Render for Row {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let i18n = cx.global::<I18n>();
|
||||
let direction = i18n.locale().direction;
|
||||
direction
|
||||
.row(div().w(px(280.0)).justify_between())
|
||||
.child(
|
||||
div()
|
||||
.debug_selector(|| "first".into())
|
||||
.w(px(40.0))
|
||||
.child(i18n.text("hi", &[])),
|
||||
)
|
||||
.child(div().debug_selector(|| "second".into()).w(px(40.0)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A new locale reaches every window, and a right-to-left one starts the row at the right.
|
||||
#[gpui::test]
|
||||
fn a_right_to_left_locale_mirrors_the_row(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
cx.set_global(
|
||||
I18n::new("en-US")
|
||||
.catalog("en-US", &[("hi", "Hi")])
|
||||
.catalog("he-IL", &[("hi", "שלום")]),
|
||||
)
|
||||
});
|
||||
let (_, cx) = cx.add_window_view(|_, _| Row);
|
||||
cx.run_until_parked();
|
||||
let first = cx.debug_bounds("first").expect("the first mark");
|
||||
let second = cx.debug_bounds("second").expect("the second mark");
|
||||
assert!(first.left() < second.left(), "left to right in en-US");
|
||||
cx.update(|_, cx| I18n::set_locale("he-IL", cx));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
cx.read(|cx| cx.global::<I18n>().text("hi", &[])),
|
||||
"\u{200f}שלום"
|
||||
);
|
||||
let first = cx.debug_bounds("first").expect("the first mark");
|
||||
let second = cx.debug_bounds("second").expect("the second mark");
|
||||
assert!(
|
||||
first.left() > second.left(),
|
||||
"right to left in he-IL: {first:?} {second:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[should_panic(expected = "no catalog for de-DE")]
|
||||
fn a_locale_without_a_catalog_fails(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
cx.set_global(I18n::new("en-US").catalog("en-US", &[("hi", "Hi")]));
|
||||
I18n::set_locale("de-DE", cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_sits_against_the_reading_side() {
|
||||
let side = |direction: Direction| {
|
||||
direction
|
||||
.align(div())
|
||||
.style()
|
||||
.text
|
||||
.as_ref()
|
||||
.and_then(|text| text.text_align)
|
||||
};
|
||||
assert_eq!(side(Direction::Ltr), Some(gpui::TextAlign::Left));
|
||||
assert_eq!(side(Direction::Rtl), Some(gpui::TextAlign::Right));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user