diff --git a/crates/text/build.rs b/crates/text/build.rs index fec5fb51a..796079968 100644 --- a/crates/text/build.rs +++ b/crates/text/build.rs @@ -3,25 +3,31 @@ //! `CRAFT_FONTS` is empty and native apps use system fonts. Required web releases enforce Inter //! Regular/SemiBold and Noto Sans Georgian Regular after filtering. Recipe: craft-fonts `docs/integration.md`. //! Noto Sans CJK only with `FIGCRAFT_EMBED_CJK` set (the web build's only way to show CJK UI text). +//! The other scripts' faces (Noto Sans Arabic, Hebrew, Thai, Devanagari…) stay out of web builds unless +//! `FIGCRAFT_EMBED_SCRIPTS` names their script tags (`Arab,Hebr,Thai,Deva`: one tag per script wanted, +//! as the manifest's scripts column lists them; #1902). //! `FIGCRAFT_WEB_FONTS` (comma-separated families) adds manifest families to the web build. use std::fmt::Write as _; use std::path::PathBuf; /// Font build script: emit `craft_fonts.rs` from `input_fonts`, passing the -/// `CRAFT_FONTS_DIR`, `CRAFT_FONTS_REQUIRED`, wasm target, `FIGCRAFT_EMBED_CJK` and `FIGCRAFT_WEB_FONTS` inputs; +/// `CRAFT_FONTS_DIR`, `CRAFT_FONTS_REQUIRED`, wasm target, `FIGCRAFT_EMBED_CJK`, `FIGCRAFT_EMBED_SCRIPTS` and +/// `FIGCRAFT_WEB_FONTS` inputs; /// report generation and write failures with `cargo::error`. fn main() { println!("cargo::rerun-if-env-changed=CRAFT_FONTS_DIR"); println!("cargo::rerun-if-env-changed=CRAFT_FONTS_REQUIRED"); println!("cargo::rerun-if-env-changed=FIGCRAFT_EMBED_CJK"); + println!("cargo::rerun-if-env-changed=FIGCRAFT_EMBED_SCRIPTS"); println!("cargo::rerun-if-env-changed=FIGCRAFT_WEB_FONTS"); let mut src = String::from("pub static CRAFT_FONTS: &[CraftFont] = &[\n"); let dir = std::env::var_os("CRAFT_FONTS_DIR").map(PathBuf::from); let required = std::env::var_os("CRAFT_FONTS_REQUIRED").is_some(); let wasm = std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32"); let embed_cjk = std::env::var_os("FIGCRAFT_EMBED_CJK").is_some(); + let embed_scripts = scripts_from(std::env::var("FIGCRAFT_EMBED_SCRIPTS").ok().as_deref()); let web_fonts = web_fonts(&std::env::var("FIGCRAFT_WEB_FONTS").unwrap_or_default()); - match input_fonts(dir.as_deref(), wasm, required, embed_cjk, &web_fonts) { + match input_fonts(dir.as_deref(), wasm, required, embed_cjk, &embed_scripts, &web_fonts) { Ok(entries) => src.push_str(&entries), Err(e) => println!("cargo::error={e}"), } @@ -32,6 +38,12 @@ fn main() { } } +/// The script tags `FIGCRAFT_EMBED_SCRIPTS` names (comma- or space-separated), trimmed; none when +/// it is unset. +fn scripts_from(var: Option<&str>) -> Vec { + var.unwrap_or_default().split([',', ' ']).map(str::trim).filter(|s| !s.is_empty()).map(str::to_owned).collect() +} + /// The families `FIGCRAFT_WEB_FONTS` names: comma-separated, trimmed, empty names dropped. fn web_fonts(list: &str) -> Vec { list.split(',').map(str::trim).filter(|f| !f.is_empty()).map(str::to_owned).collect() @@ -43,8 +55,16 @@ fn not_embedded<'a>(web_fonts: &'a [String], faces: &[(String, String)]) -> Vec< } /// Collect the effective catalog and enforce the required web release faces after filtering. -/// Families named in `web_fonts` that nothing embedded get a warning (most likely misspelt). -fn input_fonts(dir: Option<&std::path::Path>, wasm: bool, required: bool, embed_cjk: bool, web_fonts: &[String]) -> Result { +/// `embed_scripts`: script tags whose faces a web build keeps as well. Families named in `web_fonts` +/// that nothing embedded get a warning (most likely misspelt). +fn input_fonts( + dir: Option<&std::path::Path>, + wasm: bool, + required: bool, + embed_cjk: bool, + embed_scripts: &[String], + web_fonts: &[String], +) -> Result { let Some(dir) = dir else { for family in web_fonts { println!("cargo::warning=FIGCRAFT_WEB_FONTS: {family:?} isn't embedded: this build has no craft-fonts"); @@ -60,7 +80,7 @@ fn input_fonts(dir: Option<&std::path::Path>, wasm: bool, required: bool, embed_ watch(&path); continue; } - match craft_fonts(dir, manifest, wasm, embed_cjk, web_fonts) { + match craft_fonts(dir, manifest, wasm, embed_cjk, embed_scripts, web_fonts) { Ok(catalog) => { loaded = true; out.push_str(&catalog.entries); @@ -106,9 +126,16 @@ struct Catalog { /// Parse a font manifest of `family | style | file | scripts` rows into `CraftFont` /// entries: skip Adobe-authored families, skip CJK unless requested, keep the web catalog -/// to its budgeted families and the ones `web_fonts` adds, canonicalize and watch each face, and -/// emit `include_bytes!` items. -fn craft_fonts(dir: &std::path::Path, manifest_rel: &str, wasm: bool, embed_cjk: bool, web_fonts: &[String]) -> Result { +/// to its budgeted families (plus the faces of the scripts in `embed_scripts` and the families `web_fonts` +/// adds), canonicalize and watch each face, and emit `include_bytes!` items. +fn craft_fonts( + dir: &std::path::Path, + manifest_rel: &str, + wasm: bool, + embed_cjk: bool, + embed_scripts: &[String], + web_fonts: &[String], +) -> Result { let manifest = dir.join(manifest_rel); watch(&manifest); let text = std::fs::read_to_string(&manifest).map_err(|e| format!("{}: {e}", manifest.display()))?; @@ -129,11 +156,14 @@ fn craft_fonts(dir: &std::path::Path, manifest_rel: &str, wasm: bool, embed_cjk: continue; } // Web builds have a size budget: Inter, Noto Sans Georgian (small, for the Georgian UI), only the - // SemiBold wordmark, optionally Noto Sans CJK (every CJK script, for CJK UI text), and the + // SemiBold wordmark, optionally Noto Sans CJK (every CJK script, for CJK UI text), the faces + // of the scripts `FIGCRAFT_EMBED_SCRIPTS` names (an Arabic or Persian document needs Noto Sans + // Arabic, +0.8 MB; the browser has no system fonts to fall back on, #1902), and the // families a distribution names in `FIGCRAFT_WEB_FONTS` (a Thai face for Thai text, say). let wordmark = *family == "Archivo" && *style == "SemiBold"; + let asked = scripts.split(',').map(str::trim).any(|s| embed_scripts.iter().any(|e| e.eq_ignore_ascii_case(s))); let added = web_fonts.iter().any(|f| f == family); - if wasm && !matches!(*family, "Inter" | "Noto Sans Georgian") && !wordmark && !cjk && !added { + if wasm && !matches!(*family, "Inter" | "Noto Sans Georgian") && !wordmark && !cjk && !asked && !added { continue; } let path = dir.join(file).canonicalize().map_err(|e| format!("{file}: {e}"))?; @@ -187,9 +217,9 @@ mod tests { fn restored_latin_catalog_preserves_permitted_native_faces_and_web_budget() { let f = Fixture::new(); std::fs::write(f.0.join("fonts/latin-manifest.txt"), include_str!("tests/fixtures/latin-families.txt")).unwrap(); - let native = craft_fonts(&f.0, "fonts/latin-manifest.txt", false, false, &[]).unwrap(); + let native = craft_fonts(&f.0, "fonts/latin-manifest.txt", false, false, &[], &[]).unwrap(); assert_eq!(native.faces.len(), 43, "all prior permitted presentation faces remain available"); - let web = craft_fonts(&f.0, "fonts/latin-manifest.txt", true, false, &[]).unwrap(); + let web = craft_fonts(&f.0, "fonts/latin-manifest.txt", true, false, &[], &[]).unwrap(); assert_eq!(web.faces.len(), 4, "the web catalog keeps only the four Inter faces"); for catalog in [&native, &web] { assert!(!catalog.entries.contains("Source Sans 3")); @@ -197,16 +227,16 @@ mod tests { } assert!(native.faces.iter().any(|(family, _)| family == "Carlito")); assert!(native.faces.iter().any(|(family, _)| family == "Liberation Serif")); - assert!(input_fonts(Some(&f.0), true, true, false, &[]).is_ok()); + assert!(input_fonts(Some(&f.0), true, true, false, &[], &[]).is_ok()); } /// Asserts required web input rejects a catalog missing Inter Regular or Inter SemiBold. #[test] fn required_web_rejects_core_only_and_partial_latin_catalogs() { let f = Fixture::new(); - assert!(input_fonts(Some(&f.0), true, true, false, &[]).unwrap_err().contains("Inter Regular")); + assert!(input_fonts(Some(&f.0), true, true, false, &[], &[]).unwrap_err().contains("Inter Regular")); f.manifest("latin-manifest.txt", &[("Inter", "Regular"), ("Unrelated", "SemiBold")]); - assert!(input_fonts(Some(&f.0), true, true, false, &[]).unwrap_err().contains("Inter SemiBold")); + assert!(input_fonts(Some(&f.0), true, true, false, &[], &[]).unwrap_err().contains("Inter SemiBold")); } /// Asserts required web input embeds the Inter and Georgian catalogs (three entries) and @@ -215,12 +245,12 @@ mod tests { fn required_web_embeds_both_catalogs_and_requires_georgian() { let f = Fixture::new(); f.manifest("latin-manifest.txt", &[("Inter", "Regular"), ("Inter", "SemiBold")]); - let entries = input_fonts(Some(&f.0), true, true, false, &[]).unwrap(); + let entries = input_fonts(Some(&f.0), true, true, false, &[], &[]).unwrap(); assert_eq!(entries.matches("CraftFont {").count(), 3); assert!(entries.contains("family: \"Inter\"")); assert!(entries.contains("family: \"Noto Sans Georgian\"")); f.manifest("manifest.txt", &[("Other", "Regular")]); - assert!(input_fonts(Some(&f.0), true, true, false, &[]).unwrap_err().contains("Noto Sans Georgian Regular")); + assert!(input_fonts(Some(&f.0), true, true, false, &[], &[]).unwrap_err().contains("Noto Sans Georgian Regular")); } /// Asserts absent font input is empty when optional and an error when required, while present @@ -228,10 +258,10 @@ mod tests { #[test] fn optional_and_native_inputs_keep_their_existing_policy() { let f = Fixture::new(); - assert!(input_fonts(None, true, false, false, &[]).unwrap().is_empty()); - assert!(input_fonts(None, true, true, false, &[]).unwrap_err().contains("CRAFT_FONTS_DIR is missing")); - assert!(input_fonts(Some(&f.0), true, false, false, &[]).unwrap().contains("Noto Sans Georgian")); - assert!(input_fonts(Some(&f.0), false, true, false, &[]).unwrap().contains("Noto Sans Georgian")); + assert!(input_fonts(None, true, false, false, &[], &[]).unwrap().is_empty()); + assert!(input_fonts(None, true, true, false, &[], &[]).unwrap_err().contains("CRAFT_FONTS_DIR is missing")); + assert!(input_fonts(Some(&f.0), true, false, false, &[], &[]).unwrap().contains("Noto Sans Georgian")); + assert!(input_fonts(Some(&f.0), false, true, false, &[], &[]).unwrap().contains("Noto Sans Georgian")); } /// Asserts required web input rejects a manifest whose face file is missing, even when every @@ -241,7 +271,34 @@ mod tests { let f = Fixture::new(); f.manifest("latin-manifest.txt", &[("Inter", "Regular"), ("Inter", "SemiBold")]); std::fs::remove_file(f.0.join("fonts/face.ttf")).unwrap(); - assert!(input_fonts(Some(&f.0), true, true, false, &[]).unwrap_err().contains("fonts/face.ttf")); + assert!(input_fonts(Some(&f.0), true, true, false, &[], &[]).unwrap_err().contains("fonts/face.ttf")); + } + + /// #1902: a web build leaves the other scripts' faces out (an Arabic or Persian document shows + /// missing-glyph boxes) unless `FIGCRAFT_EMBED_SCRIPTS` names their script tags; native builds + /// keep every face, as before. + #[test] + fn web_embeds_other_scripts_faces_only_when_asked() { + let f = Fixture::new(); + std::fs::write( + f.0.join("fonts/manifest.txt"), + "Noto Sans Georgian | Regular | fonts/face.ttf | Geor\nNoto Sans Arabic | Regular | fonts/face.ttf | Arab\nNoto Sans Hebrew | Regular | fonts/face.ttf | Hebr\nNoto Sans Symbols | Regular | fonts/face.ttf | Zsym,Latn\n", + ) + .unwrap(); + let families = |c: &Catalog| c.faces.iter().map(|(f, _)| f.clone()).collect::>(); + let web = craft_fonts(&f.0, "fonts/manifest.txt", true, false, &[], &[]).unwrap(); + assert_eq!(families(&web), ["Noto Sans Georgian"], "the web budget: Georgian only"); + let arabic = craft_fonts(&f.0, "fonts/manifest.txt", true, false, &["Arab".to_string()], &[]).unwrap(); + assert_eq!(families(&arabic), ["Noto Sans Georgian", "Noto Sans Arabic"]); + // Several tags, any case, spaces, and a tag among a face's several scripts. + let more = craft_fonts(&f.0, "fonts/manifest.txt", true, false, &scripts_from(Some(" hebr, LATN ")), &[]).unwrap(); + assert_eq!(families(&more), ["Noto Sans Georgian", "Noto Sans Hebrew", "Noto Sans Symbols"]); + // Native builds keep every face whatever the variable says. + assert_eq!(craft_fonts(&f.0, "fonts/manifest.txt", false, false, &[], &[]).unwrap().faces.len(), 4); + // The variable reaches the catalog through `input_fonts`. + assert!(input_fonts(Some(&f.0), true, false, false, &["Arab".into()], &[]).unwrap().contains("Noto Sans Arabic")); + assert!(!input_fonts(Some(&f.0), true, false, false, &[], &[]).unwrap().contains("Noto Sans Arabic")); + assert!(scripts_from(None).is_empty()); } /// Asserts `FIGCRAFT_WEB_FONTS` (split on commas, trimmed) adds the families it names to the web @@ -254,11 +311,11 @@ mod tests { let named = web_fonts(" Noto Sans Thai, ,Noto Sans Thia "); assert_eq!(named, ["Noto Sans Thai", "Noto Sans Thia"]); let families = |c: &Catalog| c.faces.iter().map(|(family, _)| family.clone()).collect::>(); - let web = craft_fonts(&f.0, "fonts/manifest.txt", true, false, &named).unwrap(); + let web = craft_fonts(&f.0, "fonts/manifest.txt", true, false, &[], &named).unwrap(); assert_eq!(families(&web), ["Noto Sans Georgian", "Noto Sans Thai"], "the named family joins; the others stay out"); - assert_eq!(families(&craft_fonts(&f.0, "fonts/manifest.txt", true, false, &[]).unwrap()), ["Noto Sans Georgian"], "unset: as before"); - assert_eq!(craft_fonts(&f.0, "fonts/manifest.txt", false, false, &[]).unwrap().faces.len(), 3, "native builds embed every family"); + assert_eq!(families(&craft_fonts(&f.0, "fonts/manifest.txt", true, false, &[], &[]).unwrap()), ["Noto Sans Georgian"], "unset: as before"); + assert_eq!(craft_fonts(&f.0, "fonts/manifest.txt", false, false, &[], &[]).unwrap().faces.len(), 3, "native builds embed every family"); assert_eq!(not_embedded(&named, &web.faces), ["Noto Sans Thia"], "a misspelt family is reported"); - assert!(input_fonts(Some(&f.0), true, false, false, &named).unwrap().contains("family: \"Noto Sans Thai\"")); + assert!(input_fonts(Some(&f.0), true, false, false, &[], &named).unwrap().contains("family: \"Noto Sans Thai\"")); } } diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index 0643cb026..47b0399b0 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -544,10 +544,13 @@ fn names(f: &skrifa::FontRef) -> (Option, Option) { (family, style) } -/// Families tried, in order, for characters a text's own font lacks. The symbol fonts come last: -/// Apple Symbols (macOS), Segoe UI Symbol (Windows) and the Noto symbol and math fonts (Linux), for -/// signs such as ⌖ and ⤒ that no text face has. -const FALLBACK_FAMILIES: [&str; 29] = [ +/// Families tried, in order, for characters a text's own font lacks. Every script face craft-fonts +/// can embed (Noto Sans Arabic, Thai, Hebrew, Devanagari, Georgian) is here after the Latin faces, +/// so a web build that embeds one (`FIGCRAFT_EMBED_SCRIPTS`, #1902) draws that script in an Inter +/// document: the browser has no other fonts. The symbol fonts come last: Apple Symbols (macOS), +/// Segoe UI Symbol (Windows) and the Noto symbol and math fonts (Linux), for signs such as ⌖ and ⤒ +/// that no text face has. +const FALLBACK_FAMILIES: [&str; 32] = [ "Inter", "Helvetica Neue", "Arial", @@ -571,6 +574,9 @@ const FALLBACK_FAMILIES: [&str; 29] = [ "Leelawadee UI", "Thonburi", "Tahoma", + "Noto Sans Hebrew", + "Noto Sans Devanagari", + "Noto Sans Georgian", "Apple Symbols", "Segoe UI Symbol", "Noto Sans Symbols", diff --git a/crates/text/src/fontdb_tests.rs b/crates/text/src/fontdb_tests.rs index 920b73fd7..38e04c790 100644 --- a/crates/text/src/fontdb_tests.rs +++ b/crates/text/src/fontdb_tests.rs @@ -541,6 +541,31 @@ fn every_platform_has_a_thai_font_in_the_fallbacks() { } } +/// #1902: a web build has no system fonts, so a document's missing characters can only come from +/// the embedded faces. Every script face craft-fonts can embed (`FIGCRAFT_EMBED_SCRIPTS`) is a +/// document fallback after the Latin faces; else Hebrew or Devanagari typed into an Inter layer +/// would stay missing glyphs with its face embedded. +#[test] +fn every_embeddable_script_face_is_a_document_fallback() { + let at = |family: &str| FALLBACK_FAMILIES.iter().position(|f| *f == family); + let latin = at("Segoe UI").unwrap(); + for face in [ + "Noto Sans Arabic", + "Noto Sans Thai", + "Noto Sans Hebrew", + "Noto Sans Devanagari", + "Noto Sans Georgian", + "Noto Sans Symbols", + "Noto Sans Symbols 2", + ] { + assert!(at(face).is_some_and(|i| i > latin), "{face} is a fallback after the Latin faces"); + } + // And whatever script face this build embeds (CI builds have craft-fonts; CJK by request). + for f in CRAFT_FONTS.iter().filter(|f| f.family.starts_with("Noto Sans") && !f.family.contains("CJK")) { + assert!(FALLBACK_FAMILIES.contains(&f.family), "embedded {} is a document fallback", f.family); + } +} + // The paths are Unix absolute paths (font managers are a macOS thing): on Windows they aren't absolute. #[cfg(unix)] #[test] diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 5548a2644..250741a52 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (; documents: a paragraph's base direction can be forced with Text ▸ Text direction, as Add right-to-left text says (#1830); Bosnian draft catalog, one/few/other plurals; German terminology corrections (M10): gradient stops, OpenType stylistic sets and the Home-screen preference; partial review, catalog remains a draft; Arabic draft catalog, count-neutral plurals and web font gate; Thai draft catalog and Thai UI fonts; a missing translation fails no check: `cargo xtask i18n-report` lists them; Vietnamese draft catalog; string counts re-measured: 2,352 entries; Russian and Turkish catalogs reviewed by native speakers, Russian one/few/many plurals; Czech and Indonesian drafts; the Japanese draft completed; counts re-measured; Windows UI language from the first preferred language; draft catalogs catch up: every draft translates every string again; Simplified Chinese draft catalog; macOS finds its downloadable CJK system fonts (#406); string counts re-measured: 2,272 entries, the drafts at 95–99%; Swedish command labels and menu wordings; Russian, Swedish, Turkish, Czech and Indonesian listed; a duplicated paragraph removed) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (`FIGCRAFT_EMBED_SCRIPTS` embeds other scripts' craft-fonts faces in a web build, so Arabic and Persian documents draw there (#1902); ; documents: a paragraph's base direction can be forced with Text ▸ Text direction, as Add right-to-left text says (#1830); Bosnian draft catalog, one/few/other plurals; German terminology corrections (M10): gradient stops, OpenType stylistic sets and the Home-screen preference; partial review, catalog remains a draft; Arabic draft catalog, count-neutral plurals and web font gate; Thai draft catalog and Thai UI fonts; a missing translation fails no check: `cargo xtask i18n-report` lists them; Vietnamese draft catalog; string counts re-measured: 2,352 entries; Russian and Turkish catalogs reviewed by native speakers, Russian one/few/many plurals; Czech and Indonesian drafts; the Japanese draft completed; counts re-measured; Windows UI language from the first preferred language; draft catalogs catch up: every draft translates every string again; Simplified Chinese draft catalog; macOS finds its downloadable CJK system fonts (#406); string counts re-measured: 2,272 entries, the drafts at 95–99%; Swedish command labels and menu wordings; Russian, Swedish, Turkish, Czech and Indonesian listed; a duplicated paragraph removed) · **Target:** Figma Design (desktop app 126.10.7) FigCraft's interface has a localization layer with an English catalog, a Russian catalog reviewed by a native speaker, and Spanish (with a Latin American overlay), French, German, Brazilian @@ -86,7 +86,10 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v nothing (startup and memory measured unchanged); no CJK font installed means tofu, never a crash. The web build has no system fonts: built with craft-fonts and `FIGCRAFT_EMBED_CJK=1` it embeds Noto Sans CJK SC (+16 MB) and offers Japanese, Korean and Simplified Chinese; otherwise the language picker and - System leave them out. No font file is in this repo. + System leave them out. `FIGCRAFT_EMBED_SCRIPTS=Arab,Hebr,Thai,Deva` (one tag per script wanted, as + the craft-fonts manifest lists them) embeds those scripts' faces as well, so an Arabic or Persian + document draws in Noto Sans Arabic (+0.8 MB) instead of missing-glyph boxes (#1902), and with `Arab` + the picker offers the Arabic UI catalog too, as on the desktop. No font file is in this repo. ## Languages @@ -95,6 +98,8 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v | English | en | 2,352 / 2,352 (100%, source) | yes | Latin | yes (source) | full | 0 | | Simplified Chinese (Mandarin) | zh-Hans | 2,216 / 2,352 (94.2%, draft) | yes | CJK text and IME in documents; UI text in an installed Chinese face, ahead of Japanese ones (web: the `FIGCRAFT_EMBED_CJK` build); no Traditional Chinese catalog (zh-TW, zh-HK read English) | no | draft | 1–2 (after native review) | | Spanish | es, es-419 | 2,352 / 2,352 (100%, draft); es-419 overlays 104 Latin American wordings (draft) | yes | Latin | no | draft | 0–1 (after native review) | +| Hindi | hi | 0 (0%) | no | Devanagari shaping works in documents; UI font fallback to check | no | none | 4–7 | +| Arabic | ar | 1,857 / 1,857 UI strings, 620 command labels, 31 menu wordings (draft, counted 2026-10-28) | catalog complete; native review pending | Existing bidi/shaping and Arabic system fallback; web: only a build with `FIGCRAFT_EMBED_SCRIPTS=Arab` embeds Noto Sans Arabic (the font budget leaves it out otherwise, #1902); documents draw in it and the picker offers the Arabic UI then; panels do not mirror | no | draft | review + panel mirroring (not re-estimated) | | Hindi | hi | 0 (0%) | no | Devanagari shaping works in documents; the desktop UI takes an installed Devanagari face (Nirmala UI, Kohinoor Devanagari, Noto Sans Devanagari, Lohit) for names and values (#2014); no UI catalog | no | none | 4–7 | | Arabic | ar | 1,857 / 1,857 UI strings, 620 command labels, 31 menu wordings (draft, counted 2026-10-28) | catalog complete; native review pending | Existing bidi/shaping and Arabic system fallback; web not offered (font budget excludes Arabic); panels do not mirror | no | draft | review + panel mirroring (not re-estimated) | | French | fr | 2,328 / 2,352 (99.0%, draft) | yes | Latin | no | draft | 1–2 (after native review) | @@ -130,6 +135,7 @@ Hindi and Arabic. Traditional Chinese is not planned (no catalog; its users see | Date | Change | Summary | |---|---|---| +| 2026-11-03 | patch | M10, #1902: the web build drew Persian and Arabic text as missing-glyph boxes. The browser has no system fonts, and `crates/text/build.rs` keeps the web catalog to Inter, Noto Sans Georgian, the wordmark and (with `FIGCRAFT_EMBED_CJK`) Noto Sans CJK, so craft-fonts' Noto Sans Arabic never reached it. `FIGCRAFT_EMBED_SCRIPTS=Arab,Hebr,…` (script tags as the manifest's scripts column lists them) now embeds those scripts' faces in a web build; the default build and its size are unchanged, and with `Arab` embedded the picker offers the Arabic UI as well (`cjk::can_show`, as on the desktop). Noto Sans Hebrew, Devanagari and Georgian join Arabic and Thai among the document fallbacks, so an embedded face draws its script in an Inter document. Tests: `web_embeds_other_scripts_faces_only_when_asked` in the build script, `every_embeddable_script_face_is_a_document_fallback`. | | 2026-11-05 | patch | #2014: the panels drew a String variable's Amharic value as boxes: the UI took a system face for Hebrew and Arabic only. `bidi::script_fallbacks` now samples one letter of each of 16 scripts (Ethiopic, Devanagari, Gurmukhi, Gujarati, Tamil, Telugu, Kannada and Malayalam, Sinhala, Lao, Myanmar, Khmer, Armenian and Georgian as well as Hebrew and Arabic; Bengali and Thai keep `crate::cjk`'s faces) against the system's families for each (Windows, macOS, the common Linux fonts), the first installed one that draws the letter taken once. Desktop only: the web build has no system fonts. Test: `bidi::tests::other_scripts_take_an_installed_system_face` (skips scripts the machine has no face for). | | 2026-11-05 | minor | Documents: a paragraph's base direction can be forced left to right or right to left (Text ▸ Text direction, `text.direction`, `node.set textDirection`, the Type settings control, the `.fig` lines' `sourceDirectionality`), as Add right-to-left text says (#1830); detection stays the default. Four new UI strings (English only; other catalogs fall back). | | 2026-10-30 | minor | Bosnian (`bs`, Bosanski) draft catalog: every UI string (1,906), command label (629) and menu wording (31), machine-drafted in Latin script with ijekavian forms and marked as a draft awaiting native review. Bosnian plurals: one for 1, 21, 31… (not 11), few for 2–4, 22–24… (not 12–14), other for the rest. Grade and hours unchanged (Bosnian is not one of the twelve key languages) | diff --git a/docs/translating.md b/docs/translating.md index 9c8aea04d..b24a4e04b 100644 --- a/docs/translating.md +++ b/docs/translating.md @@ -78,7 +78,10 @@ CJK and Thai text need their own fonts, which egui and Inter lack: the desktop a one when something on screen needs it, so layer and page names in those scripts show whatever the UI language (see `crates/ui-egui/src/cjk.rs`). The web build has no system fonts, so Japanese, Korean and Chinese appear there only in a build with craft-fonts and -`FIGCRAFT_EMBED_CJK=1` (Noto Sans CJK SC, +16 MB); otherwise they are left out of the picker. A +`FIGCRAFT_EMBED_CJK=1` (Noto Sans CJK SC, +16 MB); otherwise they are left out of the picker. The other +scripts' faces (Noto Sans Arabic, Hebrew, Thai, Devanagari) join a web build only with +`FIGCRAFT_EMBED_SCRIPTS=Arab,Hebr,Thai,Deva` (one tag per script wanted, as the manifest lists them), for +documents in those scripts; with `Arab` embedded the picker offers Arabic too (#1902). A Chinese UI puts its Chinese face first so Han takes Simplified Chinese shapes. Georgian is drawn in craft-fonts' Noto Sans Georgian, one of the built-in faces in `theme::install_fonts`, or else in an installed font on the desktop. Web builds keep it only when the craft-fonts they're built with has