GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(text): font systems are built with the user's locale, not "en-US"
All four FontSystem constructors took the literal "en-US"; cosmic-text
orders its font fallback by the locale, so a Japanese user drew the
Chinese variants of shared Han glyphs. They take cce_core::locale()
(re-exported as cce_ui::locale): LC_ALL, else LC_CTYPE, else LANG, as a
BCP 47 tag. A page has no environment, so the browser shell hands over
navigator.language through locale::set_locale before the first font
system. Nothing changes in an en_US session.
every_font_system_is_built_with_the_users_locale is the test (run under
en_US and LANG=ja_JP.UTF-8). The RFC marks phase 0 done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 9 +++++++--
docs/rfc-accessibility-locale.md | 7 ++++++-
src/backend/frame.rs | 2 +-
src/backend/text.rs | 4 ++--
src/lib.rs | 16 +++++++++++++++-
src/web/shell.rs | 5 +++++
6 files changed, 36 insertions(+), 7 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index d27fd6c..063c1b8 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1564,8 +1564,8 @@ the ladder should be supplying, and the audit counts them.
## Accessibility and locale are on the roadmap (read `docs/rfc-accessibility-locale.md`)
-There is no accessibility support yet (no AT-SPI, AccessKit, NSAccessibility or ARIA), the
-locale is the literal `"en-US"`, and the editors assume left-to-right text (`TextBox`
+There is no accessibility support yet (no AT-SPI, AccessKit, NSAccessibility or ARIA), no
+message catalogue, and the editors assume left-to-right text (`TextBox`
assumes monospace too). The RFC has the measured state and a phased plan. Until it lands,
two rules keep the retrofit cheap:
@@ -1575,6 +1575,11 @@ two rules keep the retrofit cheap:
identity today and hosts match on it, so translating any label breaks its action; do not
add more code that matches on displayed text.
+Phase 0 is done (2026-10-08): every font system is built with `locale::locale()` (from
+`cce-core`): `LC_ALL`, else `LC_CTYPE`, else `LANG`, as a BCP 47 tag; in the browser,
+`navigator.language` through `locale::set_locale` before the first font system.
+`every_font_system_is_built_with_the_users_locale` is the test.
+
## The `scene/` core rebuild (read `docs/rfc-core-rebuild.md` before touching it)
`src/scene/` is a **retained scene graph being grown additively** to replace three overlaid legacy
diff --git a/docs/rfc-accessibility-locale.md b/docs/rfc-accessibility-locale.md
index 8ff6187..4d1d58b 100644
--- a/docs/rfc-accessibility-locale.md
+++ b/docs/rfc-accessibility-locale.md
@@ -65,7 +65,12 @@ the cheapest moment there will be.
## 3. The plan
-### Phase 0 — the locale comes from the user (small; do first)
+### Phase 0 — the locale comes from the user (small; do first) — DONE 2026-10-08
+
+Done as written below: `cce_core::locale` (re-exported as `cce_ui::locale`), used by all four
+constructors, and the browser shell sets it from `navigator.language` before the first font
+system. Tests: `locale::tests` in cce-core (the parsing and the environment's order) and
+`every_font_system_is_built_with_the_users_locale` in cce-ui.
One `cce_core::locale()`: `LC_ALL`, else `LC_CTYPE`, else `LANG`, turned from POSIX form
(`ja_JP.UTF-8`) into a BCP 47 tag (`ja-JP`), falling back to `en-US`; in the browser,
diff --git a/src/backend/frame.rs b/src/backend/frame.rs
index 2a3919d..586ca49 100644
--- a/src/backend/frame.rs
+++ b/src/backend/frame.rs
@@ -530,7 +530,7 @@ mod tests {
}
fn fonts() -> FontSystem {
- FontSystem::new_with_locale_and_db("en-US".into(), cosmic_text::fontdb::Database::new())
+ FontSystem::new_with_locale_and_db(crate::locale::locale().into(), cosmic_text::fontdb::Database::new())
}
const SIZE: LogicalSize = LogicalSize { width: 100.0, height: 50.0 };
diff --git a/src/backend/text.rs b/src/backend/text.rs
index f414ea2..fbd7687 100644
--- a/src/backend/text.rs
+++ b/src/backend/text.rs
@@ -1093,7 +1093,7 @@ mod text_cache_tests {
let mut db = Database::new();
db.load_font_file(&a).unwrap();
db.load_font_file(&b).unwrap();
- FontSystem::new_with_locale_and_db("en-US".into(), db)
+ FontSystem::new_with_locale_and_db(crate::locale::locale().into(), db)
};
let (mut one, mut two) = (system(), system());
let (fam_a, fam_b) = (face_family(&a, 0), face_family(&b, 0));
@@ -1147,7 +1147,7 @@ mod text_cache_tests {
let system = |file: &Path| {
let mut db = Database::new();
db.load_font_file(file).unwrap();
- FontSystem::new_with_locale_and_db("en-US".into(), db)
+ FontSystem::new_with_locale_and_db(crate::locale::locale().into(), db)
};
let faces = |fs: &FontSystem| {
fs.db()
diff --git a/src/lib.rs b/src/lib.rs
index 9b3237d..560947e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -32,6 +32,8 @@ pub use cce_core::ipc;
#[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
pub mod mcp;
pub use cce_core::motion;
+/// The user's locale (`locale::locale()`), what every font system is built with.
+pub use cce_core::locale;
pub mod text_input;
#[cfg(not(target_arch = "wasm32"))]
pub mod vk;
@@ -442,7 +444,7 @@ fn build_font_system(load_system_fonts: bool) -> cosmic_text::FontSystem {
#[cfg(target_arch = "wasm32")]
page_fonts::stand_in_for_missing(&mut db);
- cosmic_text::FontSystem::new_with_locale_and_db("en-US".to_string(), db)
+ cosmic_text::FontSystem::new_with_locale_and_db(crate::locale::locale().to_string(), db)
}
/// The fonts a page hands the browser shell (`web::run`). A page has no font
@@ -565,3 +567,15 @@ pub(crate) mod page_fonts {
}
}
}
+
+#[cfg(test)]
+mod locale_tests {
+ /// The font systems shape with the user's locale, not a literal: what
+ /// cosmic-text orders its fallback by (rfc-accessibility-locale, phase 0).
+ #[test]
+ fn every_font_system_is_built_with_the_users_locale() {
+ let want = crate::locale::locale();
+ assert_eq!(crate::create_font_system().locale(), want);
+ assert_eq!(crate::geometry_font_system().lock().unwrap().locale(), want);
+ }
+}
diff --git a/src/web/shell.rs b/src/web/shell.rs
index 37536bc..73399bb 100644
--- a/src/web/shell.rs
+++ b/src/web/shell.rs
@@ -112,6 +112,11 @@ pub async fn run<A: Application>(canvas: HtmlCanvasElement, fonts: Fonts, sizing
// Every font database the toolkit builds from here on — this one, its
// own for widget geometry, and the measuring one — loads the page's.
crate::page_fonts::provide(fonts.files, fonts.serif, fonts.sans_serif, fonts.monospace);
+ // A page has no environment: its locale is the browser's, and it must be
+ // decided before the first font system is built from it.
+ if let Some(lang) = web_sys::window().and_then(|w| w.navigator().language()) {
+ crate::locale::set_locale(&lang);
+ }
let fs = crate::create_font_system();
let mut renderer = WebRenderer::new(canvas.clone()).await?;