i18n: a locale per app, catalogs of messages, mirrored stacks

i18n::I18n is a gpui global holding the shown locale and one catalog of
messages per locale. text fills each {name} from its arguments; a
missing key, argument or catalog fails, and every catalog holds the
first one's keys. Locale writes numbers with its separators and dates
in CLDR's short numeric form; Hebrew replaces Arabic, whose Egyptian
digits Ely does not write. Direction::row mirrors a stack and
Direction::align sets text against the reading side. The gallery's
chapter 40 shows the messages in seven locales, every locale's number
and date, and a file list that mirrors under Hebrew.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-27 18:59:45 -04:00
1 parent 17e3e19fc9
commit ae5f2334f5
9 files changed
+616 -4

No files matched your search

+1
View File
@@ -99,6 +99,7 @@ fn main() -> Result<()> {
.run(move |cx: &mut App| {
ely_gpui_component::init(cx);
pages::bind_keys(cx);
pages::install_i18n(cx);
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
cx.on_action(|_: &Quit, cx| cx.quit());
menus(cx);
+217
View File
@@ -0,0 +1,217 @@
use ely_gpui_component::{
forms::{Choice, Select},
i18n::{I18n, LOCALES},
primitives::{Icon, IconName},
theme::{ActiveTheme, Radius},
};
use gpui::{AnyElement, App, IntoElement, ParentElement, Styled, Window, div, px};
use jiff::{Timestamp, tz::TimeZone};
use super::Page;
use crate::probe::probe;
use crate::script::Step;
use crate::ui::{section, specimen, specimens};
pub const PAGE: Page = Page {
number: 40,
slug: "i18n",
title: "i18n & a11y",
summary: "Messages, numbers and dates in the reader's locale. A right-to-left locale mirrors its stacks.",
render,
script: &[
Step::Click("locale"),
Step::Key("end"),
Step::Key("enter"),
Step::Wait(300),
Step::Shot("right-to-left"),
Step::Click("locale"),
Step::Key("home"),
Step::Key("enter"),
Step::Wait(300),
],
};
/// 1 September 2026, when the demo's folder was saved.
const SAVED: i64 = 1_788_220_800;
/// The gallery's messages, one catalog per listed locale.
pub fn install(cx: &mut App) {
cx.set_global(
I18n::new("en-US")
.catalog(
"en-US",
&[
("welcome", "Welcome back, {name}"),
("files", "{count} files in {folder}"),
("saved", "Last saved {date}"),
],
)
.catalog(
"en-GB",
&[
("welcome", "Welcome back, {name}"),
("files", "{count} files in {folder}"),
("saved", "Last saved {date}"),
],
)
.catalog(
"de-DE",
&[
("welcome", "Willkommen zurück, {name}"),
("files", "{count} Dateien in {folder}"),
("saved", "Zuletzt gespeichert am {date}"),
],
)
.catalog(
"fr-FR",
&[
("welcome", "Bon retour, {name}"),
("files", "{count} fichiers dans {folder}"),
("saved", "Enregistré le {date}"),
],
)
.catalog(
"ja-JP",
&[
("welcome", "おかえりなさい、{name}さん"),
("files", "{folder}に{count}個のファイル"),
("saved", "最終保存:{date}"),
],
)
.catalog(
"zh-CN",
&[
("welcome", "欢迎回来,{name}"),
("files", "{folder}中有{count}个文件"),
("saved", "上次保存于{date}"),
],
)
.catalog(
"he-IL",
&[
("welcome", "ברוך שובך, {name}"),
("files", "{count} קבצים ב־{folder}"),
("saved", "נשמר לאחרונה ב־{date}"),
],
),
);
}
fn render(_: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(messages(cx))
.child(formats(cx))
.child(mirrored(cx))
.into_any_element()
}
fn messages(cx: &App) -> impl IntoElement + use<> {
let i18n = cx.global::<I18n>();
let locale = i18n.locale();
let saved = Timestamp::from_second(SAVED).expect("a time");
let lines = [
i18n.text("welcome", &[("name", "Ada")]),
i18n.text(
"files",
&[("count", &locale.number(1204.0, 0)), ("folder", "Docs")],
),
i18n.text("saved", &[("date", &locale.date(saved, &TimeZone::UTC))]),
];
section(
"I18nProvider · Trans / LocalizedText",
"One locale for the app, a catalog of messages per locale. Every catalog holds the same keys; a missing key or argument fails.",
cx,
)
.child(
specimens()
.child(specimen(
"locale",
probe(
"locale",
div().w(px(240.0)).child(
Select::new(
"locale",
LOCALES.map(|locale| Choice::new(locale.tag, locale.name)),
)
.selected(locale.tag)
.on_change(|tag, _, cx| I18n::set_locale(tag, cx)),
),
),
cx,
))
.child(specimen(
"messages",
locale.direction.align(
div()
.w(px(280.0))
.flex()
.flex_col()
.gap_1()
.children(lines.map(|line| div().child(line))),
),
cx,
)),
)
}
fn formats(cx: &App) -> impl IntoElement + use<> {
let theme = cx.theme();
let saved = Timestamp::from_second(SAVED).expect("a time");
let rows = LOCALES.map(|locale| {
div()
.flex()
.flex_wrap()
.gap_x_4()
.child(
div()
.w(px(96.0))
.text_color(theme.colors.fg_muted)
.child(locale.name),
)
.child(div().w(px(120.0)).child(locale.number(-1234567.891, 2)))
.child(div().child(locale.date(saved, &TimeZone::UTC)))
});
section(
"LocaleNumber / LocaleDate",
"Each locale's separators and its short numeric date, from CLDR.",
cx,
)
.child(div().flex().flex_col().gap_1().children(rows))
}
fn mirrored(cx: &App) -> impl IntoElement + use<> {
let theme = cx.theme();
let direction = cx.global::<I18n>().locale().direction;
let file = |name: &'static str, size: &'static str| {
direction
.row(div())
.items_center()
.gap_2()
.px_3()
.py_2()
.child(Icon::new(IconName::File).color(theme.colors.fg_muted))
.child(direction.align(div().flex_1().min_w_0().child(name)))
.child(
div()
.flex_none()
.text_color(theme.colors.fg_muted)
.child(size),
)
};
section(
"RTL layout",
"A right-to-left locale starts its stacks and sets its text at the right; pick עברית above. Ely's other components stay left to right.",
cx,
)
.child(
div()
.max_w(px(320.0))
.flex()
.flex_col()
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(theme.colors.border)
.child(file("Docs", "12 KB"))
.child(file("Photos", "3.1 MB")),
)
}
+3
View File
@@ -18,6 +18,7 @@ mod finance;
mod forms;
mod generative;
mod git;
mod i18n;
mod interaction;
mod layout;
mod lists;
@@ -92,8 +93,10 @@ pub const ALL: &[Page] = &[
onboarding::PAGE,
interaction::PAGE,
theme::PAGE,
i18n::PAGE,
];
pub use i18n::install as install_i18n;
pub use interaction::bind_keys;
pub use shell::{open_about, open_managed};