M9: Japanese and Korean draft catalogs and CJK UI fonts
- **Japanese (`ja`) and Korean (`ko`) draft catalogs** (`crates/ui-egui/locales/ja.toml`, `ko.toml`): every UI string (1,351 keys), command label (519) and menu wording (29), machine-drafted from our English (Japanese: concise, no desu/masu endings, full-width punctuation where Japanese UI text uses it; Korean: no hamnida endings, instructions in -seyo). Placeholders identical to English, `|context` keys translated per context, plural entries give only `other`. Marked `status = "draft - needs native review"` and registered as drafts (not in `COMPLETE`), like fr/de/pt-BR. - **Plurals:** `plural_one` returns false for ja and ko (always "other"). - **Language picker and detection:** Preferences > Language lists the two languages by their own names; System picks `ja*` / `ko*` from the OS language list; `pref.language` doc lists `ja|ko` (engine spec and docs/mcp.md). - **CJK UI fonts** (`crates/ui-egui/src/cjk.rs`): egui's fonts and Inter have no CJK glyphs. After the existing system font scan, the desktop app works out which CJK scripts (kana, Hangul, Han) the UI shows: the UI language's, the language picker's while Preferences is open, typed/IME text, and the open file's names and comments (re-read when the tab's revision changes, not mid-drag, and never again once all three scripts have a face). For each needed script it appends one face to egui's fallback list, the UI language's script first: Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux/BSD Noto Sans CJK, Source Han Sans, Nanum, WenQuanYi; the craft-fonts build's embedded BIZ UDPGothic is tried first (free). Han without kana outside a Japanese/Korean UI reads as Chinese (Chinese faces first). English users load no CJK face. No font files are added to the repo. - **Read once, share the bytes:** new `FontDb::find_one` reads only the one face a fallback needs instead of every file of the family (Yu Gothic UI is 4 files of ~14 MB); `theme::font_data` hands egui the font database's shared bytes instead of copying them (also used for the existing Hebrew/Arabic fallbacks). The scan's existing 64 MB per-file cap applies. Any failure degrades to the previous behaviour (tofu at worst), never a panic. - **Web:** no system fonts there. `crates/text/build.rs` now embeds craft-fonts' Noto Sans CJK into the wasm build when `FIGCRAFT_EMBED_CJK` is set (it covers kana, Hangul and Han; +16 MB) and reruns when that variable changes. Without it, `cjk::can_show` keeps ja/ko out of the picker and out of System detection, so Japanese and Korean browser users keep the English UI instead of boxes. Nothing is fetched from the network. - **Docs:** localization-parity (rows, CJK UI fonts section, totals, revision), translating (catalog list, CJK conventions, fonts, plural note), target-app-parity, ROADMAP, gaps (gap 5), roadmap (focus 4), mcp.md. Localization 30% -> 35% (graded low: unreviewed drafts), 40-65 h -> 32-55 h; ready for real work ~52% -> ~53% (52.9 -> 53.1 by the dimension table); full parity ~284-554 h -> ~276-544 h; beta ~162-292 h -> ~155-280 h.
This commit is contained in:
parent
207373be15
commit
159de3ce43
30
ROADMAP.md
30
ROADMAP.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents)
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow,
|
||||
native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through
|
||||
|
|
@ -15,17 +15,17 @@ start with [`docs/gaps.md`](docs/gaps.md).
|
|||
|---|---|---|
|
||||
| Menu/command breadth | **100%** (204 / 204) | measured, [`docs/parity-checklist.md`](docs/parity-checklist.md) (`cargo xtask parity`) |
|
||||
| Feature breadth | **~84%** (core editor ~94%) | estimated, [`docs/target-app-parity.md`](docs/target-app-parity.md) |
|
||||
| Ready for real work | **~52%** | estimated, same doc (written dimension weights and scores) |
|
||||
| Ready for real work | **~53%** | estimated, same doc (written dimension weights and scores) |
|
||||
| Essentials user | **~57%** | estimated: core features 77.8% × 0.92 launch/stability × 0.92 discoverability × 0.88 opening files people send ([method](docs/target-app-parity.md#essentials-user)) |
|
||||
| Mainstream practitioner | **~49%** | estimated: weekly areas 72.6% × 0.9 interaction × 0.9 stability × 0.85 file exchange with Figma users ([method](docs/target-app-parity.md#mainstream-practitioner)) |
|
||||
| Remaining to beta | **~162–292 h** one agent | estimated |
|
||||
| Remaining to full parity | **~276–538 h** one agent (~85–148 h wall-clock with 5 agents) | estimated |
|
||||
| Remaining to beta | **~155–280 h** one agent | estimated |
|
||||
| Remaining to full parity | **~268–528 h** one agent (~83–145 h wall-clock with 5 agents) | estimated |
|
||||
|
||||
### Readiness by audience
|
||||
|
||||
| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates |
|
||||
|---|---|---|---|
|
||||
| Full target (ready for real work) | ~52% | ~281–540 h | multiplayer (42–84), localization (40–65), plugins (10–30), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) |
|
||||
| Full target (ready for real work) | ~53% | ~273–530 h | multiplayer (42–84), localization (32–55), plugins (10–30), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) |
|
||||
| Mainstream practitioner | ~49% | ~130–244 h | core editor depth incl. the `.fig` import's tail (49–98), the interaction pass (40–60), real-file corpus and platform QA (25–50), `.fig` export for handing work back (15–30) |
|
||||
| Essentials user | ~57% | ~60–110 h | the `.fig` import's tail so the files people send open well (5–10), core tool polish (25–45), interaction pass on the core tools (15–25), stability QA (10–20), onboarding and starter templates (5–10) |
|
||||
|
||||
|
|
@ -46,11 +46,11 @@ lines, a typical feature PR 1–2.5 h). Details and what needs a human:
|
|||
**Alpha.** Core workflows exist end to end (frames, shapes, vectors, text, auto layout, components
|
||||
with variants and overrides, styles, variables, prototyping, Dev Mode, SVG/PNG/JPG/PDF), but
|
||||
FigCraft opens `.fig` files only best effort, has only a preview of multiplayer, and nothing has been checked
|
||||
against real Figma documents. Ready-for-real-work is ~52%, inside alpha's ~40–75% band, and the alpha gate passes: every daily core workflow (design screens, build a design system, prototype, hand off to developers) works end to end on FigCraft's own files, which save and reopen; review works only asynchronously and `.fig` import is unproven on real files, both beta gaps ([alpha gate](docs/roadmap.md#alpha-gate)). Beta needs
|
||||
against real Figma documents. Ready-for-real-work is ~53%, inside alpha's ~40–75% band, and the alpha gate passes: every daily core workflow (design screens, build a design system, prototype, hand off to developers) works end to end on FigCraft's own files, which save and reopen; review works only asynchronously and `.fig` import is unproven on real files, both beta gaps ([alpha gate](docs/roadmap.md#alpha-gate)). Beta needs
|
||||
~75% **and** reliable `.fig` import: the `.fig` import's tail (5–10 h), a real-file corpus with measured
|
||||
fidelity (15–30 h), the interaction pass (25–40 h of the 40–60), core depth to ~80% (40–75 h),
|
||||
basic multiplayer on the landed core (30–55 h), stability and platform QA (20–40 h), a partial GPU path (15–30 h) and
|
||||
Figma's last two languages, Japanese and Korean, on the landed localization layer (7–12 h).
|
||||
basic multiplayer on the landed core (30–55 h), stability and platform QA (20–40 h), a partial GPU path (15–30 h);
|
||||
native speakers' reviews of the six language drafts need people.
|
||||
|
||||
## By dimension
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ Figma's last two languages, Japanese and Korean, on the landed localization laye
|
|||
| Stability | 55% | 20–40 | [gaps](docs/gaps.md) |
|
||||
| Performance | 60% | 30–60 | [hardware-parity](docs/hardware-parity.md) |
|
||||
| Hardware | 60% | 10–21 | [hardware-parity](docs/hardware-parity.md) |
|
||||
| Localization | 30% (layer; English; Spanish, French, German, Brazilian Portuguese drafts) | 40–65 | [localization-parity](docs/localization-parity.md) |
|
||||
| Localization | 35% (layer; English; Spanish, French, German, Brazilian Portuguese, Japanese, Korean drafts; CJK UI fonts) | 32–55 | [localization-parity](docs/localization-parity.md) |
|
||||
| Platforms | 75% | 10–20 | [hardware-parity](docs/hardware-parity.md) |
|
||||
| Ecosystem / plugins | 34% (own WASM plugins with panels, settings and widgets) | 10–30 | [gaps](docs/gaps.md) |
|
||||
| AI features | 0% | 30–60 | [gaps](docs/gaps.md) |
|
||||
|
|
@ -106,14 +106,15 @@ Figma's last two languages, Japanese and Korean, on the landed localization laye
|
|||
| French | fr | 100% | draft (needs native review) |
|
||||
| Portuguese (Brazil) | pt-BR | 100% | draft (needs native review) |
|
||||
| Indonesian | id | 0% | none |
|
||||
| Japanese | ja | 0% | none |
|
||||
| Japanese | ja | 100% | draft (needs native review) |
|
||||
| German | de | 100% | draft (needs native review) |
|
||||
| Korean | ko | 0% | none |
|
||||
| Korean | ko | 100% | draft (needs native review) |
|
||||
| Vietnamese | vi | 0% | none |
|
||||
|
||||
No other languages. Figma ships English, Japanese, Korean, French, German, Spanish and Portuguese
|
||||
(Brazil); FigCraft has the localization layer, English and Spanish, French, German and Brazilian
|
||||
Portuguese drafts (Preferences ▸ Language; adding a language:
|
||||
(Brazil); FigCraft has the localization layer, English and Spanish, French, German, Brazilian
|
||||
Portuguese, Japanese and Korean drafts (Preferences ▸ Language; CJK UI text uses an installed CJK
|
||||
font, loaded only when needed; adding a language:
|
||||
[`docs/translating.md`](docs/translating.md)). Document text in any script (bidi, Arabic shaping,
|
||||
IME) already works. Detail: [`docs/localization-parity.md`](docs/localization-parity.md).
|
||||
|
||||
|
|
@ -124,7 +125,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
|||
1. `.fig` import's tail: fixes from real files, connector labels, table cells, filled areas with their own fills (the best-effort import, editable vector networks, the other layer kinds and variable prototype actions landed 2026-10-11): 5–10 h.
|
||||
2. Real-file corpus and measured fidelity: 15–30 h.
|
||||
3. Tool-by-tool interaction pass against Figma: 40–60 h.
|
||||
4. Languages on the landed localization layer: ja and ko (7–12 h), and native reviews of the es, fr, de and pt-BR drafts (people).
|
||||
4. Languages: native reviews of the es, fr, de, pt-BR, ja and ko drafts (people), Latin American Spanish (1–2 h), then Simplified Chinese (3–5 h).
|
||||
5. Multiplayer (M9): accounts and teams on top of the live sessions and the standalone server (transport, hosting, invite links that open the app, document sync, presence with remote text carets, cursor chat and spotlight, per-user undo, reconnect with offline edits, collision-free ids for styles, variables and comments, the browser client, `figcraft-server` with persistence and share links to edit or view, team libraries on the server landed): 42–84 h.
|
||||
|
||||
## Progress log
|
||||
|
|
@ -229,6 +230,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Localization 30% → 35%, 40–65 h → 32–55 h; ready for real work ~52% → ~53% (52.9 → 53.1); full parity ~284–554 h → ~276–544 h; beta ~162–292 h → ~155–280 h |
|
||||
| 2026-10-12 | minor | Plugin widgets and the panel tail (gap 6, plugin API 3; v1 and v2 plugins unchanged): a plugin marked as a widget puts a layer of its own on the canvas from Plugins ▸ Widgets or `widget.insert`: it describes the layer (frames with auto layout, text, rectangles, ellipses, images it carries) and FigCraft validates the description (size, layer count, depth, numbers, colours, text and image sizes) and builds it as a frame with locked layers inside; a click on a part the plugin named (on the canvas with the Move tool, a pointing hand over it, or `widget.click`) calls the plugin back in a fresh, metered sandbox with the widget's state, as one undo step, and the widget is redrawn; the state lives in the widget's own layer data, so widgets survive save and reload, copying, undo, export and live sessions; no plugin can write that data or call `widget.*`. `widget.list` lists them. A Counter example widget. Plugin panels of different plugins open side by side, and a colour field's swatch opens the colour picker (the plugin hears of the colour when a drag ends). Plugins & widgets 45%/28% → 55%/32%, 15–40 h → 10–30 h; Ecosystem dimension 28% → 34%; weighted totals unchanged at this rounding; full parity ~286–558 h → ~281–548 h |
|
||||
| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Localization 20% → 30%, 45–75 h → 40–65 h; ready for real work unchanged (~52%); full parity ~289–564 h → ~284–554 h; beta ~170–305 h → ~162–292 h |
|
||||
| 2026-10-12 | minor | Cursor chat and spotlight in live sessions: `/` opens a chat bubble at your pointer that everyone sees as you type and that fades a few seconds after the last change; a spotlight (your avatar's menu, `collab.spotlight`) makes everyone follow your view and page until they move their own. Ephemeral presence the host validates and rate-limits, passed only to clients that read it (older clients keep working). Multiplayer & sharing breadth 15% → 17%, ready unchanged (5%), 45–90 h → 42–84 h; collaboration 13% unchanged, 49–98 h → 46–92 h; feature breadth (~84%) and ready for real work (~51%) unchanged at this rounding; full parity ~301–587 h → ~298–581 h |
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
s.prefs.user_name = if name.is_empty() { super::comment::DEFAULT_AUTHOR.into() } else { name };
|
||||
Ok(json!({"name": s.prefs.user_name}))
|
||||
}),
|
||||
spec!("pref.language", "Interface language", "{language: system|en|es|fr|de|pt-BR|… (no catalog: English)}", false, |s, p| {
|
||||
spec!("pref.language", "Interface language", "{language: system|en|es|fr|de|pt-BR|ja|ko|… (no catalog: English)}", false, |s, p| {
|
||||
let lang = req_string("pref.language", p, "language")?;
|
||||
let lang = lang.trim();
|
||||
// A BCP 47-style tag ("es", "pt-BR", "zh_Hans") or "system".
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
//! Embeds the optional craft-fonts build input (`CRAFT_FONTS_DIR`): both `fonts/manifest.txt` and the
|
||||
//! opt-in Latin presentation fonts in `fonts/latin-manifest.txt` (Inter, Roboto, …). Unset or missing,
|
||||
//! `CRAFT_FONTS` is empty and the app uses system fonts. 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).
|
||||
use std::fmt::Write as _;
|
||||
use std::path::PathBuf;
|
||||
|
||||
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");
|
||||
let mut src = String::from("pub static CRAFT_FONTS: &[CraftFont] = &[\n");
|
||||
if let Some(dir) = std::env::var_os("CRAFT_FONTS_DIR").map(PathBuf::from) {
|
||||
let mut ok = false;
|
||||
|
|
@ -37,7 +39,6 @@ fn craft_fonts(dir: &std::path::Path, manifest_rel: &str) -> Result<String, Stri
|
|||
let manifest = dir.join(manifest_rel);
|
||||
println!("cargo::rerun-if-changed={}", manifest.display());
|
||||
let text = std::fs::read_to_string(&manifest).map_err(|e| format!("{}: {e}", manifest.display()))?;
|
||||
// Web builds have a size budget: embed only Inter (the default UI and document font) there.
|
||||
let wasm = std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32");
|
||||
let mut out = String::new();
|
||||
for line in text.lines().map(str::trim).filter(|l| !l.is_empty() && !l.starts_with('#')) {
|
||||
|
|
@ -45,11 +46,14 @@ fn craft_fonts(dir: &std::path::Path, manifest_rel: &str) -> Result<String, Stri
|
|||
let [family, style, file, scripts, ..] = f.as_slice() else {
|
||||
return Err(format!("malformed manifest line: {line}"));
|
||||
};
|
||||
if wasm && *family != "Inter" {
|
||||
// CJK fonts are large; the desktop app finds system CJK fonts instead unless asked.
|
||||
let cjk = scripts.contains("Hans");
|
||||
if cjk && std::env::var_os("FIGCRAFT_EMBED_CJK").is_none() {
|
||||
continue;
|
||||
}
|
||||
// CJK fonts are large; the desktop app finds system CJK fonts instead unless asked.
|
||||
if scripts.contains("Hans") && std::env::var_os("FIGCRAFT_EMBED_CJK").is_none() {
|
||||
// Web builds have a size budget: Inter, plus Noto Sans CJK (every CJK script, for CJK UI
|
||||
// text) when asked.
|
||||
if wasm && *family != "Inter" && !cjk {
|
||||
continue;
|
||||
}
|
||||
let path = dir.join(file).canonicalize().map_err(|e| format!("{file}: {e}"))?;
|
||||
|
|
|
|||
|
|
@ -321,6 +321,12 @@ fn norm_style(s: &str) -> String {
|
|||
s.to_lowercase().replace([' ', '-', '_'], "")
|
||||
}
|
||||
|
||||
/// A style name's weight before its file is read: the usual names, and Hiragino's "W3" (300).
|
||||
fn pending_weight(style: &str) -> i32 {
|
||||
let w = norm_style(style).strip_prefix('w').and_then(|n| n.parse::<i32>().ok()).filter(|n| (0..=9).contains(n));
|
||||
w.map_or(i32::from(figcraft_doc::style_weight(style)), |n| n * 100)
|
||||
}
|
||||
|
||||
/// The axes (user-space ranges) and named instances of a variable font.
|
||||
fn variation_info(f: &skrifa::FontRef) -> (Vec<Axis>, Vec<NamedInstance>) {
|
||||
let all = f.axes();
|
||||
|
|
@ -455,7 +461,7 @@ impl FontDb {
|
|||
|
||||
/// Style names of a family, by weight then italic.
|
||||
pub fn styles(&mut self, family: &str) -> Vec<String> {
|
||||
self.load_family(family);
|
||||
self.load_family(family, None);
|
||||
let Some((_, idx)) = self.families.get(&family.to_lowercase()) else { return vec![] };
|
||||
// (weight, italic, order in the font, name): named instances keep the font's order.
|
||||
let mut v: Vec<(u16, bool, usize, String)> = Vec::new();
|
||||
|
|
@ -482,9 +488,30 @@ impl FontDb {
|
|||
self.families.contains_key(&k) || self.pending.contains_key(&k)
|
||||
}
|
||||
|
||||
fn load_family(&mut self, family: &str) {
|
||||
/// Read a system family's faces from disk; with `only`, just the face nearest that style (the
|
||||
/// rest stay pending).
|
||||
fn load_family(&mut self, family: &str, only: Option<&str>) {
|
||||
let key = family.to_lowercase();
|
||||
let Some((_, entries)) = self.pending.remove(&key) else { return };
|
||||
let entries = match only {
|
||||
None => match self.pending.remove(&key) {
|
||||
Some((_, entries)) => entries,
|
||||
None => return,
|
||||
},
|
||||
Some(style) => {
|
||||
let Some((_, pending)) = self.pending.get_mut(&key) else { return };
|
||||
let (want, want_w) = (norm_style(style), pending_weight(style));
|
||||
let cost = |e: &Pending| {
|
||||
let italic = if e.style.to_lowercase().contains("italic") { 1000 } else { 0 };
|
||||
if norm_style(&e.style) == want { 0 } else { 1 + (pending_weight(&e.style) - want_w).abs() + italic }
|
||||
};
|
||||
let Some(i) = pending.iter().enumerate().min_by_key(|(_, e)| cost(e)).map(|(i, _)| i) else { return };
|
||||
let one = pending.remove(i);
|
||||
if pending.is_empty() {
|
||||
self.pending.remove(&key);
|
||||
}
|
||||
vec![one]
|
||||
}
|
||||
};
|
||||
let mut by_path: BTreeMap<std::path::PathBuf, Arc<Vec<u8>>> = BTreeMap::new();
|
||||
for e in entries {
|
||||
let bytes = match by_path.get(&e.path) {
|
||||
|
|
@ -506,7 +533,22 @@ impl FontDb {
|
|||
|
||||
/// The best face for `family` + `style` (exact style name, else nearest weight/italic).
|
||||
pub fn find(&mut self, family: &str, style: &str) -> Option<Arc<Face>> {
|
||||
self.load_family(family);
|
||||
self.load_family(family, None);
|
||||
self.best(family, style)
|
||||
}
|
||||
|
||||
/// [`find`](Self::find), but a system family not loaded yet reads only its face nearest
|
||||
/// `style`: a fallback for missing characters needs one face, not every file of a CJK family
|
||||
/// (10–20 MB each). The family's other faces load when it is used by name.
|
||||
pub fn find_one(&mut self, family: &str, style: &str) -> Option<Arc<Face>> {
|
||||
if !self.families.contains_key(&family.to_lowercase()) {
|
||||
self.load_family(family, Some(style));
|
||||
}
|
||||
self.best(family, style)
|
||||
}
|
||||
|
||||
/// The loaded face of `family` nearest `style`.
|
||||
fn best(&self, family: &str, style: &str) -> Option<Arc<Face>> {
|
||||
let (_, idx) = self.families.get(&family.to_lowercase())?;
|
||||
let faces: Vec<&Arc<Face>> = idx.iter().filter_map(|i| self.faces.get(*i)).collect();
|
||||
let want = norm_style(style);
|
||||
|
|
@ -714,5 +756,9 @@ pub fn system_fonts_scanned() -> bool {
|
|||
/// that needs a font for a script its own fonts lack.
|
||||
pub fn face_with_char(c: char, families: &[&str]) -> Option<Arc<Face>> {
|
||||
let mut db = db().write().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
families.iter().filter_map(|f| db.find(f, "Regular")).find(|f| f.has_char(c))
|
||||
families.iter().filter_map(|f| db.find_one(f, "Regular")).find(|f| f.has_char(c))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "fontdb_tests.rs"]
|
||||
mod tests;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
use super::*;
|
||||
|
||||
fn pending(styles: &[&str]) -> FontDb {
|
||||
let mut db = FontDb::default();
|
||||
let entries = styles.iter().map(|s| Pending { path: format!("no-such-dir/{s}.ttf").into(), index: 0, style: (*s).into() }).collect();
|
||||
db.pending.insert("cjk ui".into(), ("CJK UI".into(), entries));
|
||||
db
|
||||
}
|
||||
|
||||
fn left(db: &FontDb) -> Vec<String> {
|
||||
db.pending.get("cjk ui").map(|(_, v)| v.iter().map(|e| e.style.clone()).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_one_reads_only_the_nearest_style() {
|
||||
// The files don't exist: what matters is which entry is tried, and that the rest stay pending.
|
||||
let mut db = pending(&["Bold", "Light", "Regular", "Italic"]);
|
||||
assert!(db.find_one("CJK UI", "Regular").is_none());
|
||||
assert_eq!(left(&db), ["Bold", "Light", "Italic"]);
|
||||
assert!(db.has_family("cjk ui"), "the family is still offered");
|
||||
// No exact name: the nearest weight, upright before italic.
|
||||
let mut db = pending(&["W6", "Italic", "W3", "Bold"]);
|
||||
db.find_one("CJK UI", "Regular");
|
||||
assert_eq!(left(&db), ["W6", "Italic", "Bold"]);
|
||||
// `find` reads the whole family.
|
||||
db.find("CJK UI", "Regular");
|
||||
assert!(left(&db).is_empty() && !db.has_family("cjk ui"));
|
||||
assert_eq!(pending_weight("W3"), 300);
|
||||
assert_eq!(pending_weight("SemiBold"), 600);
|
||||
assert_eq!(pending_weight("W12"), 400);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -298,9 +298,7 @@ pub fn script_fallbacks(have: &dyn Fn(char) -> bool) -> Vec<(String, egui::FontD
|
|||
continue;
|
||||
}
|
||||
let Some(face) = figcraft_text::face_with_char(c, &FAMILIES) else { continue };
|
||||
let mut data = egui::FontData::from_owned(face.bytes.data().to_vec());
|
||||
data.index = face.index;
|
||||
out.push((format!("system-{}-{}", face.family, face.style), data));
|
||||
out.push((format!("system-{}-{}", face.family, face.style), crate::theme::font_data(&face)));
|
||||
taken.push(face);
|
||||
}
|
||||
out
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
//! Chinese, Japanese and Korean text in the UI. egui's own fonts and Inter have no CJK glyphs, so
|
||||
//! the UI adds a face per script it needs: the UI language's, the scripts in the open file's names
|
||||
//! and comments, the language picker's and typed text. Desktop finds them among the installed
|
||||
//! fonts (the system font scan), only once one is needed, so English users load none; the web
|
||||
//! build has only the CJK faces built in from craft-fonts (`FIGCRAFT_EMBED_CJK`), else none.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use figcraft_text::Face;
|
||||
|
||||
pub const KANA: u8 = 1;
|
||||
pub const HANGUL: u8 = 2;
|
||||
/// Han characters (and CJK punctuation and full-width forms).
|
||||
pub const HAN: u8 = 4;
|
||||
|
||||
/// Japanese faces, the built-in one first: Windows, macOS, then Linux names.
|
||||
const JAPANESE: &[&str] = &[
|
||||
"BIZ UDPGothic",
|
||||
"Yu Gothic UI",
|
||||
"Yu Gothic",
|
||||
"Meiryo UI",
|
||||
"Meiryo",
|
||||
"MS UI Gothic",
|
||||
"Hiragino Sans",
|
||||
"Hiragino Kaku Gothic ProN",
|
||||
"Noto Sans CJK JP",
|
||||
"Noto Sans JP",
|
||||
"Source Han Sans JP",
|
||||
"IPAexGothic",
|
||||
"IPAPGothic",
|
||||
"TakaoPGothic",
|
||||
];
|
||||
const KOREAN: &[&str] =
|
||||
&["Malgun Gothic", "Apple SD Gothic Neo", "Noto Sans CJK KR", "Noto Sans KR", "Source Han Sans KR", "NanumGothic", "NanumBarunGothic", "UnDotum"];
|
||||
const CHINESE: &[&str] = &[
|
||||
"Noto Sans CJK SC",
|
||||
"Microsoft YaHei UI",
|
||||
"Microsoft YaHei",
|
||||
"PingFang SC",
|
||||
"Noto Sans SC",
|
||||
"Source Han Sans SC",
|
||||
"WenQuanYi Micro Hei",
|
||||
"WenQuanYi Zen Hei",
|
||||
"Droid Sans Fallback",
|
||||
];
|
||||
|
||||
/// The CJK scripts in `s`.
|
||||
pub fn scripts(s: &str) -> u8 {
|
||||
s.chars().fold(0, |m, c| {
|
||||
m | match u32::from(c) {
|
||||
0x3040..=0x30FF | 0x31F0..=0x31FF | 0xFF66..=0xFF9F => KANA,
|
||||
0x1100..=0x11FF | 0x3130..=0x318F | 0xA960..=0xA97F | 0xAC00..=0xD7FF | 0xFFA0..=0xFFDC => HANGUL,
|
||||
0x3000..=0x303F | 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF | 0xFF01..=0xFF65 | 0x20000..=0x3FFFF => HAN,
|
||||
_ => 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The CJK scripts a UI language (a catalog code) is written in.
|
||||
pub fn language(code: &str) -> u8 {
|
||||
match code.split('-').next() {
|
||||
Some("ja") => KANA | HAN,
|
||||
Some("ko") => HANGUL,
|
||||
Some("zh") => HAN,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the UI can draw language `code`: always on desktop (a system face is found when it's
|
||||
/// needed; tofu at worst), on the web only with built-in faces for its scripts. The language
|
||||
/// picker and the system language skip a language it can't.
|
||||
pub fn can_show(code: &str) -> bool {
|
||||
let need = language(code);
|
||||
need == 0
|
||||
|| !cfg!(target_arch = "wasm32")
|
||||
|| samples(need, false).iter().all(|(c, _)| figcraft_text::face_with_char(*c, &["BIZ UDPGothic", "Noto Sans CJK SC"]).is_some())
|
||||
}
|
||||
|
||||
/// A character standing for each script in `need`, with the families to look for it in, the
|
||||
/// language's own script first. Han without kana in a non-Japanese, non-Korean UI reads as Chinese
|
||||
/// (a simplified character, Chinese faces first); else a Japanese or Korean face draws it.
|
||||
fn samples(need: u8, korean_first: bool) -> Vec<(char, Vec<&'static str>)> {
|
||||
let chinese = need & HAN != 0 && need & KANA == 0 && !korean_first;
|
||||
let han: (char, Vec<&str>) = match (chinese, korean_first) {
|
||||
(true, _) => ('们', [CHINESE, JAPANESE, KOREAN].concat()),
|
||||
(false, true) => ('漢', [KOREAN, JAPANESE, CHINESE].concat()),
|
||||
(false, false) => ('漢', [JAPANESE, KOREAN, CHINESE].concat()),
|
||||
};
|
||||
let mut v = vec![(KANA, ('あ', JAPANESE.to_vec())), (HAN, han), (HANGUL, ('한', KOREAN.to_vec()))];
|
||||
if korean_first {
|
||||
v.reverse();
|
||||
}
|
||||
v.into_iter().filter(|(bit, _)| need & bit != 0).map(|(_, s)| s).collect()
|
||||
}
|
||||
|
||||
/// Faces (built-in or installed) drawing the scripts in `need` that `have` (the UI's other fonts)
|
||||
/// lacks, in fallback order: one per script, unless an earlier one draws it too. Only the families
|
||||
/// tried are read from disk.
|
||||
pub fn faces(need: u8, lang: &str, have: &dyn Fn(char) -> bool) -> Vec<Arc<Face>> {
|
||||
let mut out: Vec<Arc<Face>> = Vec::new();
|
||||
for (c, families) in samples(need, language(lang) == HANGUL) {
|
||||
if have(c) || out.iter().any(|f| f.has_char(c)) {
|
||||
continue;
|
||||
}
|
||||
if let Some(face) = figcraft_text::face_with_char(c, &families) {
|
||||
out.push(face);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn scripts_and_languages() {
|
||||
assert_eq!(scripts("Frame 1"), 0);
|
||||
assert_eq!(scripts("ボタン"), KANA);
|
||||
assert_eq!(scripts("버튼"), HANGUL);
|
||||
assert_eq!(scripts("按钮"), HAN);
|
||||
assert_eq!(scripts("日本語のレイヤー(1)"), KANA | HAN);
|
||||
assert_eq!(language("ja"), KANA | HAN);
|
||||
assert_eq!(language("ko"), HANGUL);
|
||||
assert_eq!(language("zh-Hant"), HAN);
|
||||
assert_eq!(language("fr"), 0);
|
||||
assert!(["en", "es", "fr", "de", "pt-BR"].iter().all(|c| can_show(c)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_ui_language_script_comes_first() {
|
||||
let order = |need, ko| samples(need, ko).into_iter().map(|(c, _)| c).collect::<String>();
|
||||
assert_eq!(order(KANA | HAN | HANGUL, false), "あ漢한");
|
||||
assert_eq!(order(KANA | HAN | HANGUL, true), "한漢あ");
|
||||
assert_eq!(order(HANGUL, false), "한");
|
||||
// Han without kana outside a Japanese or Korean UI reads as Chinese.
|
||||
assert_eq!(order(HAN, false), "们");
|
||||
assert_eq!(samples(HAN, false).first().and_then(|(_, f)| f.first()).copied(), Some("Noto Sans CJK SC"));
|
||||
assert_eq!(samples(HAN, true).first().and_then(|(_, f)| f.first()).copied(), Some("Malgun Gothic"));
|
||||
assert!(faces(0, "en", &|_| false).is_empty());
|
||||
assert!(faces(KANA | HANGUL | HAN, "ja", &|_| true).is_empty(), "nothing is looked up for scripts the UI draws");
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ struct Ui {
|
|||
impl Ui {
|
||||
fn new() -> Self {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx, false);
|
||||
crate::theme::install_fonts(&ctx, false, 0);
|
||||
crate::theme::apply(&ctx, Tokens::light());
|
||||
let mut u = Self { ctx, time: 0.0 };
|
||||
// Fonts set now take effect on the next frame.
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ pub fn show(app: &mut FigApp, ctx: &Context) {
|
|||
ui.horizontal(|ui| {
|
||||
ui.label(tr("Language"));
|
||||
// Each language by its own name; System follows the OS (the browser on the web).
|
||||
let langs = i18n::languages().iter().map(|l| (l.code, l.name));
|
||||
let langs = i18n::languages().iter().filter(|l| crate::cjk::can_show(l.code)).map(|l| (l.code, l.name));
|
||||
for (code, name) in [("system", tr("System"))].into_iter().chain(langs) {
|
||||
if ui.selectable_label(app.session.prefs.language.eq_ignore_ascii_case(code), name).clicked() {
|
||||
app.run("pref.language", json!({"language": code}));
|
||||
|
|
|
|||
|
|
@ -61,13 +61,14 @@ fn resolve(pref: &str, system: Option<&str>) -> usize {
|
|||
}
|
||||
|
||||
/// The [`LANGS`] index for one tag: the exact tag ("pt-BR"), else its base language ("es" for
|
||||
/// "es_ES.UTF-8"), else a regional catalog of that language ("pt-BR" for "pt-PT").
|
||||
/// "es_ES.UTF-8"), else a regional catalog of that language ("pt-BR" for "pt-PT"). A language the
|
||||
/// UI can't draw (CJK on the web without CJK faces built in) doesn't count.
|
||||
fn resolve_one(want: &str) -> Option<usize> {
|
||||
let tag = want.trim().split(['.', '@']).next().unwrap_or("").replace('_', "-");
|
||||
let base = tag.split('-').next().unwrap_or("");
|
||||
let find = |t: &str| LANGS.iter().position(|l| l.code.eq_ignore_ascii_case(t));
|
||||
let regional = || LANGS.iter().position(|l| !base.is_empty() && l.code.split('-').next().is_some_and(|b| b.eq_ignore_ascii_case(base)));
|
||||
find(&tag).or_else(|| find(base)).or_else(regional)
|
||||
find(&tag).or_else(|| find(base)).or_else(regional).filter(|i| LANGS.get(*i).is_some_and(|l| crate::cjk::can_show(l.code)))
|
||||
}
|
||||
|
||||
fn find(table: &'static [(&'static str, Text)], key: &str) -> Option<&'static Text> {
|
||||
|
|
@ -112,13 +113,16 @@ pub fn trn(key: &str, n: usize, args: &[(&str, &dyn Display)]) -> String {
|
|||
fill(text, &all)
|
||||
}
|
||||
|
||||
/// Whether count `n` takes the "one" form: 0 and 1 in French and Brazilian Portuguese, 1 elsewhere
|
||||
/// (CLDR's integer rules).
|
||||
/// Whether count `n` takes the "one" form: 0 and 1 in French and Brazilian Portuguese, never in
|
||||
/// Japanese and Korean (no plural forms: always "other"), 1 elsewhere (CLDR's integer rules).
|
||||
// shortcut: CLDR's "many" (French, Spanish and Portuguese millions) reads as "other" here; add it
|
||||
// when a language needs a third form.
|
||||
fn plural_one(lang: usize, n: usize) -> bool {
|
||||
let zero_is_one = LANGS.get(lang).is_some_and(|l| matches!(l.code, "fr" | "pt-BR"));
|
||||
n == 1 || (n == 0 && zero_is_one)
|
||||
match LANGS.get(lang).map(|l| l.code) {
|
||||
Some("ja" | "ko") => false,
|
||||
Some("fr" | "pt-BR") => n <= 1,
|
||||
_ => n == 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace each `{name}` in `text` with its argument, in one pass (an argument's own braces stay).
|
||||
|
|
|
|||
|
|
@ -328,6 +328,20 @@ fn french_and_portuguese_count_zero_as_singular() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_and_korean_have_no_plural_forms() {
|
||||
let code = |pref: &str, system: Option<&str>| LANGS[resolve(pref, system)].code;
|
||||
assert_eq!(code("system", Some("ja-JP")), "ja");
|
||||
assert_eq!(code("system", Some("ko_KR.UTF-8")), "ko");
|
||||
assert_eq!(code("system", Some("ja,en-US")), "ja");
|
||||
for code in ["ja", "ko"] {
|
||||
let lang = LANGS.iter().position(|l| l.code == code).unwrap();
|
||||
assert!(!plural_one(lang, 0) && !plural_one(lang, 1) && !plural_one(lang, 2), "{code}");
|
||||
let ones: Vec<_> = LANGS[lang].ui.iter().filter(|(_, t)| matches!(t, Text::Plural(Some(_), _))).map(|(k, _)| k).collect();
|
||||
assert!(ones.is_empty(), "{code}: plural entries give only `other`: {ones:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_key_scanner_reads_rust_literals() {
|
||||
let src = r#"x(tr("A \"b\"")); trf(
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ pub mod assets_panel;
|
|||
pub mod bidi;
|
||||
pub mod branch_ui;
|
||||
pub mod canvas;
|
||||
pub mod cjk;
|
||||
pub mod colorpicker;
|
||||
pub mod comments;
|
||||
pub mod component_panel;
|
||||
|
|
@ -406,8 +407,11 @@ pub struct FigApp {
|
|||
pub services: Box<dyn Services>,
|
||||
worker: Option<render_worker::Worker>,
|
||||
fonts_installed: bool,
|
||||
/// System faces for scripts the built-in fonts lack were added (after the font scan).
|
||||
script_fonts: bool,
|
||||
/// System faces for scripts the built-in fonts lack were added (after the font scan), with
|
||||
/// faces for these CJK scripts ([`cjk`]).
|
||||
script_fonts: Option<u8>,
|
||||
/// The tab (uid) and revision whose names were last checked for CJK scripts.
|
||||
cjk_checked: Option<(u64, u64)>,
|
||||
/// The language preference the UI strings follow (applied when it changes).
|
||||
language: Option<String>,
|
||||
/// A field had the keyboard when the last frame ended.
|
||||
|
|
@ -433,7 +437,8 @@ impl FigApp {
|
|||
services,
|
||||
worker: None,
|
||||
fonts_installed: false,
|
||||
script_fonts: false,
|
||||
script_fonts: None,
|
||||
cjk_checked: None,
|
||||
language: None,
|
||||
field_focus: false,
|
||||
control_queue: Vec::new(),
|
||||
|
|
@ -663,20 +668,52 @@ impl FigApp {
|
|||
self.session.prefs.reduce_motion || self.services.reduce_motion()
|
||||
}
|
||||
|
||||
/// The CJK scripts the UI shows: the language's, the language picker's (Preferences open),
|
||||
/// typed text's, and those in the open file's names and comments (read when it changes, not
|
||||
/// mid-drag). Nothing is checked once every script has a face.
|
||||
fn cjk_needed(&mut self, ctx: &egui::Context, installed: u8) -> u8 {
|
||||
if installed == cjk::KANA | cjk::HANGUL | cjk::HAN {
|
||||
return installed;
|
||||
}
|
||||
let mut need = i18n::current().map_or(0, |l| cjk::language(l.code));
|
||||
if self.ui.preferences {
|
||||
need |= i18n::languages().iter().filter(|l| cjk::can_show(l.code)).fold(0, |m, l| m | cjk::language(l.code));
|
||||
}
|
||||
need |= ctx.input(|i| {
|
||||
i.events.iter().fold(0, |m, e| match e {
|
||||
egui::Event::Text(t) | egui::Event::Ime(egui::ImeEvent::Preedit { text: t, .. } | egui::ImeEvent::Commit(t)) => m | cjk::scripts(t),
|
||||
_ => m,
|
||||
})
|
||||
});
|
||||
let dragging = ctx.input(|i| i.pointer.any_down());
|
||||
if let Some(d) = self.session.docs.get(self.session.active).filter(|d| !dragging && self.cjk_checked != Some((d.uid, d.revision))) {
|
||||
self.cjk_checked = Some((d.uid, d.revision));
|
||||
let doc = &d.doc;
|
||||
let comments = doc.comments.iter().flat_map(|c| std::iter::once(&c.first).chain(&c.replies)).flat_map(|m| [&m.author, &m.text]);
|
||||
need |= std::iter::once(&doc.name).chain(doc.nodes().map(|n| &n.name)).chain(comments).fold(0, |m, s| m | cjk::scripts(s));
|
||||
}
|
||||
need
|
||||
}
|
||||
|
||||
/// Per-frame logic that doesn't draw (control requests, theme, language).
|
||||
pub fn logic(&mut self, ctx: &egui::Context) {
|
||||
if !self.fonts_installed {
|
||||
theme::install_fonts(ctx, false);
|
||||
self.fonts_installed = true;
|
||||
} else if !self.script_fonts && figcraft_text::system_fonts_scanned() {
|
||||
// Once the system fonts are indexed: faces for names in scripts the UI fonts lack.
|
||||
self.script_fonts = true;
|
||||
theme::install_fonts(ctx, true);
|
||||
}
|
||||
if self.language.as_ref() != Some(&self.session.prefs.language) {
|
||||
i18n::set(&self.session.prefs.language, self.services.system_language().as_deref());
|
||||
self.language = Some(self.session.prefs.language.clone());
|
||||
}
|
||||
if !self.fonts_installed {
|
||||
theme::install_fonts(ctx, false, 0);
|
||||
self.fonts_installed = true;
|
||||
} else if figcraft_text::system_fonts_scanned() {
|
||||
// Once the system fonts are indexed: faces for names in scripts the UI fonts lack, and
|
||||
// for each CJK script once something shows it.
|
||||
let installed = self.script_fonts.unwrap_or(0);
|
||||
let cjk = installed | self.cjk_needed(ctx, installed);
|
||||
if self.script_fonts != Some(cjk) {
|
||||
self.script_fonts = Some(cjk);
|
||||
theme::install_fonts(ctx, true, cjk);
|
||||
}
|
||||
}
|
||||
let dark = match self.session.prefs.theme.as_str() {
|
||||
"dark" => true,
|
||||
"system" => ctx.global_style().visuals.dark_mode,
|
||||
|
|
|
|||
|
|
@ -227,8 +227,9 @@ pub fn on_dark<R>(ui: &mut egui::Ui, f: impl FnOnce(&mut egui::Ui) -> R) -> R {
|
|||
}
|
||||
|
||||
/// UI fonts: Inter (from the craft-fonts build input) when present, else egui's defaults; with
|
||||
/// `system`, also system faces for scripts those lack (Hebrew, joined Arabic) so names in them show.
|
||||
pub fn install_fonts(ctx: &Context, system: bool) {
|
||||
/// `system`, also system faces for scripts those lack (Hebrew, joined Arabic) so names in them show,
|
||||
/// and faces for the CJK scripts in `cjk` ([`crate::cjk`]).
|
||||
pub fn install_fonts(ctx: &Context, system: bool, cjk: u8) {
|
||||
let mut fonts = FontDefinitions::default();
|
||||
let find = |family: &str, style: &str| figcraft_text::CRAFT_FONTS.iter().find(|f| f.family == family && f.style == style).map(|f| f.bytes);
|
||||
let regular = find("Inter", "Regular");
|
||||
|
|
@ -251,9 +252,10 @@ pub fn install_fonts(ctx: &Context, system: bool) {
|
|||
}
|
||||
semi.extend(fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default());
|
||||
fonts.families.insert(FontFamily::Name("semibold".into()), semi);
|
||||
// CJK and other scripts from craft-fonts as fallbacks.
|
||||
// CJK and other scripts from craft-fonts as fallbacks (Noto Sans CJK only with FIGCRAFT_EMBED_CJK).
|
||||
const BUILT_IN: [&str; 4] = ["Inter", "BIZ UDPGothic", "Noto Sans CJK SC", "Noto Sans Arabic"];
|
||||
for f in figcraft_text::CRAFT_FONTS.iter().filter(|f| f.family != "Inter" && f.style == "Regular") {
|
||||
if matches!(f.family, "BIZ UDPGothic" | "Noto Sans Arabic") {
|
||||
if BUILT_IN.contains(&f.family) {
|
||||
let key = format!("craft-{}", f.family);
|
||||
fonts.font_data.insert(key.clone(), Arc::new(FontData::from_static(f.bytes)));
|
||||
for fam in [FontFamily::Proportional, FontFamily::Name("semibold".into())] {
|
||||
|
|
@ -266,13 +268,15 @@ pub fn install_fonts(ctx: &Context, system: bool) {
|
|||
if system {
|
||||
// What the built-in faces above already draw.
|
||||
let embedded = |c: char| {
|
||||
["Inter", "BIZ UDPGothic", "Noto Sans Arabic"]
|
||||
BUILT_IN
|
||||
.into_iter()
|
||||
.filter(|f| figcraft_text::CRAFT_FONTS.iter().any(|cf| cf.family == *f))
|
||||
.filter_map(|f| figcraft_text::find_face(f, "Regular"))
|
||||
.any(|x| x.has_char(c))
|
||||
};
|
||||
let extra = crate::bidi::script_fallbacks(&embedded);
|
||||
let mut extra = crate::bidi::script_fallbacks(&embedded);
|
||||
let lang = crate::i18n::current().map_or("en", |l| l.code);
|
||||
extra.extend(crate::cjk::faces(cjk, lang, &embedded).iter().map(|f| (format!("system-{}-{}", f.family, f.style), font_data(f))));
|
||||
for (key, data) in extra {
|
||||
fonts.font_data.insert(key.clone(), Arc::new(data));
|
||||
for fam in [FontFamily::Proportional, FontFamily::Name("semibold".into())] {
|
||||
|
|
@ -285,6 +289,19 @@ pub fn install_fonts(ctx: &Context, system: bool) {
|
|||
ctx.set_fonts(fonts);
|
||||
}
|
||||
|
||||
/// egui font data over a face's bytes, without copying them (CJK faces are 10–20 MB). Font
|
||||
/// database faces are never dropped, so a system face's shared bytes are kept for good: leaking
|
||||
/// one more reference to them costs a pointer and gives egui the `'static` slice it needs.
|
||||
pub fn font_data(face: &figcraft_text::Face) -> FontData {
|
||||
let bytes: &'static [u8] = match &face.bytes {
|
||||
figcraft_text::fontdb::FontBytes::Static(b) => b,
|
||||
figcraft_text::fontdb::FontBytes::Shared(v) => Box::leak(Box::new(v.clone())).as_slice(),
|
||||
};
|
||||
let mut data = FontData::from_static(bytes);
|
||||
data.index = face.index;
|
||||
data
|
||||
}
|
||||
|
||||
pub fn semibold(size: f32) -> FontId {
|
||||
FontId::new(size, FontFamily::Name("semibold".into()))
|
||||
}
|
||||
|
|
|
|||
25
docs/gaps.md
25
docs/gaps.md
|
|
@ -1,6 +1,6 @@
|
|||
# Where FigCraft falls short of Figma
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from
|
||||
doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The
|
||||
|
|
@ -18,7 +18,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
|||
| 2 | Multiplayer and sharing are a preview, without accounts (**beta blocker**) | Feature | 42–84 | [collaboration-parity](collaboration-parity.md) |
|
||||
| 3 | Nothing checked against real Figma files | Fidelity | 15–30 | [target-app-parity](target-app-parity.md) |
|
||||
| 4 | Interaction fidelity not compared tool by tool | UI/UX | 40–60 | [ui-parity](ui-parity.md) |
|
||||
| 5 | Localization: English plus unreviewed Spanish, French, German and Brazilian Portuguese drafts; no Japanese or Korean | Localization | 40–65 | [localization-parity](localization-parity.md) |
|
||||
| 5 | Localization: English plus unreviewed Spanish, French, German, Brazilian Portuguese, Japanese and Korean drafts | Localization | 32–55 | [localization-parity](localization-parity.md) |
|
||||
| 6 | Plugins: no distribution, widgets take clicks only; Figma's plugins don't run | Ecosystem | 10–30 | [target-app-parity](target-app-parity.md) |
|
||||
| 7 | CPU canvas where Figma uses the GPU | Hardware/performance | 30–60 | [hardware-parity](hardware-parity.md) |
|
||||
| 8 | Platform QA: Linux and the browser left (Windows pass done 2026-10-11) | Stability/platforms | 10–20 | [hardware-parity](hardware-parity.md) |
|
||||
|
|
@ -100,19 +100,21 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
|||
- **Impact:** Figma users notice the feel within minutes; it decides whether they stay.
|
||||
- **Estimate:** 40–60 h. Detail in [`ui-parity.md`](ui-parity.md).
|
||||
|
||||
### 5. Localization: English plus unreviewed drafts; no Japanese or Korean
|
||||
- **Missing:** Figma's Japanese and Korean, native reviews of the Spanish, French, German and
|
||||
Brazilian Portuguese drafts, localized engine messages and number formatting.
|
||||
### 5. Localization: English plus unreviewed drafts
|
||||
- **Missing:** native reviews of the Spanish, French, German, Brazilian Portuguese, Japanese and
|
||||
Korean drafts, Latin American Spanish, localized engine messages and number formatting; CJK UI
|
||||
text on the web without the craft-fonts CJK build.
|
||||
- **Evidence (measured from source):** the layer landed 2026-10-11: catalogs in
|
||||
`crates/ui-egui/locales/` (1,351 UI keys plus 519 command labels and 29 menu wordings),
|
||||
Preferences ▸ Language, `pref.language`; `en.toml` is complete, and `es.toml`, `fr.toml`,
|
||||
`de.toml` and `pt-BR.toml` (2026-10-12) are complete machine drafts (only Spanish is held to
|
||||
completeness by the check; the others show English for strings added later). Engine error
|
||||
messages and Dev Mode values stay English.
|
||||
- **Impact:** keeps out non-English designers, including Figma's large Japanese and Korean user
|
||||
bases.
|
||||
`de.toml`, `pt-BR.toml`, `ja.toml` and `ko.toml` (2026-10-12) are complete machine drafts (only
|
||||
Spanish is held to completeness by the check; the others show English for strings added later).
|
||||
CJK UI text uses an installed CJK face the desktop app loads when something on screen needs it
|
||||
(`crates/ui-egui/src/cjk.rs`). Engine error messages and Dev Mode values stay English.
|
||||
- **Impact:** non-English designers get machine drafts nobody has reviewed yet, and English
|
||||
errors.
|
||||
- **Estimate:** 3–7 h per language plus native review, 4–6 h for engine messages and numbers;
|
||||
40–65 h for the key languages left and the drafts' follow-ups. Detail in
|
||||
32–55 h for the key languages left and the drafts' follow-ups. Detail in
|
||||
[`localization-parity.md`](localization-parity.md); how to add one in
|
||||
[`translating.md`](translating.md).
|
||||
|
||||
|
|
@ -334,6 +336,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Gap 5 40–65 h → 32–55 h, rank unchanged |
|
||||
| 2026-10-12 | minor | Plugin widgets (API 3) and the panel tail (panels side by side, colour picker on plugin fields) added to gap 6's landed list; gap 6 now 10–30 h; ranking unchanged |
|
||||
| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Gap 5 45–75 h → 40–65 h, rank unchanged |
|
||||
| 2026-10-12 | minor | Cursor chat and spotlight in live sessions (gap 2): `/` opens a chat bubble at your pointer, a spotlight makes everyone follow your view; ephemeral presence the host validates and rate-limits. Gap 2 45–90 h → 42–84 h |
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
# Localization parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (French, German and Brazilian Portuguese drafts) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Japanese and Korean drafts, CJK UI fonts) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
FigCraft's interface has a localization layer with an English catalog and complete Spanish, French,
|
||||
German and Brazilian Portuguese drafts (machine-drafted, waiting for native speakers' review). This
|
||||
German, Brazilian Portuguese, Japanese and Korean drafts (machine-drafted, waiting for native
|
||||
speakers' review). This
|
||||
page lists the twelve key languages of the craftrules standard, then the other languages Figma
|
||||
ships. How to add a language: [`translating.md`](translating.md).
|
||||
|
||||
|
|
@ -28,20 +29,20 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v
|
|||
and 29 menu wordings by command id: 1,899 entries. Command ids, the CLI, MCP and the control
|
||||
channel stay English. Accessibility names come from the same strings.
|
||||
- **Language preference:** Preferences ▸ Language (System, English, Deutsch, Español, Français,
|
||||
Português (Brasil)), applied at once without a restart; System takes the first of the OS's
|
||||
日本語, 한국어, Português (Brasil)), applied at once without a restart; System takes the first of the OS's
|
||||
preferred languages that has a catalog (Windows' language list, macOS AppleLanguages, `LANGUAGE`
|
||||
then the locale variables on Linux and BSD; a regional tag falls back to its language, and pt-PT
|
||||
or pt to pt-BR) or the browser's language on the web. `pref.language {language}` sets it from
|
||||
the CLI, MCP and the control channel.
|
||||
- **Check:** `cargo test -p figcraft-ui-egui i18n` fails on keys the code uses that `en.toml`
|
||||
lacks, keys no code uses, unknown command ids, placeholders that differ from English and, for
|
||||
complete languages (Spanish), anything untranslated. The French, German and Brazilian Portuguese
|
||||
drafts translate everything today but are not held to it: a string added later shows English
|
||||
complete languages (Spanish), anything untranslated. The French, German, Brazilian Portuguese,
|
||||
Japanese and Korean drafts translate everything today but are not held to it: a string added later shows English
|
||||
there until someone translates it.
|
||||
- **Not yet localized:** messages the engine writes (errors, a few toasts, Dev Mode property values
|
||||
such as "inside"), number formatting (decimal points stay `.`), and plural forms beyond one/other
|
||||
(French and Brazilian Portuguese take the singular for 0 as well; CLDR's "many" for millions
|
||||
reads as the plural).
|
||||
(French and Brazilian Portuguese take the singular for 0 as well; Japanese and Korean have one
|
||||
form; CLDR's "many" for millions reads as the plural).
|
||||
- **Script support in documents:** Unicode bidi with Arabic shaping and mirrored brackets in text
|
||||
layers; right-to-left list markers and indents; right-to-left names in the layers panel, tabs
|
||||
and canvas labels (visual order, joined Arabic); inline IME composition with the candidate window
|
||||
|
|
@ -50,23 +51,35 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v
|
|||
- **Script support in the UI:** menus widen to their longest label; dropdowns, the two-column
|
||||
labels of the right panel and the home screen's cards keep a long translation on one line with an
|
||||
ellipsis and show it in full on hover (checked in German, the longest); no mirrored
|
||||
(right-to-left) layout of panels.
|
||||
(right-to-left) layout of panels. CJK translations are shorter than English; the same ellipsis
|
||||
and tooltip cover the few that don't fit (the sizing dropdowns).
|
||||
- **CJK UI fonts:** egui's fonts and Inter have no CJK glyphs. Once the UI language, the open
|
||||
file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han,
|
||||
the desktop app adds one installed face per script to the UI's fallbacks (Windows: Yu Gothic UI,
|
||||
Meiryo, Malgun Gothic, Microsoft YaHei; macOS: Hiragino Sans, Apple SD Gothic Neo, PingFang SC;
|
||||
Linux and BSD: Noto Sans CJK, Source Han Sans, Nanum, WenQuanYi; the craft-fonts build's BIZ
|
||||
UDPGothic first), the UI language's script first; Han without kana outside a Japanese or Korean UI
|
||||
reads as Chinese. Only that face's file is read and egui shares its bytes, so English users load
|
||||
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 (+16 MB) and offers Japanese and Korean; otherwise the language picker and
|
||||
System leave them out. No font file is in this repo.
|
||||
|
||||
## Languages
|
||||
|
||||
| Language | Code | UI strings translated | Dialogs / tooltips / help | Script support | Native review | Status | Hours to full |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| English | en | 1,899 / 1,899 (100%, source) | yes | Latin | yes (source) | full | 0 |
|
||||
| Simplified Chinese (Mandarin) | zh-Hans | 0 (0%) | no | CJK text, IME and font fallback work in documents; UI font needs CJK coverage | no | none | 4–7 |
|
||||
| Simplified Chinese (Mandarin) | zh-Hans | 0 (0%) | no | CJK text, IME and font fallback work in documents; CJK UI faces load on demand (desktop) | no | none | 3–5 |
|
||||
| Spanish | es | 1,899 / 1,899 (100%, draft) | yes | Latin | no | draft | 1–2 (es-419 differences, after native review) |
|
||||
| Hindi | hi | 0 (0%) | no | Devanagari shaping works in documents; UI font fallback to check | no | none | 4–7 |
|
||||
| Arabic | ar | 0 (0%) | no | Bidi, shaping and RTL names work; the UI does not mirror | no | none | 9–15 |
|
||||
| French | fr | 1,899 / 1,899 (100%, draft) | yes | Latin | no | draft | 1–2 (after native review) |
|
||||
| Portuguese | pt-BR | 1,899 / 1,899 (100%, draft) | yes | Latin | no | draft | 1–2 (after native review) |
|
||||
| Indonesian | id | 0 (0%) | no | Latin | no | none | 3–5 |
|
||||
| Japanese | ja | 0 (0%) | no | CJK text and IME work in documents; UI font needs CJK coverage | no | none | 4–7 |
|
||||
| Japanese | ja | 1,899 / 1,899 (100%, draft) | yes | CJK text and IME in documents; UI text in an installed Japanese face (web: the `FIGCRAFT_EMBED_CJK` build) | no | draft | 1–2 (after native review) |
|
||||
| German | de | 1,899 / 1,899 (100%, draft) | yes | Latin; long words elide in narrow fields | no | draft | 1–2 (after native review) |
|
||||
| Korean | ko | 0 (0%) | no | Hangul text and IME work in documents; UI font needs coverage | no | none | 3–5 |
|
||||
| Korean | ko | 1,899 / 1,899 (100%, draft) | yes | Hangul text and IME in documents; UI text in an installed Korean face (web: the `FIGCRAFT_EMBED_CJK` build) | no | draft | 1–2 (after native review) |
|
||||
| Vietnamese | vi | 0 (0%) | no | Latin with stacked diacritics | no | none | 3–5 |
|
||||
|
||||
Other languages: none shipped (Figma ships none beyond the eight above).
|
||||
|
|
@ -75,15 +88,16 @@ The layer is done. Per-language hours above are agent hours for a catalog of ~1,
|
|||
(a machine draft, then a pass over the panels for overflow), plus each language's plural rule where
|
||||
it isn't one/other; every language also needs a native speaker's review (human). Left before full
|
||||
parity besides the languages: localizing engine messages and number formatting (4–6 h);
|
||||
right-to-left mirroring of the panels is in Arabic's hours. Total: **40–65 h**.
|
||||
right-to-left mirroring of the panels is in Arabic's hours. Total: **32–55 h**.
|
||||
|
||||
Priority: Japanese and Korean next (the last of Figma's own languages, so their users expect
|
||||
them), and native reviews of the Spanish, French, German and Brazilian Portuguese drafts.
|
||||
Priority: native reviews of the six drafts (every one of Figma's languages now has one; Latin
|
||||
American Spanish is the last Figma variant), then Simplified Chinese, whose UI fonts already work.
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Localization 30% → 35%, 40–65 h → 32–55 h |
|
||||
| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): `fr.toml`, `de.toml` and `pt-BR.toml` translate every UI string (1,351 keys, 36 with plural forms), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; they fall back to English per key, so new strings need only English and Spanish. French and Brazilian Portuguese count 0 as singular (`plural_one`); System picks the first OS language with a catalog (Windows' and macOS's preferred-language lists, `LANGUAGE` then the locale on Linux and BSD; pt-PT and pt read pt-BR); Preferences ▸ Language lists Français, Deutsch and Português (Brasil). Dropdowns, two-column labels and the home cards keep long translations on one line with an ellipsis and show them in full on hover. Localization 20% → 30%, 45–75 h → 40–65 h |
|
||||
| 2026-10-11 | major | Localization layer landed (gap 5): catalogs per language baked in at build time, English text as keys and fallback, `{named}` placeholders and one/other plurals, command labels by command id, Preferences ▸ Language (System follows the OS or the browser) applied live, `pref.language`, and a catalog check; English catalog (1,280 UI keys) and a complete Spanish draft (1,810 entries with command labels) waiting for native review; localization 10% → 20%, 60–100 h → 45–75 h |
|
||||
| 2026-10-10 | major | Created: English only, string counts measured from source, Figma's eight UI languages from its help center |
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ on; `key` resolves keys the way the canvas does. Commands behind keys:
|
|||
nested layers), Escape or a click drops it.
|
||||
- `pref.reduceMotion {on?}`: reduced motion (the interface's own animations off; the system's
|
||||
accessibility setting turns it on too). Prototype transitions still play.
|
||||
- `pref.language {language: system|en|es|fr|de|pt-BR|…}`: the app's interface language, applied live ("system":
|
||||
- `pref.language {language: system|en|es|fr|de|pt-BR|ja|ko|…}`: the app's interface language, applied live ("system":
|
||||
the OS or browser language; a language without a catalog shows English). Command ids, labels and
|
||||
every result stay English.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# FigCraft roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; `.fig` import, best effort; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; `.fig` import, best effort; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and
|
||||
[`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours
|
||||
|
|
@ -15,8 +15,9 @@ are one Opus 5.5 agent; most milestones split across 4–6 agents on disjoint cr
|
|||
exports, turn estimated percentages into measured ones: 15–30 h.
|
||||
3. **Tool-by-tool interaction pass** against the running Figma ([`ui-parity.md`](ui-parity.md)):
|
||||
40–60 h, parallel by tool.
|
||||
4. **Languages** on the landed localization layer, Figma's own first (ja, ko; es, fr, de and pt-BR
|
||||
are drafts awaiting native review): 3–7 h per language plus native review.
|
||||
4. **Languages** on the landed localization layer: native reviews of the es, fr, de, pt-BR, ja and
|
||||
ko drafts (people), Latin American Spanish, then the other key languages: 3–7 h per language
|
||||
plus native review.
|
||||
5. **Multiplayer M9**: accounts and teams on top of the live sessions and the standalone server (the WebSocket transport, hosting from the app, invite links that open the app, document sync, live cursors, selections, text carets, avatars and follow, per-user undo, reconnect with offline edits, collision-free ids for styles, variables, collections and comments, joining from the browser, and `figcraft-server` with persistence, publishing from the app and share links to edit or view landed), per [`collab-protocol.md`](collab-protocol.md) and [`server.md`](server.md): 45–90 h.
|
||||
|
||||
## Alpha gate
|
||||
|
|
@ -72,6 +73,7 @@ blockers.
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Current focus 4 now lists the drafts' native reviews |
|
||||
| 2026-10-12 | minor | M11: plugin widgets (API 3: plugin-drawn layers on the canvas, redrawn on clicks from state kept in the document), panels of several plugins side by side and the colour picker on plugin colour fields; 15–40 h → 10–30 h |
|
||||
| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Current focus 4 now lists Japanese and Korean |
|
||||
| 2026-10-11 | minor | `.fig` import, best effort: current focus 1 and the alpha gate's `.fig` row move to the import's tail (30–60 h → 8–15 h); M6 ~55% → ~65%, 37–75 h → 15–30 h |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# FigCraft parity with Figma
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS)
|
||||
|
||||
The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md)
|
||||
summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is
|
||||
|
|
@ -17,11 +17,11 @@ summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu c
|
|||
|---|---|---|
|
||||
| Menu/command breadth | **100%** (204 / 204 catalogued menu items have a live command) | **measured** (`cargo xtask parity`, 2026-10-11) |
|
||||
| Feature breadth (does each Figma Design feature exist, weighted) | **~84%** (core editor areas 1–16: ~94%) | estimated from the area table below |
|
||||
| Ready for real work | **~52%** | estimated (dimension table below: written weights × written per-dimension scores) |
|
||||
| Ready for real work | **~53%** | estimated (dimension table below: written weights × written per-dimension scores) |
|
||||
| Essentials user | **~57%** | estimated ([below](#essentials-user): core features 77.8% × discounts 0.92 × 0.92 × 0.88) |
|
||||
| Mainstream practitioner | **~49%** | estimated ([below](#mainstream-practitioner): weekly areas 72.6% × discounts 0.9 × 0.9 × 0.85) |
|
||||
| Remaining to beta | **~162–292 h** one Opus 5.5 agent (~33–67 h wall-clock with 5 parallel agents) | estimated |
|
||||
| Remaining to full parity | **~276–538 h** one agent (~85–148 h wall-clock with 5 parallel agents) | estimated |
|
||||
| Remaining to beta | **~155–280 h** one Opus 5.5 agent (~32–64 h wall-clock with 5 parallel agents) | estimated |
|
||||
| Remaining to full parity | **~268–528 h** one agent (~83–145 h wall-clock with 5 parallel agents) | estimated |
|
||||
| Stage | **alpha**: the alpha gate passes ([roadmap](roadmap.md#alpha-gate)); multiplayer and checking the best-effort `.fig` import on real files are beta gaps (see [ROADMAP](../ROADMAP.md#stage)) | |
|
||||
|
||||
The previous figures (ROADMAP of 2026-10-10, "feature depth ~79%", "full parity 115–195 h") were
|
||||
|
|
@ -35,7 +35,7 @@ tool; presence was being graded as behaviour in places.
|
|||
|
||||
| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates |
|
||||
|---|---|---|---|
|
||||
| Full target (ready for real work) | ~52% | ~281–540 h | multiplayer (42–84), localization (40–65), plugins (10–30), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) |
|
||||
| Full target (ready for real work) | ~53% | ~273–530 h | multiplayer (42–84), localization (32–55), plugins (10–30), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) |
|
||||
| Mainstream practitioner | ~49% | ~130–244 h | core editor depth incl. the `.fig` import's tail (49–98), the interaction pass (40–60), real-file corpus and platform QA (25–50), `.fig` export for handing work back (15–30) |
|
||||
| Essentials user | ~57% | ~60–110 h | the `.fig` import's tail so the files people send open well (5–10), core tool polish (25–45), interaction pass on the core tools (15–25), stability QA (10–20), onboarding and starter templates (5–10) |
|
||||
|
||||
|
|
@ -129,11 +129,11 @@ Breadth: does it exist. Ready: depth, correctness and fidelity for real projects
|
|||
| Stability | 8 | 55% | 20–40 | Never-crash lints, command guard with rollback, recovery copies, 963 tests incl. junk-parameter sweeps and a seeded retained-render walk; but two days old, one release (v0.2.0), no field reports, QA mostly on macOS | [gaps](gaps.md) |
|
||||
| Performance | 6 | 60% | 30–60 | `cargo xtask bench`: 50k-layer page ~8 ms zoomed out, 10k-layer edits ~0.2 ms, 2,000-instance design system edits 4–64 ms; CPU (tiny-skia) canvas where Figma renders on the GPU; nothing timed against Figma on the same file | [hardware-parity](hardware-parity.md) |
|
||||
| Hardware | 3 | 60% | 10–21 | Trackpad pinch and HiDPI yes; pen pressure on Windows and from browser styluses (none from macOS/Linux tablets); several windows (screen-reader tree in the main one only); no GPU canvas, no P3 on screen | [hardware-parity](hardware-parity.md) |
|
||||
| Localization | 3 | 30% | 40–65 | The layer, an English catalog and complete machine drafts of Spanish, French, German and Brazilian Portuguese (none reviewed by a native speaker); Figma's Japanese, Korean and Latin American Spanish are missing; engine messages stay English | [localization-parity](localization-parity.md) |
|
||||
| Localization | 3 | 35% | 32–55 | The layer, an English catalog and complete machine drafts of Spanish, French, German, Brazilian Portuguese, Japanese and Korean (none reviewed by a native speaker), CJK UI text in installed faces loaded on demand (web: only the craft-fonts CJK build); Latin American Spanish is missing; engine messages stay English | [localization-parity](localization-parity.md) |
|
||||
| Platforms | 3 | 75% | 10–20 | macOS, Windows (x64/x86/arm64), Linux, FreeBSD, web all packaged (v0.2.0); Figma also has iOS/Android apps; non-macOS QA thin | [hardware-parity](hardware-parity.md) |
|
||||
| Ecosystem/plugins | 3 | 34% | 10–30 | Own WASM plugins with a Rust SDK and three examples (#101), plugin panels and settings (ABI v2), widgets that take clicks (API 3); no Figma plugin compatibility, no distribution or community files; the CLI, control channel and MCP server are ahead of Figma for agents | [gaps](gaps.md) |
|
||||
| AI features | 2 | 0% | 30–60 | None | [gaps](gaps.md) |
|
||||
| **Ready for real work** | 100 | **~52%** | **~276–538** | | |
|
||||
| **Ready for real work** | 100 | **~53%** | **~268–528** | | |
|
||||
|
||||
Dimension hours add up to the full-parity total without double counting: the feature-area rows
|
||||
for import/export (14), multiplayer and team libraries (17, 19), plugins (18) and AI (21) are
|
||||
|
|
@ -204,7 +204,7 @@ from anyone but the maintainers (`gh issue list`, `gh api .../issues/comments`;
|
|||
private, 1 star, PRs only from maintainers). No outside evidence either way; nothing raises the
|
||||
number.
|
||||
|
||||
**Why it is lower than ready for real work (~52%), unusually.** The full number averages
|
||||
**Why it is lower than ready for real work (~53%), unusually.** The full number averages
|
||||
dimensions additively, so the missing `.fig` exchange costs only the file-format dimension's 12%
|
||||
weight, and FigCraft earns points the mainstream number doesn't count (platforms 75%: Linux,
|
||||
FreeBSD and the web, where Figma has no desktop app). The mainstream number applies file exchange
|
||||
|
|
@ -617,6 +617,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Localization 30% → 35% (drafts, not reviewed: graded low), 40–65 h → 32–55 h; ready for real work ~52% → ~53% (52.9 → 53.1 by the dimension table); full parity ~284–554 h → ~276–544 h; beta ~162–292 h → ~155–280 h (Japanese and Korean leave the beta list; their native reviews need people) |
|
||||
| 2026-10-12 | minor | Plugin widgets and the panel tail (gap 6, plugin API 3; v1 and v2 plugins unchanged): a plugin marked as a widget puts a layer of its own on the canvas from Plugins ▸ Widgets or `widget.insert`: it describes the layer (frames with auto layout, text, rectangles, ellipses, images it carries) and FigCraft validates the description (size, layer count, depth, numbers, colours, text and image sizes) and builds it as a frame with locked layers inside; a click on a part the plugin named (on the canvas with the Move tool, a pointing hand over it, or `widget.click`) calls the plugin back in a fresh, metered sandbox with the widget's state, as one undo step, and the widget is redrawn; the state lives in the widget's own layer data, so widgets survive save and reload, copying, undo, export and live sessions; no plugin can write that data or call `widget.*`. `widget.list` lists them. A Counter example widget. Plugin panels of different plugins open side by side, and a colour field's swatch opens the colour picker (the plugin hears of the colour when a drag ends). Plugins & widgets 45%/28% → 55%/32%, 15–40 h → 10–30 h; Ecosystem dimension 28% → 34%; weighted totals unchanged at this rounding; full parity ~286–558 h → ~281–548 h |
|
||||
| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Localization 20% → 30%, 45–75 h → 40–65 h; ready for real work unchanged (~52%; 52.6 → 52.9); full parity ~289–564 h → ~284–554 h; beta ~170–305 h → ~162–292 h (Figma's languages left on the way to beta: Japanese and Korean, 7–12 h) |
|
||||
| 2026-10-12 | minor | Cursor chat and spotlight in live sessions: `/` opens a chat bubble at your pointer that everyone sees as you type and that fades a few seconds after the last change; a spotlight (your avatar's menu, `collab.spotlight`) makes everyone follow your view and page, latecomers included, until they move their own. Both are ephemeral presence the host validates and rate-limits, passed only to clients that read them. Multiplayer & sharing breadth 15% → 17%, ready unchanged (5%), 45–90 h → 42–84 h; the Collaboration dimension's hours 55–110 → 52–104 h (the dimension hours still add up to the total); feature breadth (~84%) and ready for real work (~51%) unchanged at this rounding; full parity ~301–587 h → ~298–581 h |
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ a catalog (the browser's language on the web): a regional tag reads its language
|
|||
(`fr-CA` → `fr`), and a language with only a regional catalog reads that one (`pt-PT` → `pt-BR`).
|
||||
`pref.language {language}` does the same from the CLI, MCP and the control channel.
|
||||
|
||||
The catalogs: English (`en`, the source), and Spanish (`es`), French (`fr`), German (`de`) and
|
||||
Brazilian Portuguese (`pt-BR`), machine-drafted and waiting for native speakers' review.
|
||||
The catalogs: English (`en`, the source), and Spanish (`es`), French (`fr`), German (`de`),
|
||||
Brazilian Portuguese (`pt-BR`), Japanese (`ja`) and Korean (`ko`), machine-drafted and waiting for
|
||||
native speakers' review.
|
||||
|
||||
## The catalogs
|
||||
|
||||
|
|
@ -29,7 +30,15 @@ A key a catalog lacks shows its English text, so a partial catalog is safe to sh
|
|||
|
||||
Typography follows each language: French puts a no-break space (`\u00A0` in the TOML) before
|
||||
`:` `;` `?` `!`, inside « » and before `%`; German uses „ “ and formal address (Sie); Brazilian
|
||||
Portuguese uses Brazilian words (arquivo, salvar, tela, você), not European ones.
|
||||
Portuguese uses Brazilian words (arquivo, salvar, tela, você), not European ones. Japanese is
|
||||
concise, without です/ます endings, with full-width ()「」:、。 where Japanese UI text has them;
|
||||
Korean labels end without 합니다 (instructions in -세요). Neither has plural forms.
|
||||
|
||||
CJK text needs a CJK font, which egui and Inter lack: the desktop app adds an installed one when
|
||||
something on screen needs it (see `crates/ui-egui/src/cjk.rs`). The web build has no system fonts,
|
||||
so Japanese and Korean appear there only in a build with craft-fonts and `FIGCRAFT_EMBED_CJK=1`
|
||||
(Noto Sans CJK, +16 MB); otherwise they are left out of the picker. A new CJK or other non-Latin
|
||||
language also needs its script in `cjk::language`.
|
||||
|
||||
## Adding a language
|
||||
|
||||
|
|
@ -38,13 +47,14 @@ Portuguese uses Brazilian words (arquivo, salvar, tela, você), not European one
|
|||
2. Translate every value; keep the keys. Keep `{placeholders}` exactly, "…" where English has it,
|
||||
brand names and keyboard glyphs. English labels of commands: `figcraft-cli commands`.
|
||||
3. Plurals: give `one` only if the language has a singular form (Japanese, Korean and Chinese give
|
||||
only `other`). `trn` picks `one` for a count of 1, and also for 0 in French and Brazilian
|
||||
Portuguese (keep `{n}` in their `one` forms); a language with other rules needs its rule in
|
||||
only `other`, and `plural_one` returns false for them). `trn` picks `one` for a count of 1, and
|
||||
also for 0 in French and Brazilian Portuguese (keep `{n}` in their `one` forms); a language with other rules needs its rule in
|
||||
`plural_one` in [`crates/ui-egui/src/i18n.rs`](../crates/ui-egui/src/i18n.rs).
|
||||
4. To require full coverage, add the code to `COMPLETE` in
|
||||
[`crates/ui-egui/src/i18n_tests.rs`](../crates/ui-egui/src/i18n_tests.rs). Every later PR that
|
||||
adds a string then has to translate it into each complete language, so only Spanish is there:
|
||||
French, German and Brazilian Portuguese are complete today but registered as drafts, and a
|
||||
French, German, Brazilian Portuguese, Japanese and Korean are complete today but registered as
|
||||
drafts, and a
|
||||
string added later shows English in them until it is translated. To list what a draft lacks,
|
||||
add its code to `COMPLETE` locally and run the check.
|
||||
5. Run the check and look at the panels in the app (longer words must fit the right panel):
|
||||
|
|
|
|||
Loading…
Reference in New Issue