mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-03 08:49:37 -06:00
Merge #257: M9: Russian interface translation (@VanTan666)
This commit is contained in:
@@ -227,7 +227,7 @@ The desktop app also offers a loopback-only control channel (`figcraft --control
|
||||
- **Its own renderer:** tiny-skia on every core for the canvas and exports, with layer blending, effects and masks; booleans from linesweeper, geometry from kurbo, text from skrifa and HarfRust.
|
||||
- **Runs in the browser:** the whole engine and UI compile to WebAssembly.
|
||||
- **Accessible:** screen readers get names, roles, values and states for the tools, layer rows, fields, dropdowns, tabs and menus (AccessKit on desktop), F6 moves the keyboard between the toolbar, layers, canvas and properties (Tab within them, Escape back to the canvas), the system's reduce-motion setting is honoured, and panel text meets WCAG AA contrast in both themes.
|
||||
- **Languages:** English and Spanish (a draft waiting for a native speaker's review), picked in Preferences or taken from the system or browser and switched live; adding a language is one catalog file ([`docs/translating.md`](docs/translating.md)).
|
||||
- **Languages:** English, Russian (reviewed by a native speaker), and Spanish, French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese drafts, picked in Preferences or taken from the system or browser and switched live; adding a language is one catalog file ([`docs/translating.md`](docs/translating.md)).
|
||||
- **Plugins:** sandboxed WebAssembly plugins (a metered, pure-Rust interpreter with a memory cap and no file or network access, in the browser too) that act through the same commands, each run one undo step, within the permissions their manifest asks for. A Rust SDK and two example plugins are in [`plugins/`](plugins); see [`docs/plugins.md`](docs/plugins.md).
|
||||
- **Live sessions (preview):** share the open file from the app (Share ▸ Start sharing: online through a server, or on your local network), give the others the short join code it shows (or the invite link), and edit it together: everyone's changes sync live, with cursors, selections, avatars and follow, and undo takes back only your own changes. Others join from the desktop app (invite links open it) or from the browser build. See [`docs/collab-protocol.md`](docs/collab-protocol.md).
|
||||
- **A server of your own (preview):** `figcraft-server` keeps a team's files, publishes them from the app (File ▸ Publish to server…, or Share ▸ Online with your server in Preferences) and hands out share links and join codes that let people edit or only view; files survive restarts and apps reconnect where they were. No accounts yet: links are the access. Releases ship it for Linux, and the desktop app joins over `wss://`; [hosting it on a rented Linux server](docs/server.md#deploy-on-a-linux-server) takes a systemd unit and Caddy or nginx (or Docker). See [`docs/server.md`](docs/server.md).
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
# To remove your names, delete your entry (or set fields empty) in a PR. The next
|
||||
# contributors.py run, and the next build, drop them.
|
||||
|
||||
[people.VanTan666]
|
||||
display_name = "VanTan"
|
||||
|
||||
[people.tylerjenningsw]
|
||||
real_name = "Tyler Jennings"
|
||||
display_name = "tylerjenningsw"
|
||||
|
||||
@@ -369,18 +369,24 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
Ok(json!({"url": s.prefs.publish_server, "default": crate::live::DEFAULT_SERVER}))
|
||||
}
|
||||
),
|
||||
spec!("pref.language", "Interface language", "{language: system|en|es|es-419|fr|de|pt-BR|ja|ko|zh-Hans|…; else 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".
|
||||
let tag = lang.len() <= 35
|
||||
&& lang.split(['-', '_']).all(|part| !part.is_empty() && part.len() <= 8 && part.chars().all(|c| c.is_ascii_alphanumeric()));
|
||||
if !tag {
|
||||
return Err(bad("pref.language", format!("`{lang}` is not a language code")));
|
||||
spec!(
|
||||
"pref.language",
|
||||
"Interface language",
|
||||
"{language: system|en|ru|es|es-419|fr|de|pt-BR|ja|ko|zh-Hans|…; else 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".
|
||||
let tag = lang.len() <= 35
|
||||
&& lang.split(['-', '_']).all(|part| !part.is_empty() && part.len() <= 8 && part.chars().all(|c| c.is_ascii_alphanumeric()));
|
||||
if !tag {
|
||||
return Err(bad("pref.language", format!("`{lang}` is not a language code")));
|
||||
}
|
||||
s.prefs.language = lang.to_string();
|
||||
Ok(json!({"language": s.prefs.language}))
|
||||
}
|
||||
s.prefs.language = lang.to_string();
|
||||
Ok(json!({"language": s.prefs.language}))
|
||||
}),
|
||||
),
|
||||
spec!("app.preferences", "Preferences", "{}", false, |s, _| ui(s, "dialog.preferences")),
|
||||
spec!("app.actions", "Actions", "{}", false, |s, _| ui(s, "palette.actions")),
|
||||
spec!("help.shortcuts", "Keyboard shortcuts", "{}", false, |s, _| ui(s, "panel.shortcuts")),
|
||||
|
||||
+11
-3
@@ -71,7 +71,7 @@ fn locales() -> String {
|
||||
o
|
||||
}
|
||||
|
||||
/// The rows of one catalog table: `"key" = "text"` or `"key" = { one = "…", other = "…" }`.
|
||||
/// The rows of one catalog table: strings or plural forms (`other` required).
|
||||
fn table(o: &mut String, path: &std::path::Path, t: Option<&toml::Value>) {
|
||||
let Some(t) = t.and_then(toml::Value::as_table) else { return };
|
||||
// Sorted by key (byte order), as the lookup's binary search needs.
|
||||
@@ -81,8 +81,16 @@ fn table(o: &mut String, path: &std::path::Path, t: Option<&toml::Value>) {
|
||||
let s = |k: &str| v.get(k).and_then(toml::Value::as_str);
|
||||
match (v.as_str(), s("one"), s("other")) {
|
||||
(Some(text), ..) => _ = writeln!(o, " ({k:?}, Text::Plain({text:?})),"),
|
||||
(None, one, Some(other)) => _ = writeln!(o, " ({k:?}, Text::Plural({}, {other:?})),", name(one)),
|
||||
_ => println!("cargo::warning={}: `{k}` is neither a string nor {{ one, other }}; left out", path.display()),
|
||||
(None, one, Some(other)) => {
|
||||
_ = writeln!(
|
||||
o,
|
||||
" ({k:?}, Text::Plural {{ one: {}, few: {}, many: {}, other: {other:?} }}),",
|
||||
name(one),
|
||||
name(s("few")),
|
||||
name(s("many")),
|
||||
);
|
||||
}
|
||||
_ => println!("cargo::warning={}: `{k}` is neither a string nor plural forms with `other`; left out", path.display()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+55
-17
@@ -13,8 +13,13 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
/// One catalog entry.
|
||||
pub enum Text {
|
||||
Plain(&'static str),
|
||||
/// Plural forms: `one` (when the language has it) and `other`.
|
||||
Plural(Option<&'static str>, &'static str),
|
||||
/// Optional language-specific plural forms, with `other` as the fallback.
|
||||
Plural {
|
||||
one: Option<&'static str>,
|
||||
few: Option<&'static str>,
|
||||
many: Option<&'static str>,
|
||||
other: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
/// A language's catalog (generated from `locales/<code>.toml`).
|
||||
@@ -104,7 +109,7 @@ fn lookup(lang: usize, key: &str) -> Option<&'static Text> {
|
||||
/// two translations gets a context after `|` ("Fill|sizing"); the English catalog maps it back.
|
||||
pub fn tr(key: &str) -> &str {
|
||||
match lookup(CURRENT.load(Ordering::Relaxed), key) {
|
||||
Some(Text::Plain(s) | Text::Plural(_, s)) => s,
|
||||
Some(Text::Plain(s) | Text::Plural { other: s, .. }) => s,
|
||||
None => key,
|
||||
}
|
||||
}
|
||||
@@ -123,26 +128,59 @@ pub fn trf(key: &str, args: &[(&str, &dyn Display)]) -> String {
|
||||
/// The plural form of `key` (its English "other" form) for count `n`, with `{n}` and `args` filled.
|
||||
pub fn trn(key: &str, n: usize, args: &[(&str, &dyn Display)]) -> String {
|
||||
let lang = CURRENT.load(Ordering::Relaxed);
|
||||
let text = match lookup(lang, key) {
|
||||
Some(Text::Plural(one, other)) => one.filter(|_| plural_one(lang, n)).unwrap_or(other),
|
||||
Some(Text::Plain(s)) => s,
|
||||
None => key,
|
||||
};
|
||||
let text = counted(lang, key, n);
|
||||
let mut all: Vec<(&str, &dyn Display)> = vec![("n", &n)];
|
||||
all.extend_from_slice(args);
|
||||
fill(text, &all)
|
||||
}
|
||||
|
||||
/// Pick a plural using the catalog that supplies the text: an English fallback keeps English's
|
||||
/// rule, even when the selected language is Russian.
|
||||
fn counted(lang: usize, key: &str, n: usize) -> &str {
|
||||
chain(lang)
|
||||
.find_map(|l| {
|
||||
find(l.ui, key).map(|text| match text {
|
||||
Text::Plain(s) => *s,
|
||||
Text::Plural { one, few, many, other } => match plural(l.code, n) {
|
||||
Plural::One => one.unwrap_or(other),
|
||||
Plural::Few => few.unwrap_or(other),
|
||||
Plural::Many => many.unwrap_or(other),
|
||||
Plural::Other => other,
|
||||
},
|
||||
})
|
||||
})
|
||||
.unwrap_or(key)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Plural {
|
||||
One,
|
||||
Few,
|
||||
Many,
|
||||
Other,
|
||||
}
|
||||
|
||||
/// CLDR integer rules. Existing one/other catalogs may omit the extra forms.
|
||||
fn plural(code: &str, n: usize) -> Plural {
|
||||
match code {
|
||||
"ru" if n % 10 == 1 && n % 100 != 11 => Plural::One,
|
||||
"ru" if (2..=4).contains(&(n % 10)) && !(12..=14).contains(&(n % 100)) => Plural::Few,
|
||||
"ru" => Plural::Many,
|
||||
"ja" | "ko" | "zh-Hans" => Plural::Other,
|
||||
"fr" | "pt-BR" if n <= 1 => Plural::One,
|
||||
_ if n == 1 => Plural::One,
|
||||
_ => Plural::Other,
|
||||
}
|
||||
}
|
||||
|
||||
// CLDR's million-count "many" in French, Spanish and Portuguese still reads their existing
|
||||
// "other" wording. Only Russian currently supplies separate few/many forms.
|
||||
|
||||
/// Whether count `n` takes the "one" form: 0 and 1 in French and Brazilian Portuguese, never in
|
||||
/// Japanese, Korean and Chinese (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.
|
||||
#[cfg(test)]
|
||||
fn plural_one(lang: usize, n: usize) -> bool {
|
||||
match LANGS.get(lang).map(|l| l.code) {
|
||||
Some("ja" | "ko" | "zh-Hans") => false,
|
||||
Some("fr" | "pt-BR") => n <= 1,
|
||||
_ => n == 1,
|
||||
}
|
||||
plural(LANGS.get(lang).map_or("en", |l| l.code), n) == Plural::One
|
||||
}
|
||||
|
||||
/// Replace each `{name}` in `text` with its argument, in one pass (an argument's own braces stay).
|
||||
@@ -186,8 +224,8 @@ pub fn menu_item<'a>(id: &str, english: &'a str) -> Cow<'a, str> {
|
||||
fn label<'a>(menu: bool, id: &str, english: &'a str) -> Cow<'a, str> {
|
||||
let text = chain(CURRENT.load(Ordering::Relaxed)).find_map(|l| menu.then(|| find(l.menu, id)).flatten().or_else(|| find(l.commands, id)));
|
||||
match text {
|
||||
Some(Text::Plain(s) | Text::Plural(_, s)) if english.ends_with('…') && !s.ends_with('…') => Cow::Owned(format!("{s}…")),
|
||||
Some(Text::Plain(s) | Text::Plural(_, s)) => Cow::Borrowed(s),
|
||||
Some(Text::Plain(s) | Text::Plural { other: s, .. }) if english.ends_with('…') && !s.ends_with('…') => Cow::Owned(format!("{s}…")),
|
||||
Some(Text::Plain(s) | Text::Plural { other: s, .. }) => Cow::Borrowed(s),
|
||||
None => Cow::Borrowed(english),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ fn placeholders(s: &str) -> BTreeSet<&str> {
|
||||
fn forms(t: &Text) -> Vec<&'static str> {
|
||||
match t {
|
||||
Text::Plain(s) => vec![s],
|
||||
Text::Plural(one, other) => one.iter().copied().chain([*other]).collect(),
|
||||
Text::Plural { one, few, many, other } => one.iter().chain(few).chain(many).copied().chain([*other]).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,10 +265,10 @@ fn catalogs_have_known_keys_and_matching_placeholders() {
|
||||
for (i, form) in forms(t).iter().enumerate() {
|
||||
let got = placeholders(form);
|
||||
// A "one" form may spell the count out ("una capa").
|
||||
let ok = if matches!(t, Text::Plural(Some(_), _)) && i == 0 { got.is_subset(&want) } else { got == want };
|
||||
let ok = if matches!(t, Text::Plural { one: Some(_), .. }) && i == 0 { got.is_subset(&want) } else { got == want };
|
||||
assert!(ok, "{}: {k:?} has placeholders {got:?}, English has {want:?}", lang.code);
|
||||
}
|
||||
assert_eq!(matches!(t, Text::Plural(..)), matches!(src, Text::Plural(..)), "{}: {k:?} plural or not as in English", lang.code);
|
||||
assert_eq!(matches!(t, Text::Plural { .. }), matches!(src, Text::Plural { .. }), "{}: {k:?} plural or not as in English", lang.code);
|
||||
}
|
||||
for (id, t) in lang.commands.iter().chain(lang.menu) {
|
||||
assert!(ids.contains(id), "{}: unknown command id {id:?}", lang.code);
|
||||
@@ -369,7 +369,7 @@ fn japanese_korean_and_chinese_have_no_plural_forms() {
|
||||
for code in ["ja", "ko", "zh-Hans"] {
|
||||
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();
|
||||
let ones: Vec<_> = LANGS[lang].ui.iter().filter(|(_, t)| matches!(t, Text::Plural { one: Some(_), .. })).map(|(k, _)| k).collect();
|
||||
assert!(ones.is_empty(), "{code}: plural entries give only `other`: {ones:?}");
|
||||
}
|
||||
}
|
||||
@@ -397,6 +397,50 @@ fn the_key_scanner_reads_rust_literals() {
|
||||
assert_eq!(literals(src), vec!["A \"b\"", "C {n}", "DE"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn russian_language_tags_and_integer_plurals() {
|
||||
let ru = LANGS.iter().position(|l| l.code == "ru").unwrap();
|
||||
assert_eq!(LANGS[ru].name, "Русский");
|
||||
for tag in ["ru", "ru-RU", "ru_RU.UTF-8", "RU", "ru-BY", "ru-Cyrl-RU"] {
|
||||
assert_eq!(resolve(tag, None), ru, "{tag}");
|
||||
assert_eq!(resolve("system", Some(tag)), ru, "{tag}");
|
||||
}
|
||||
assert_eq!(resolve("system", Some("nl,ru-RU,en-US")), ru);
|
||||
for n in [1, 21, 31, 101, 1_001] {
|
||||
assert_eq!(plural("ru", n), Plural::One, "{n}");
|
||||
assert_eq!(fill(counted(ru, "{n} layers", n), &[("n", &n)]), format!("{n} слой"));
|
||||
}
|
||||
for n in [2, 3, 4, 22, 23, 24, 102, 104, 1_002] {
|
||||
assert_eq!(plural("ru", n), Plural::Few, "{n}");
|
||||
assert_eq!(fill(counted(ru, "{n} layers", n), &[("n", &n)]), format!("{n} слоя"));
|
||||
}
|
||||
for n in [0, 5, 10, 11, 12, 13, 14, 19, 20, 25, 100, 111, 112, 114, 1_000_000] {
|
||||
assert_eq!(plural("ru", n), Plural::Many, "{n}");
|
||||
assert_eq!(fill(counted(ru, "{n} layers", n), &[("n", &n)]), format!("{n} слоёв"));
|
||||
}
|
||||
for (key, text) in LANGS[ru].ui {
|
||||
if let Text::Plural { one, few, many, .. } = text {
|
||||
assert!(one.is_some() && few.is_some() && many.is_some(), "{key}: missing Russian plural forms");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plural_fallback_uses_the_language_of_its_catalog() {
|
||||
let la = LANGS.iter().position(|l| l.code == "es-419").unwrap();
|
||||
assert_eq!(counted(la, "{n} layers", 1), "{n} capa");
|
||||
assert_eq!(counted(la, "{n} layers", 21), "{n} capas");
|
||||
// Unsupported optional categories use other; existing one/other languages are unchanged.
|
||||
for code in ["en", "es", "de"] {
|
||||
for n in [0, 2, 11, 21, 1_000_000] {
|
||||
assert_eq!(plural(code, n), Plural::Other, "{code}: {n}");
|
||||
}
|
||||
assert_eq!(plural(code, 1), Plural::One, "{code}");
|
||||
}
|
||||
assert_eq!(counted(0, "{n} layers", 21), "{n} layers");
|
||||
assert_eq!(counted(0, "unknown {n}", 21), "unknown {n}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_language_preference_takes_language_codes() {
|
||||
let mut s = figcraft_engine::Session::new();
|
||||
|
||||
@@ -111,7 +111,8 @@ pub fn rail(app: &mut FigApp, ui: &mut Ui) {
|
||||
ui.painter().rect_filled(ir, CornerRadius::same(6), t.hover);
|
||||
}
|
||||
icons::paint(ui.painter(), Rect::from_center_size(ir.center(), vec2(16.0, 16.0)), icon, if active { t.accent_text } else { t.icon });
|
||||
ui.painter().text(pos2(r.center().x, y + 38.0), Align2::CENTER_CENTER, label, regular(10.0), t.text);
|
||||
let shown = crate::widgets::elide_font(ui, label, (r.width() - 6.0).max(0.0), regular(10.0));
|
||||
ui.painter().text(pos2(r.center().x, y + 38.0), Align2::CENTER_CENTER, shown, regular(10.0), t.text);
|
||||
if resp.clicked() {
|
||||
match tab {
|
||||
Some(tb) => {
|
||||
@@ -684,7 +685,7 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(16.0);
|
||||
let hint = if filter.is_empty() { tr("Add a frame (F) or a shape (R) to get started") } else { tr("No matching layers") };
|
||||
crate::widgets::label(ui, hint, t.text_secondary);
|
||||
ui.add(egui::Label::new(egui::RichText::new(hint).font(regular(11.0)).color(t.text_secondary)).wrap());
|
||||
});
|
||||
}
|
||||
// Dragging near the list's top or bottom edge scrolls it, faster closer to the edge.
|
||||
|
||||
+7
-5
@@ -1,6 +1,6 @@
|
||||
# Where FigCraft falls short of Figma
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (`.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; 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)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; 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 (and Latin American Spanish), French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese drafts | Localization | 29–51 | [localization-parity](localization-parity.md) |
|
||||
| 5 | Localization: English, reviewed Russian, plus unreviewed Spanish (and Latin American Spanish), French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese drafts | Localization | 29–51 | [localization-parity](localization-parity.md) |
|
||||
| 6 | Plugins: widgets take clicks only, a self-hosted directory but no public catalogue; Figma's plugins don't run | Ecosystem | 8–26 | [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) |
|
||||
@@ -142,7 +142,7 @@ 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
|
||||
### 5. Localization: English, reviewed Russian, plus unreviewed drafts
|
||||
- **Missing:** native reviews of the Spanish, French, German, Brazilian Portuguese, Japanese,
|
||||
Korean and Simplified Chinese drafts and the Latin American Spanish overlay, the 25 newest strings
|
||||
in five drafts, Hindi, Arabic, Indonesian and Vietnamese, localized engine messages and number
|
||||
@@ -152,8 +152,9 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
||||
Preferences ▸ Language, `pref.language`; `en.toml` is complete, and `es.toml`, `fr.toml`,
|
||||
`de.toml`, `pt-BR.toml`, `ja.toml`, `ko.toml` and `zh-Hans.toml` (2026-10-12) are machine drafts
|
||||
(all but `zh-Hans.toml` lack the 25 newest strings), with `es-419.toml` a Latin American overlay
|
||||
listing only what differs from Spanish (only Spanish is held to completeness by the check; the
|
||||
others show English for strings added later).
|
||||
listing only what differs from Spanish (Spanish and Russian are held to completeness by the check; the
|
||||
others show English for strings added later). Russian adds all 2,165 current entries,
|
||||
reviewed and approved by VanTan, with one/few/many integer plurals.
|
||||
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
|
||||
@@ -394,6 +395,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. |
|
||||
| 2026-10-13 | minor | Gap 1: the experimental `.fig` export writes instance sizes (resized, filling an auto layout, scaled), variables with modes and bindings, shared styles and variable-width strokes, and the import reads them back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Hours and the ranking unchanged |
|
||||
| 2026-10-12 | minor | Gap 2: your online files landed (the app records the files it put on a server with their owner tokens, Home ▸ Online lists them and opens them again as their owner, with sharing, rename, removal and forget from the list; one request per server tells which are still there; tabs mark files in a live session). What is left is unchanged: accounts, teams and per-person permissions, shared comments and versions. Hours unchanged |
|
||||
| 2026-10-12 | minor | Gap 2: sharing with a join code (Share ▸ Online or Local network, one Start sharing button, a short code to give others, one join field for a code or a link; the server names links with codes and limits wrong ones) added to the landed list; usability, so the estimate and ranking are unchanged |
|
||||
|
||||
+1
-1
@@ -349,7 +349,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|es-419|fr|de|pt-BR|ja|ko|zh-Hans|…}`: the app's interface language, applied live ("system":
|
||||
- `pref.language {language: system|en|ru|es|es-419|fr|de|pt-BR|ja|ko|zh-Hans|…}`: 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 parity with Figma
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; 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)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; 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
|
||||
@@ -129,7 +129,7 @@ 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 | 39% | 29–51 | The layer, an English catalog and machine drafts of Spanish (with a Latin American overlay), French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese (none reviewed by a native speaker; every draft covers all current strings), CJK UI text in installed faces loaded on demand (web: only the craft-fonts CJK build); engine messages stay English | [localization-parity](localization-parity.md) |
|
||||
| Localization | 3 | 39% | 29–51 | The layer, English, Russian reviewed by VanTan, and machine drafts of Spanish (with a Latin American overlay), French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese (none reviewed by a native speaker; every draft covers all current strings), CJK UI text in installed faces loaded on demand (web: only the craft-fonts CJK build); 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 | 38% | 8–26 | Own WASM plugins with a Rust SDK and three examples (#101), plugin panels and settings (ABI v2), widgets that take clicks (API 3), a self-hosted plugin directory on `figcraft-server`; no Figma plugin compatibility, no public catalogue 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) |
|
||||
@@ -628,6 +628,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. |
|
||||
| 2026-10-13 | minor | Vector edit mode regions: the filled area under the pointer is hatched (clear of points and segments, holes left out, spacing by octave folding fitted to measurements at several zoom levels, fixed to the shape); click, Shift-click and drag select and move areas with their points (drawn unselected); a click on an empty spot clears them and stays in edit mode (see [ui-parity](ui-parity.md)) |
|
||||
| 2026-10-13 | minor | Plugin log limits (#191): cap each UTF-8 line at 8 KiB, total log bytes at 1 MiB and lines at 1,000 per run/callback before copying guest memory or forwarding to the host. Over-budget runs fail and roll back document edits; callbacks get fresh budgets. Added sandbox boundary/forwarding tests and an engine rollback regression. Percentages and hours unchanged. |
|
||||
| 2026-10-13 | minor | Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. |
|
||||
|
||||
+10
-5
@@ -17,6 +17,8 @@ Simplified.
|
||||
The catalogs: English (`en`, the source), and Spanish (`es`, with the Latin American overlay
|
||||
`es-419`), French (`fr`), German (`de`), Brazilian Portuguese (`pt-BR`), Japanese (`ja`), Korean (`ko`)
|
||||
and Simplified Chinese (`zh-Hans`), machine-drafted and waiting for native speakers' review.
|
||||
Russian (`ru`, Русский) was reviewed and approved by VanTan; its catalog keeps Frame, Dev Mode
|
||||
and Smart animate as familiar tool names and uses « » quotes.
|
||||
|
||||
## The catalogs
|
||||
|
||||
@@ -61,12 +63,15 @@ language also needs its script in `cjk::language`.
|
||||
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`, 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).
|
||||
only `other`). `other` is required; `one`, `few` and `many` are optional. `trn` picks `one` for
|
||||
1 (also 0 in French and Brazilian Portuguese). Russian uses `one` for 1, 21, 31… except 11;
|
||||
`few` for 2–4, 22–24… except 12–14; and `many` for the other integers, including 0. Keep `{n}`
|
||||
in all Russian forms. Add new rules to `plural` in
|
||||
[`crates/ui-egui/src/i18n.rs`](../crates/ui-egui/src/i18n.rs). A missing form reads `other`;
|
||||
a missing key uses the fallback catalog's own plural rule.
|
||||
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:
|
||||
adds a string then has to translate it into each complete language: Spanish and Russian are there.
|
||||
French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese are 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.
|
||||
@@ -88,7 +93,7 @@ Wrap every string a user sees or a screen reader hears: `tr` for plain text, `tr
|
||||
placeholders, `trn` for counts, `tr_noop` for literals in `const` tables that are looked up later
|
||||
with `tr(label)`. The key must be a string literal as the first argument (the check reads the
|
||||
source). Don't wrap ids, command ids, JSON, log messages, file extensions or document content. Then
|
||||
add the key to `en.toml` and to each complete language (Spanish); the drafts may follow later.
|
||||
add the key to `en.toml` and to each complete language (Spanish and Russian); the drafts may follow later.
|
||||
|
||||
Labels from the engine and document crates (tool names, blend modes, easing curves…) are looked up
|
||||
at display with `tr(x.label())`; they are listed in `dynamic_labels` in `i18n_tests.rs`.
|
||||
|
||||
Reference in New Issue
Block a user