From 93e4ff10c47bbfafe2b807671465c3abc8e74107 Mon Sep 17 00:00:00 2001 From: Davit Matchakhelidze Date: Fri, 23 Oct 2026 02:23:44 +0400 Subject: [PATCH 1/5] fonts: draw the Georgian interface with the craft-fonts Georgian face MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump craft-fonts-ref to a71c20e3, which adds Noto Sans Georgian (`Geor`), and build the web release with craft-fonts (`web-craft-fonts: true`). The interface loads the embedded `Geor` face after the CJK faces in every language (the picker names ქართული), and the web build embeds it too (232 KB). Without it `can_show("ka")` is false: the saved choice is kept, the interface stays English and the language command says why. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF --- .github/workflows/freebsd.yml | 2 +- .github/workflows/release.yml | 3 +- crates/fonts/build.rs | 10 +++---- crates/fonts/src/craft_fonts.rs | 3 +- crates/ui-egui/src/menus.rs | 12 +++++--- crates/ui-egui/src/theme.rs | 52 +++++++++++++++++++++++++++------ docs/control-protocol.md | 4 +-- docs/localization.md | 7 +++++ 8 files changed, 70 insertions(+), 23 deletions(-) diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index e7a607181..b23a86341 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -26,4 +26,4 @@ jobs: # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos. uses: storytold/craft-common-actions/.github/workflows/freebsd.yml@main with: - craft-fonts-ref: 3d9ab3a132ed0eff457e424226a435a901da30d3 + craft-fonts-ref: a71c20e30af613287c504f7e685ca8fad77752bc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04206b9c8..12fc211ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,8 @@ jobs: with: app-name: cadcraft app-title: CADCraft - craft-fonts-ref: 3d9ab3a132ed0eff457e424226a435a901da30d3 + craft-fonts-ref: a71c20e30af613287c504f7e685ca8fad77752bc + web-craft-fonts: true version: ${{ inputs.version }} secrets: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} diff --git a/crates/fonts/build.rs b/crates/fonts/build.rs index 7eb2a784b..833ba6f63 100644 --- a/crates/fonts/build.rs +++ b/crates/fonts/build.rs @@ -3,9 +3,9 @@ //! empty, `CRAFT_FONTS` is empty and the build is unchanged. It only reads the local checkout //! (no network). A bad checkout is a warning, or an error with `CRAFT_FONTS_REQUIRED=1`. //! -//! On wasm32 it embeds only BIZ UDPGothic Regular and the `Arab` faces (about 5.5 MB), to keep the -//! web build within hosting limits (Cloudflare Pages: 25 MiB per file). The web build has no -//! system fonts, so these are its only fallback for Japanese and Arabic-script text. +//! On wasm32 it embeds only BIZ UDPGothic Regular and the `Arab` and `Geor` faces (about 5.7 MB), +//! to keep the web build within hosting limits (Cloudflare Pages: 25 MiB per file). The web build +//! has no system fonts, so these are its only fallback for Japanese, Arabic-script and Georgian text. use std::fmt::Write as _; use std::path::PathBuf; @@ -50,8 +50,8 @@ fn craft_fonts(dir: &std::path::Path) -> Result { println!("cargo::warning=craft-fonts: not embedding {family} {style} (AGENTS.md: no Adobe fonts)"); continue; } - let arabic = scripts.split(',').any(|s| s.trim() == "Arab"); - let web_face = (*family == "BIZ UDPGothic" && *style == "Regular") || arabic; + let web_script = scripts.split(',').any(|s| matches!(s.trim(), "Arab" | "Geor")); + let web_face = (*family == "BIZ UDPGothic" && *style == "Regular") || web_script; if wasm && !web_face { continue; } diff --git a/crates/fonts/src/craft_fonts.rs b/crates/fonts/src/craft_fonts.rs index fb290775f..713b6632a 100644 --- a/crates/fonts/src/craft_fonts.rs +++ b/crates/fonts/src/craft_fonts.rs @@ -1,7 +1,7 @@ //! Fonts from the optional craft-fonts build input (). //! //! Built with `CRAFT_FONTS_DIR=`, `build.rs` embeds the fonts listed in its -//! `fonts/manifest.txt` (web builds: only BIZ UDPGothic Regular and the Arabic faces). +//! `fonts/manifest.txt` (web builds: only BIZ UDPGothic Regular and the Arabic and Georgian faces). //! Without it [`CRAFT_FONTS`] is empty and nothing changes. The font table ([`crate::ttf`]) //! lists the embedded fonts with the installed ones, under [`CraftFont::stem`]. A text style //! can name one (`BIZ UDPGothic`), and they are fallbacks for characters a text's font lacks. @@ -84,6 +84,7 @@ mod tests { Some("Jpan") => '\u{3042}', Some("Arab") => '\u{0628}', Some("Hang" | "Kore") => '가', + Some("Geor") => 'ა', _ => 'A', }; assert!(face.is_some_and(|x| x.has_glyph(sample)), "{} lacks {sample}", f.family); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index d4ac0d3e6..35e381c7a 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -137,10 +137,14 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option { + "No font covers the Korean interface, so it stays in English. Install a Hangul font (Malgun Gothic or Nanum Gothic), or build with a Hang/Kore craft-fonts face." + } + "ka" => "This build has no Georgian font, so the interface stays in English. Build with craft-fonts (its Geor face) to use Georgian.", + _ => { + "No Chinese font is installed, so the interface stays in English. Install one (Noto Sans CJK, for example) to use Chinese." + } }); } Ok(json!({"interfaceLanguage": preference.code(), "shown": shown})) diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 6c8dd83d0..9105423c1 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -307,7 +307,8 @@ pub fn mono() -> FontId { /// language: file, layout and layer names can be Japanese or Chinese whatever the interface /// shows (#513). In a Chinese interface (`language` "zh…") the Chinese font comes first, else a /// Japanese one, so names take their own glyph forms; in an Arabic interface an installed -/// Arabic font follows. A font embedded from craft-fonts comes +/// Arabic font follows. The embedded Georgian face loads in every language, so the language +/// picker can show ქართული. A font embedded from craft-fonts comes /// last, for systems without any (and the web build, which has no system fonts). A Korean /// interface adds a Hangul font only when the CJK font found doesn't cover Hangul /// ([`korean_font`]). @@ -391,7 +392,8 @@ pub fn install_fonts(ctx: &egui::Context, language: &str) { /// shows (#513), with the interface's own face leading. A sans-serif face reads best, and a /// Chinese interface takes the face with its own glyphs ahead of a Japanese one — a Japanese face /// shares most ideographs but lacks the simplified-only ones (图 园 测), which would be tofu. -/// العربية adds its Arabic face after those, egui's bundled faces carrying no Arabic glyphs. +/// The Georgian face follows in every language (egui's bundled faces carry no Georgian glyphs, and +/// the picker names the language ქართული); العربية adds its Arabic face after those. fn embedded_ui_faces<'a>(faces: &[&'a cadcraft_fonts::CraftFont], language: &str) -> Vec<&'a cadcraft_fonts::CraftFont> { let cjk = |f: &&cadcraft_fonts::CraftFont| f.style == "Regular" && f.scripts.iter().any(|s| matches!(*s, "Jpan" | "Hans" | "Hant")); let mut out: Vec<_> = faces.iter().copied().filter(cjk).collect(); @@ -399,6 +401,7 @@ fn embedded_ui_faces<'a>(faces: &[&'a cadcraft_fonts::CraftFont], language: &str if language.starts_with("zh") { out.sort_by_key(|f| !f.scripts.contains(&"Hans")); } + out.extend(faces.iter().copied().filter(|f| f.style == "Regular" && f.scripts.contains(&"Geor"))); if language == "ar" { out.extend(faces.iter().copied().filter(|f| f.style == "Regular" && f.scripts.contains(&"Arab"))); } @@ -636,11 +639,13 @@ pub fn shows_hangul(language: &str) -> bool { /// Whether the interface can show `language` without missing-glyph boxes: Chinese needs a font /// with its glyphs (installed, or the CJK face this build embedded from craft-fonts), Korean a -/// font with Hangul, العربية an installed Arabic font or the embedded Arabic face. +/// font with Hangul, العربية an installed Arabic font or the embedded Arabic face, ქართული the +/// embedded Georgian face. pub fn can_show(language: &str) -> bool { match language { "ko" => shows_hangul("ko"), "ar" => arabic_font().is_some() || embedded_script("Arab"), + "ka" => embedded_script("Geor"), l if l.starts_with("zh") => cjk_font().is_some() || embedded_script("Hans") || embedded_script("Hant"), _ => true, } @@ -738,13 +743,16 @@ mod tests { let japanese = face("BIZ UDPGothic", &["Jpan", "Latn"]); let chinese = face("Han Test Face", &["Hans", "Latn"]); let arabic = face("Noto Sans Arabic", &["Arab"]); - let faces = [&japanese, &chinese, &arabic]; + let georgian = face("Noto Sans Georgian", &["Geor"]); + let faces = [&japanese, &chinese, &arabic, &georgian]; // Every interface reads CJK names, so both CJK faces load, the sans-serif face first. - assert_eq!(families(&super::embedded_ui_faces(&faces, "en")), ["BIZ UDPGothic", "Han Test Face"]); + // The Georgian face loads in every interface too, after them: the picker shows ქართული. + assert_eq!(families(&super::embedded_ui_faces(&faces, "en")), ["BIZ UDPGothic", "Han Test Face", "Noto Sans Georgian"]); // A Chinese interface takes the Han face first: a Japanese face lacks 图 园 测. - assert_eq!(families(&super::embedded_ui_faces(&faces, "zh")), ["Han Test Face", "BIZ UDPGothic"]); + assert_eq!(families(&super::embedded_ui_faces(&faces, "zh")), ["Han Test Face", "BIZ UDPGothic", "Noto Sans Georgian"]); // العربية adds the Arabic face, after the CJK ones egui's bundled faces have no glyphs for. - assert_eq!(families(&super::embedded_ui_faces(&faces, "ar")), ["BIZ UDPGothic", "Han Test Face", "Noto Sans Arabic"]); + assert_eq!(families(&super::embedded_ui_faces(&faces, "ar")), ["BIZ UDPGothic", "Han Test Face", "Noto Sans Georgian", "Noto Sans Arabic"]); + assert_eq!(families(&super::embedded_ui_faces(&faces, "ka")), ["BIZ UDPGothic", "Han Test Face", "Noto Sans Georgian"]); // Another weight is a style of a face, not an interface face of its own. let bold = CraftFont { family: "BIZ UDPGothic", style: "Bold", scripts: &["Jpan"], bytes: &[] }; assert!(super::embedded_ui_faces(&[&bold], "en").is_empty()); @@ -755,13 +763,39 @@ mod tests { /// leaving the installed-font answer the gate had before. #[test] fn embedded_faces_answer_for_their_scripts() { - let faces = [face("BIZ UDPGothic", &["Jpan", "Latn"]), face("Noto Sans Arabic", &["Arab"])]; - assert!(super::any_script(&faces, "Jpan") && super::any_script(&faces, "Arab")); + let faces = [face("BIZ UDPGothic", &["Jpan", "Latn"]), face("Noto Sans Arabic", &["Arab"]), face("Noto Sans Georgian", &["Geor"])]; + assert!(super::any_script(&faces, "Jpan") && super::any_script(&faces, "Arab") && super::any_script(&faces, "Geor")); assert!(!super::any_script(&faces, "Hans") && !super::any_script(&faces, "Hant")); // A `Bold`-only face is not the interface's, and a script code is matched whole. let bold = CraftFont { family: "Some Han Bold", style: "Bold", scripts: &["Hans"], bytes: &[] }; assert!(!super::any_script(&[bold], "Hans") && !super::any_script(&faces, "Ara")); assert!(!super::embedded_script("Hans") || super::can_show("zh")); + // ქართული rides on the embedded Georgian face alone. + assert_eq!(super::can_show("ka"), super::embedded_script("Geor")); assert!(super::can_show("en") && super::can_show("uk")); } + + /// With the craft-fonts Georgian face (release and web builds), every Mkhedruli letter and the + /// picker's ქართული render in both families, in a Georgian and in an English interface. + #[test] + fn embedded_georgian_face_covers_the_alphabet_in_every_interface() { + if !super::embedded_script("Geor") { + return; + } + let letters: Vec = ('ა'..='ჰ').chain("ქართული".chars()).collect(); + for language in ["ka", "en"] { + let ctx = egui::Context::default(); + super::install_fonts(&ctx, language); + let mut output = ctx.run_ui(Default::default(), |_| {}); + output.textures_delta.clear(); + ctx.fonts_mut(|view| { + for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] { + let mut font = view.fonts.font(&family); + for c in &letters { + assert!(font.characters().contains_key(c), "{language}: missing Georgian glyph {c} in {family:?}"); + } + } + }); + } + } } diff --git a/docs/control-protocol.md b/docs/control-protocol.md index b1e91a52c..168e34b24 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -182,9 +182,9 @@ Select a language with `engine.execute` and the UI command `ui.language`: {"id":1,"method":"engine.execute","params":{"command":"ui.language","params":{"lang":"uk"}}} ``` -Supported values are `auto`, `en`, `uk`, `pt-BR`, `de`, `es`, `cs`, `ru`, `fr`, `zh`, `it`, `ko`, `sv`, `ar` and `hu`. +Supported values are `auto`, `en`, `uk`, `pt-BR`, `de`, `es`, `cs`, `ru`, `fr`, `zh`, `it`, `ko`, `sv`, `ar`, `hu`, `tr`, `el` and `ka`. Chinese needs an installed CJK font, Arabic an installed Arabic font; Korean needs a font covering the Korean interface from the -optional craft-fonts input or the operating system. Without a suitable font, the saved choice is +optional craft-fonts input or the operating system; Georgian needs the craft-fonts Georgian (`Geor`) face. Without a suitable font, the saved choice is retained and the result's `shown` is `en`. `ui.set` also accepts `{"interfaceLanguage":"uk"}`; `ui.inspect` reports the saved choice in its UI state. Invalid choices leave the current language unchanged. Menus, command labels and prompt text diff --git a/docs/localization.md b/docs/localization.md index d3c6cc4e1..ec198bd02 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -98,6 +98,13 @@ catalog, the saved `ko` preference is kept but the interface falls back to Engli command reports that a font is needed, and the picker uses the readable label **Korean** wherever Hangul can't be shown. This change does not fix platform IME issues or localize engine output. +The bundled egui fonts have no Georgian letters. Georgian is drawn with the Noto Sans Georgian face +(`Geor`) of the craft-fonts build input, which release builds embed (`craft-fonts-ref` in the +workflows, and `web-craft-fonts` for the web build, whose embedded faces include it). The face loads +in every interface language, so the picker shows **ქართული**. A build without craft-fonts keeps the +saved `ka` choice but shows the interface in English, names the language **Georgian** in the picker, +and the language command reports that a Georgian font is needed. + ## Greek (`el`) interface The Greek translation covers all **1,330** distinct source keys collected from the existing From d11c418fe608def4d28d8783c075ac8cc2336b4a Mon Sep 17 00:00:00 2001 From: Davit Matchakhelidze Date: Sun, 25 Oct 2026 19:17:07 +0400 Subject: [PATCH 2/5] theme: the Georgian glyph test asks FontsView::has_glyph (egui 0.37 made its fonts field private) Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/theme.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 9627becb4..75dd4f6ac 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -813,9 +813,8 @@ mod tests { output.textures_delta.clear(); ctx.fonts_mut(|view| { for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] { - let mut font = view.fonts.font(&family); for c in &letters { - assert!(font.characters().contains_key(c), "{language}: missing Georgian glyph {c} in {family:?}"); + assert!(view.has_glyph(&egui::FontId::new(12.0, family.clone()), *c), "{language}: missing Georgian glyph {c} in {family:?}"); } } }); From 4cd88a3815009417e159a27dc614a588d55739a9 Mon Sep 17 00:00:00 2001 From: Davit Matchakhelidze Date: Sun, 25 Oct 2026 19:57:06 +0400 Subject: [PATCH 3/5] docs: ka joins the supported ui.language values with the language PR, not this one (CodeRabbit) Co-Authored-By: Claude Opus 5.5 --- docs/control-protocol.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 6e642497a..46e2d379d 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -188,7 +188,7 @@ Select a language with `engine.execute` and the UI command `ui.language`: {"id":1,"method":"engine.execute","params":{"command":"ui.language","params":{"lang":"uk"}}} ``` -Supported values are `auto`, `en`, `uk`, `pt-BR`, `de`, `es`, `cs`, `ru`, `fr`, `zh`, `it`, `ko`, `sv`, `ar`, `hu`, `tr`, `el` and `ka`. +Supported values are `auto`, `en`, `uk`, `pt-BR`, `de`, `es`, `cs`, `ru`, `fr`, `zh`, `it`, `ko`, `sv`, `ar`, `hu`, `tr` and `el`. Chinese needs an installed CJK font, Arabic an installed Arabic font; Korean needs a font covering the Korean interface from the optional craft-fonts input or the operating system; Georgian needs the craft-fonts Georgian (`Geor`) face. Without a suitable font, the saved choice is retained and the result's `shown` is `en`. `ui.set` also accepts From e8408851cddf7e5ef94d69344a77c52e4f4dcde9 Mon Sep 17 00:00:00 2001 From: Davit Matchakhelidze Date: Tue, 3 Nov 2026 11:17:59 +0400 Subject: [PATCH 4/5] docs: the Georgian face paragraph describes glyph support only Choosing ka, its English fallback and the picker name come with the language PR (#2016), which registers ka; this PR alone doesn't. Co-Authored-By: Claude Opus 5.5 --- docs/control-protocol.md | 4 ++-- docs/localization.md | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 91c2982f9..d10d43abb 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -188,9 +188,9 @@ Select a language with `engine.execute` and the UI command `ui.language`: {"id":1,"method":"engine.execute","params":{"command":"ui.language","params":{"lang":"uk"}}} ``` -Supported values are `auto`, `en`, `uk`, `pt-BR`, `de`, `es`, `cs`, `ru`, `fr`, `zh`, `it`, `ko`, `sv`, `ar`, `hu`, `tr` and `el`. +Supported values are `auto`, `en`, `uk`, `pt-BR`, `de`, `es`, `cs`, `ru`, `fr`, `zh`, `it`, `ko`, `sv`, `ar` and `hu`. Chinese needs an installed CJK font, Arabic an installed Arabic font; Korean needs a font covering the Korean interface from the -optional craft-fonts input or the operating system; Georgian needs the craft-fonts Georgian (`Geor`) face. Without a suitable font, the saved choice is +optional craft-fonts input or the operating system. Without a suitable font, the saved choice is retained and the result's `shown` is `en`. `ui.set` also accepts `{"interfaceLanguage":"uk"}`; `ui.inspect` reports the saved choice in its UI state. Invalid choices leave the current language unchanged. Menus, command labels and prompt text diff --git a/docs/localization.md b/docs/localization.md index 1d6a71ae6..4c9baef0c 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -104,9 +104,8 @@ Hangul can't be shown. This change does not fix platform IME issues or localize The bundled egui fonts have no Georgian letters. Georgian is drawn with the Noto Sans Georgian face (`Geor`) of the craft-fonts build input, which release builds embed (`craft-fonts-ref` in the workflows, and `web-craft-fonts` for the web build, whose embedded faces include it). The face loads -in every interface language, so the picker shows **ქართული**. A build without craft-fonts keeps the -saved `ka` choice but shows the interface in English, names the language **Georgian** in the picker, -and the language command reports that a Georgian font is needed. +in every interface language, so Georgian text (file, layout and layer names, labels) draws. A build +without craft-fonts has no Georgian face. ## Greek (`el`) interface From 40f060018ddac180c762e368c2d90f3a370a952b Mon Sep 17 00:00:00 2001 From: Davit Matchakhelidze Date: Thu, 5 Nov 2026 09:01:04 +0400 Subject: [PATCH 5/5] Leave .github/workflows/release.yml to a maintainer CLAUDE.md: no CI changes in a feature or fix PR. The web-craft-fonts line is described in the PR description. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4bfc4ffec..c2cf475e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,7 +69,6 @@ jobs: app-name: cadcraft app-title: CADCraft craft-fonts-ref: b076417b5880bd4154bd22f00695b1d28c6bea83 - web-craft-fonts: true version: ${{ inputs.version }} secrets: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}