M10: Traditional Chinese (Taiwan) interface draft

Add a 2,647-row zh-Hant catalog (1,975 UI strings, 641 command labels, 31 menu wordings), locale mapping for zh-TW/zh-HK/zh-MO/zh-Hant, and a Noto Sans CJK TC-first fallback. The catalog is marked draft pending native review; the web picker stays hidden because craft-fonts currently ships only the Simplified CJK face.
This commit is contained in:
SHXJ committed 2026-11-03 22:56:03 +08:00
1 parent 35684eaa02
commit 9770f1696e
12 files changed
+2844 -55

No files matched your search

+6 -4
View File
@@ -101,6 +101,7 @@ native speakers' reviews of the nine language drafts need people.
|---|---|---|---|
| English | en | 100% | full |
| Simplified Chinese | zh-Hans | 95% | draft (needs native review) |
| Traditional Chinese (Taiwan) | zh-Hant | 2,647 / 2,647 entries (100%, counted 2026-11-03) | draft (needs native review) |
| Spanish | es, es-419 | 100% (es-419: an overlay of 101 entries) | draft (needs native review) |
| Hindi | hi | 0% | none |
| Arabic | ar | 2,508 / 2,508 entries (100%, counted 2026-10-28) | draft; native review and panel mirroring pending |
@@ -114,9 +115,9 @@ native speakers' reviews of the nine language drafts need people.
Percentages measured 2026-10-17 over 2,272 entries (strings added since a draft show English). Also shipped: Russian (`ru`, 99%, reviewed by VanTan), Swedish (`sv`, 99.6%, command labels since 2026-10-17; native review welcome), Turkish (`tr`, 99%), Czech (`cs`, 99%, draft) and Bosnian (`bs`, 100% on 2026-10-30, draft). Figma ships English, Japanese, Korean, French, German, Spanish and Portuguese
(Brazil); FigCraft has the localization layer, English and Spanish (Spain and Latin America),
French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Indonesian, Vietnamese and Arabic drafts (Preferences ▸
Language; Traditional Chinese has no catalog; CJK UI
text uses an installed CJK font, loaded only when needed; adding a language:
French, German, Brazilian Portuguese, Japanese, Korean, Simplified and Traditional Chinese, Indonesian, Vietnamese and Arabic drafts (Preferences ▸
Language; Traditional Chinese reads zh-TW, zh-HK, zh-MO and zh-Hant, while an explicit Hans stays
Simplified; 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 (macOS IME composition fixed for #747, not yet checked on a Mac). Detail: [`docs/localization-parity.md`](docs/localization-parity.md).
@@ -127,7 +128,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; then checking the rest of the experimental `.fig` export (landed 2026-10-12; written files open and draw in other software) on effects, images, prototypes and real files: 5–15 h.
2. Real-file corpus and measured fidelity: 15–30 h.
3. Tool-by-tool interaction pass against Figma: 40–60 h.
4. Languages: native reviews of the es (with es-419), fr, de, pt-BR, ja, ko, zh-Hans, id and cs drafts (people), the entries added since each catalog was written, then Hindi and Vietnamese; Arabic native review and panel mirroring ([localization-parity](docs/localization-parity.md)).
4. Languages: native reviews of the es (with es-419), fr, de, pt-BR, ja, ko, zh-Hans, zh-Hant, id and cs drafts (people), the entries added since each catalog was written, then Hindi and Vietnamese; Arabic native review and panel mirroring ([localization-parity](docs/localization-parity.md)).
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
@@ -357,6 +358,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
| Date | Change | Summary |
|---|---|---|
| 2026-11-03 | minor | Traditional Chinese (Taiwan) (`zh-Hant`, 繁體中文(台灣)) draft catalog: every UI string (1,975), command label (641) and menu wording (31), translated clean-room from the English source; `zh-TW`, `zh-HK`, `zh-MO` and `zh-Hant` read it while an explicit Hans stays Simplified; the desktop UI prefers an installed Noto Sans CJK TC, then Microsoft JhengHei, PingFang TC and Heiti TC; no font is bundled; native review pending. Numbers unchanged |
| 2026-10-31 | minor | M12: as the help describes, holding Cmd/Ctrl while moving a connector's end places it on any part of an object instead of snapping to a side. It keeps that spot as a fraction of the object's box, so it follows the object when it moves or resizes, and without the key an end snaps as before. `board.connector {fromAt, toAt}` and `board.reattach {at}` take it; saved only when set. Numbers unchanged |
| 2026-10-31 | patch | M11: a trackpad or touch tap sends its press and release in one frame, so a plugin panel's field committed in the frame its button was clicked and the value was lost: the Counter's Set count… kept the old count. The plugin now gets both, the field's value first. The `window.figcraft` docs name `screenshot`'s `data`. Numbers unchanged |
| 2026-10-31 | patch | M4: Dash and Custom strokes have a Dash cap (None, Round, Square) in the advanced stroke settings and `node.set {dashCap}`, on both ends of every dash on the canvas and in PNG, PDF and SVG. Unset, dashes keep taking the end point caps, so older files look the same; the saved file writes it only when set. Numbers unchanged |
+1 -1
View File
@@ -480,7 +480,7 @@ pub fn specs() -> Vec<CommandSpec> {
spec!(
"pref.language",
"Interface language",
"{language: system|en|bs|cs|ru|es|es-419|fr|de|pt-BR|ja|ka|ko|vi|zh-Hans|…; else English}",
"{language: system|en|bs|cs|ru|es|es-419|fr|de|pt-BR|ja|ka|ko|vi|zh-Hans|zh-Hant|…; else English}",
false,
|s, p| {
let lang = req_string("pref.language", p, "language")?;
File diff suppressed because it is too large. Load diff
+49 -12
View File
@@ -59,6 +59,21 @@ const CHINESE: &[&str] = &[
"WenQuanYi Zen Hei",
"Droid Sans Fallback",
];
/// Traditional Chinese (Taiwan) faces, an installed Noto Sans CJK TC first, then Windows, macOS
/// and Linux names. Nothing is bundled: the desktop finds one of these among the installed fonts.
const TRADITIONAL_CHINESE: &[&str] = &[
"Noto Sans CJK TC",
"Microsoft JhengHei UI",
"Microsoft JhengHei",
"PingFang TC",
"Heiti TC",
"Noto Sans TC",
"Source Han Sans TC",
"LiHei Pro",
"Apple LiGothic",
"MingLiU",
"PMingLiU",
];
/// The CJK, Bengali and Thai scripts in `s`.
pub fn scripts(s: &str) -> u8 {
@@ -86,6 +101,16 @@ pub fn language(code: &str) -> u8 {
}
}
/// Whether `code` is Traditional Chinese: `zh-Hant`, or `zh` with a Taiwan, Hong Kong or Macau
/// region and no explicit `Hans` (`zh-TW`, `zh-HK`, `zh-MO`, `zh-Hant-TW`). `zh-Hans-TW` is
/// Simplified.
pub fn traditional(code: &str) -> bool {
let tag = code.trim().split(['.', '@']).next().unwrap_or("").replace('_', "-");
let base = tag.split('-').next().unwrap_or("");
let has = |subtags: &[&str]| tag.split('-').skip(1).any(|t| subtags.iter().any(|s| s.eq_ignore_ascii_case(t)));
base.eq_ignore_ascii_case("zh") && (has(&["Hant"]) || (!has(&["Hans"]) && has(&["TW", "HK", "MO"])))
}
/// 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 (Georgian: craft-fonts'
/// Noto Sans Georgian). The language picker and the system language skip a language it can't.
@@ -107,19 +132,20 @@ fn shows(code: &str, web: bool) -> bool {
return figcraft_text::face_with_char('ع', &["Noto Sans Arabic"]).is_some();
}
let need = language(code);
need == 0 || samples(need, 0).iter().all(|(c, _)| figcraft_text::face_with_char(*c, &["BIZ UDPGothic", "Noto Sans CJK SC"]).is_some())
need == 0 || samples(need, 0, traditional(code)).iter().all(|(c, families)| figcraft_text::face_with_char(*c, families).is_some())
}
/// A character standing for each script in `need`, with the families to look for it in, the UI
/// language's script (`ui`, see [`language`]) first. Han reads as Chinese (a simplified character,
/// Chinese faces first) in a Chinese UI, and without kana in a UI that isn't Japanese or Korean;
/// else a Japanese or Korean face draws it.
fn samples(need: u8, ui: u8) -> Vec<(char, Vec<&'static str>)> {
/// Simplified or Traditional Chinese faces first as the UI language says) in a Chinese UI, and
/// without kana in a UI that isn't Japanese or Korean; else a Japanese or Korean face draws it.
fn samples(need: u8, ui: u8, hant: bool) -> Vec<(char, Vec<&'static str>)> {
let chinese = need & HAN != 0 && (ui == HAN || (ui == 0 && need & KANA == 0));
let han: (char, Vec<&str>) = match (chinese, ui) {
(true, _) => ('们', [CHINESE, JAPANESE, KOREAN].concat()),
(false, HANGUL) => ('漢', [KOREAN, JAPANESE, CHINESE].concat()),
(false, _) => ('漢', [JAPANESE, KOREAN, CHINESE].concat()),
let han: (char, Vec<&str>) = match (chinese, hant, ui) {
(true, true, _) => ('們', [TRADITIONAL_CHINESE, JAPANESE, KOREAN].concat()),
(true, false, _) => ('们', [CHINESE, JAPANESE, KOREAN].concat()),
(false, _, HANGUL) => ('漢', [KOREAN, JAPANESE, CHINESE].concat()),
(false, _, _) => ('漢', [JAPANESE, KOREAN, CHINESE].concat()),
};
let mut v = vec![
(KANA, ('あ', JAPANESE.to_vec())),
@@ -147,7 +173,7 @@ fn samples(need: u8, ui: u8) -> Vec<(char, Vec<&'static str>)> {
/// 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)) {
for (c, families) in samples(need, language(lang), traditional(lang)) {
if have(c) || out.iter().any(|f| f.has_char(c)) {
continue;
}
@@ -177,6 +203,9 @@ mod tests {
assert_eq!(language("ko"), HANGUL);
assert_eq!(language("zh-Hant"), HAN);
assert_eq!(language("zh-Hans"), HAN);
assert!(traditional("zh-Hant") && traditional("zh-TW") && traditional("zh-HK") && traditional("zh-MO"));
assert!(traditional("zh-Hant-HK") && traditional("zh_TW.UTF-8"));
assert!(!traditional("zh-Hans") && !traditional("zh-CN") && !traditional("zh-Hans-TW") && !traditional("zh") && !traditional("ja"));
assert_eq!(language("bn"), BENGALI);
assert_eq!(language("fr"), 0);
assert!(["en", "es", "es-419", "fr", "de", "pt-BR", "tr", "id", "vi", "bs"].iter().all(|c| can_show(c)));
@@ -202,8 +231,8 @@ mod tests {
/// The UI language's script leads the fallback order; the others follow.
#[test]
fn the_ui_language_script_comes_first() {
let order = |need, ui| samples(need, ui).into_iter().map(|(c, _)| c).collect::<String>();
let first_face = |need, ui| samples(need, ui).first().and_then(|(_, f)| f.first()).copied();
let order = |need, ui| samples(need, ui, false).into_iter().map(|(c, _)| c).collect::<String>();
let first_face = |need, ui| samples(need, ui, false).first().and_then(|(_, f)| f.first()).copied();
assert_eq!(order(KANA | HAN | HANGUL, 0), "あ漢한");
assert_eq!(order(KANA | HAN | HANGUL | BENGALI, language("bn")), "অあ漢한");
assert_eq!(order(KANA | HAN | HANGUL, KANA | HAN), "あ漢한");
@@ -220,7 +249,15 @@ mod tests {
// A Chinese UI draws Han in a Chinese face first, Japanese names in it too.
assert_eq!(order(KANA | HAN | HANGUL, HAN), "们あ한");
assert_eq!(order(KANA | HAN, language("zh-Hans")), "们あ");
assert!(["Microsoft YaHei UI", "PingFang SC", "Noto Sans CJK SC"].iter().all(|f| samples(HAN, HAN)[0].1[..4].contains(f)));
assert!(["Microsoft YaHei UI", "PingFang SC", "Noto Sans CJK SC"].iter().all(|f| samples(HAN, HAN, false)[0].1[..4].contains(f)));
// Traditional Chinese puts an installed Noto Sans CJK TC, then Taiwan's faces, first.
let order_hant = |need, ui| samples(need, ui, true).into_iter().map(|(c, _)| c).collect::<String>();
assert_eq!(order_hant(KANA | HAN, language("zh-Hant")), "們あ");
assert_eq!(first_face(HAN, HAN), Some("Noto Sans CJK SC"));
let hant = samples(HAN, language("zh-Hant"), true);
assert_eq!(hant[0].0, '們');
assert_eq!(hant[0].1[0], "Noto Sans CJK TC");
assert!(["Microsoft JhengHei UI", "PingFang TC", "Noto Sans CJK TC"].iter().all(|f| hant[0].1.contains(f)));
assert!(faces(0, "en", &|_| false).is_empty());
assert!(faces(KANA | HANGUL | HAN | BENGALI, "ja", &|_| true).is_empty(), "nothing is looked up for scripts the UI draws");
}
+10 -6
View File
@@ -78,15 +78,19 @@ const LATIN_AMERICA: &[&str] =
/// 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"); Latin American
/// Spanish ("es-MX") reads es-419, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) none: it
/// has no catalog and Simplified isn't a stand-in. A language the UI can't draw (CJK on the web
/// without CJK faces built in) doesn't count.
/// Spanish ("es-MX") reads es-419. A Chinese tag reads Traditional (zh-Hant) for `Hant`, `TW`, `HK`
/// or `MO` and Simplified (zh-Hans) otherwise; an explicit `Hans` always reads Simplified. 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 has = |subtags: &[&str]| tag.split('-').skip(1).any(|t| subtags.iter().any(|s| s.eq_ignore_ascii_case(t)));
if base.eq_ignore_ascii_case("zh") && (has(&["Hant"]) || (!has(&["Hans"]) && has(&["TW", "HK", "MO"]))) {
return None;
let zh = |code: &str| {
LANGS.iter().position(|l| l.code.eq_ignore_ascii_case(code)).filter(|i| LANGS.get(*i).is_some_and(|l| crate::cjk::can_show(l.code)))
};
if base.eq_ignore_ascii_case("zh") {
let traditional = has(&["Hant"]) || (!has(&["Hans"]) && has(&["TW", "HK", "MO"]));
return zh(if traditional { "zh-Hant" } else { "zh-Hans" });
}
let find = |t: &str| LANGS.iter().position(|l| l.code.eq_ignore_ascii_case(t));
let alias = (base.eq_ignore_ascii_case("es") && has(LATIN_AMERICA)).then(|| find("es-419")).flatten();
@@ -182,7 +186,7 @@ fn plural(code: &str, n: usize) -> Plural {
"bs" if n % 10 == 1 && n % 100 != 11 => Plural::One,
"bs" if (2..=4).contains(&(n % 10)) && !(12..=14).contains(&(n % 100)) => Plural::Few,
"bs" => Plural::Other,
"ja" | "ko" | "th" | "zh-Hans" | "vi" => Plural::Other,
"ja" | "ko" | "th" | "zh-Hans" | "zh-Hant" | "vi" => Plural::Other,
"fr" | "pt-BR" if n <= 1 => Plural::One,
_ if n == 1 => Plural::One,
_ => Plural::Other,
+55 -7
View File
@@ -514,7 +514,7 @@ fn japanese_korean_chinese_and_vietnamese_have_no_plural_forms() {
assert_eq!(code("system", Some("ja,en-US")), "ja");
assert_eq!(LANGS[resolve("vi", None)].name, "Tiếng Việt");
assert!(LANGS[resolve("vi", None)].draft);
for code in ["ja", "ko", "th", "zh-Hans", "vi"] {
for code in ["ja", "ko", "th", "zh-Hans", "zh-Hant", "vi"] {
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(Forms { one: Some(_), .. }))).map(|(k, _)| k).collect();
@@ -594,17 +594,65 @@ fn the_built_in_georgian_face_draws_the_georgian_catalog() {
}
#[test]
fn chinese_reads_simplified_and_traditional_has_no_catalog() {
fn chinese_tags_pick_simplified_or_traditional() {
let code = |pref: &str, system: Option<&str>| LANGS[resolve(pref, system)].code;
for tag in ["zh-CN", "zh_CN.UTF-8", "zh-SG", "zh-Hans", "zh-Hans-CN", "zh-Hans-HK", "zh"] {
assert_eq!(code("system", Some(tag)), "zh-Hans", "{tag}");
}
// No Traditional Chinese catalog: English, never Simplified; else the next OS language.
for tag in ["zh-TW", "zh-HK", "zh-MO", "zh-Hant", "zh-Hant-TW", "zh_TW.UTF-8"] {
assert_eq!(code("system", Some(tag)), "en", "{tag}");
assert_eq!(code(tag, None), "en", "{tag}");
// Taiwan, Hong Kong, Macau and Hant read Traditional Chinese, never Simplified.
for tag in ["zh-TW", "zh-HK", "zh-MO", "zh-Hant", "zh-Hant-TW", "zh_TW.UTF-8", "zh-Hant-HK"] {
assert_eq!(code("system", Some(tag)), "zh-Hant", "{tag}");
assert_eq!(code(tag, None), "zh-Hant", "{tag}");
}
assert_eq!(code("system", Some("zh-TW,ja-JP")), "ja");
// An explicit Hans still reads Simplified, whatever the region.
assert_eq!(code("system", Some("zh-Hans-TW")), "zh-Hans");
assert_eq!(code("system", Some("zh-TW,ja-JP")), "zh-Hant");
assert_eq!(code("system", Some("ja-JP,zh-TW")), "ja");
}
#[test]
fn traditional_chinese_covers_every_english_key_and_command() {
let zh = LANGS.iter().position(|l| l.code == "zh-Hant").expect("locales/zh-Hant.toml is baked in");
assert_eq!(LANGS[zh].name, "繁體中文(台灣)");
assert!(LANGS[zh].draft, "a machine-drafted catalog stays a draft until a native speaker reviews it");
for (k, _) in english().ui {
assert!(find(LANGS[zh].ui, k).is_some(), "zh-Hant lacks the UI string {k:?}");
}
for id in command_ids() {
assert!(find(LANGS[zh].commands, id).is_some(), "zh-Hant lacks the command label {id:?}");
}
for id in menu_worded_differently() {
assert!(find(LANGS[zh].menu, id).is_some(), "zh-Hant lacks the menu wording {id:?}");
}
// Spot checks for the Taiwan design terms the catalog is written in.
assert!(matches!(lookup(zh, "Canvas"), Some(Text::Plain("畫布"))));
assert!(matches!(lookup(zh, "Layers"), Some(Text::Plain("圖層"))));
assert!(matches!(lookup(zh, "Frame"), Some(Text::Plain("框架"))));
assert!(matches!(lookup(zh, "Component"), Some(Text::Plain("元件"))));
assert!(matches!(lookup(zh, "Constraints"), Some(Text::Plain("約束"))));
assert!(matches!(lookup(zh, "Fill"), Some(Text::Plain("填色"))));
assert!(matches!(lookup(zh, "Stroke"), Some(Text::Plain("邊框"))));
assert!(matches!(lookup(zh, "Export"), Some(Text::Plain("匯出"))));
assert!(matches!(lookup(zh, "Version"), Some(Text::Plain("版本"))));
assert!(matches!(lookup(zh, "Prototype"), Some(Text::Plain("原型"))));
}
/// Changing the preference and reading it back keeps the Chinese tag; the tag still resolves to
/// the same catalog after the preferences are saved and loaded (a restart).
#[test]
fn traditional_chinese_switches_and_the_preference_persists() {
let mut s = figcraft_engine::Session::new();
s.execute("pref.language", &serde_json::json!({"language": "zh-TW"})).unwrap();
assert_eq!(s.prefs.language, "zh-TW");
assert_eq!(LANGS[resolve(&s.prefs.language, None)].code, "zh-Hant");
let saved = serde_json::to_string(&s.prefs).unwrap();
let loaded: figcraft_engine::Prefs = serde_json::from_str(&saved).unwrap();
assert_eq!(loaded.language, "zh-TW");
assert_eq!(LANGS[resolve(&loaded.language, None)].code, "zh-Hant");
s.execute("pref.language", &serde_json::json!({"language": "zh-Hans"})).unwrap();
assert_eq!(LANGS[resolve(&s.prefs.language, None)].code, "zh-Hans");
s.execute("pref.language", &serde_json::json!({"language": "zh-Hant"})).unwrap();
assert_eq!(LANGS[resolve(&s.prefs.language, None)].code, "zh-Hant");
}
#[test]
+10 -8
View File
@@ -208,16 +208,17 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
### 5. Localization: English, reviewed Russian and Turkish, plus unreviewed drafts
- **Missing:** native reviews of the Spanish, French, German, Brazilian Portuguese, Japanese,
Korean, Simplified Chinese, Indonesian and Czech drafts and the Latin American Spanish
overlay, the strings added since each catalog was drafted (1–5% of each), the Arabic draft's native review and panel mirroring, Hindi and
Vietnamese, localized engine messages and number formatting; CJK UI text on the web without the
craft-fonts CJK build.
Korean, Simplified Chinese, Traditional Chinese, Indonesian and Czech drafts and the Latin
American Spanish overlay, the strings added since each catalog was drafted (1–5% of each), the
Arabic draft's native review and panel mirroring, Hindi and Vietnamese, 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/` (measured 2026-10-17: 1,680 UI keys plus 561 command labels and 31
menu wordings, 2,272 entries), Preferences ▸ Language, `pref.language`; `en.toml` is complete,
`crates/ui-egui/locales/` (measured 2026-11-03: 1,975 UI keys plus 641 command labels and 31
menu wordings, 2,647 entries), Preferences ▸ Language, `pref.language`; `en.toml` is complete,
Spanish is held to completeness by the check, and the French, German, Brazilian Portuguese,
Japanese, Korean and Simplified Chinese machine drafts cover 95–99% (strings added since they
were drafted show English), with `es-419.toml` a Latin American overlay listing only what
Japanese, Korean, Simplified Chinese and Traditional Chinese machine drafts cover 95–100%
(the Traditional Chinese draft covers all 2,647 entries; strings added since the other drafts
were written show English), with `es-419.toml` a Latin American overlay listing only what
differs from Spanish. Russian (reviewed and approved by VanTan, one/few/many integer plurals)
covers 98.8%; Indonesian and Czech drafts, a Turkish catalog whose header says a native speaker
reviewed it, and a Swedish catalog (with its command labels since 2026-10-17) cover 98.7–99.6% ([localization-parity](localization-parity.md#languages)).
@@ -588,6 +589,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
| Date | Change | Summary |
|---|---|---|
| 2026-11-03 | minor | Traditional Chinese (Taiwan) (`zh-Hant`) draft catalog: 1,975 UI strings, 641 command labels and 31 menu wordings, clean-room from the English source; zh-TW, zh-HK, zh-MO and zh-Hant read it, explicit Hans stays Simplified; Noto Sans CJK TC then Taiwan system faces; no bundled font; native review pending. Numbers unchanged |
| 2026-10-31 | minor | Gap 10: library analytics list the layers and files using each style and variable; their insertions, detachments and modes stay on the missing list. |
| 2026-10-31 | minor | Gap 15: the Controller menu names buttons as PlayStation and Switch controllers do; that item leaves the missing list. Estimate unchanged. Test: `present::tail_tests`. |
| 2026-10-30 | minor | Gap 7: text reads only the fallback fonts it draws with: a face per fallback family and style, a file's bytes shared between families, the faces worked out once per style (first text laid out: 41 font files and 265 MiB read down to 3 files and 20 MiB on a Linux machine with Noto Sans CJK). Numbers unchanged |
+15 -10
View File
@@ -1,10 +1,10 @@
# Localization parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** minor (Bosnian draft catalog, one/few/other plurals; German terminology corrections (M10): gradient stops, OpenType stylistic sets and the Home-screen preference; partial review, catalog remains a draft; Arabic draft catalog, count-neutral plurals and web font gate; Thai draft catalog and Thai UI fonts; a missing translation fails no check: `cargo xtask i18n-report` lists them; Vietnamese draft catalog; string counts re-measured: 2,352 entries; Russian and Turkish catalogs reviewed by native speakers, Russian one/few/many plurals; Czech and Indonesian drafts; the Japanese draft completed; counts re-measured; Windows UI language from the first preferred language; draft catalogs catch up: every draft translates every string again; Simplified Chinese draft catalog; macOS finds its downloadable CJK system fonts (#406); string counts re-measured: 2,272 entries, the drafts at 95–99%; Swedish command labels and menu wordings; Russian, Swedish, Turkish, Czech and Indonesian listed; a duplicated paragraph removed) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** Traditional Chinese (Taiwan) draft catalog (`zh-Hant`, 2,647 entries) with a Noto Sans CJK TC-first desktop fallback and the web picker hidden until craft-fonts ships a TC face; previously: minor (Bosnian draft catalog, one/few/other plurals; German terminology corrections (M10): gradient stops, OpenType stylistic sets and the Home-screen preference; partial review, catalog remains a draft; Arabic draft catalog, count-neutral plurals and web font gate; Thai draft catalog and Thai UI fonts; a missing translation fails no check: `cargo xtask i18n-report` lists them; Vietnamese draft catalog; string counts re-measured: 2,352 entries; Russian and Turkish catalogs reviewed by native speakers, Russian one/few/many plurals; Czech and Indonesian drafts; the Japanese draft completed; counts re-measured; Windows UI language from the first preferred language; draft catalogs catch up: every draft translates every string again; Simplified Chinese draft catalog; macOS finds its downloadable CJK system fonts (#406); string counts re-measured: 2,272 entries, the drafts at 95–99%; Swedish command labels and menu wordings; Russian, Swedish, Turkish, Czech and Indonesian listed; a duplicated paragraph removed) · **Target:** Figma Design (desktop app 126.10.7)
FigCraft's interface has a localization layer with an English catalog, a Russian catalog reviewed
by a native speaker, and Spanish (with a Latin American overlay), French, German, Brazilian
Portuguese, Japanese, Korean, Simplified Chinese, Indonesian, Vietnamese, Bosnian and Czech drafts (machine-drafted,
Portuguese, Japanese, Korean, Simplified Chinese, Traditional Chinese, Indonesian, Vietnamese, Bosnian and Czech drafts (machine-drafted,
waiting for native speakers' review), plus Swedish and Turkish catalogs. This page lists the twelve key
languages, then the other languages Figma ships. How to add a
language: [`translating.md`](translating.md).
@@ -32,15 +32,15 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v
channel stay English. Accessibility names come from the same strings.
- **Language preference:** Preferences ▸ Language (System, then each catalog by its own name:
English, Bosanski, Čeština, Deutsch, Español, Español (Latinoamérica), Français, Bahasa Indonesia, 日本語,
한국어, Português (Brasil), Русский, Svenska, Türkçe, Tiếng Việt, 简体中文), applied at once without a
한국어, Português (Brasil), Русский, Svenska, Türkçe, Tiếng Việt, 简体中文, 繁體中文(台灣)), applied at once without a
restart; System takes the first of the OS's preferred languages that has a catalog (on Windows
only the first preferred language counts, since that list also holds the languages added for a
keyboard; macOS AppleLanguages, `LANGUAGE` then the locale variables on Linux and BSD; a regional
tag falls back to its language, pt-PT or pt to pt-BR, and es-419, es-MX, es-AR, es-CO and the
other Latin American and US Spanish tags to es-419; regional ru, tr, cs and id tags (ru-RU,
tr-TR, cs-CZ, id-ID) to their catalogs; zh, zh-CN, zh-SG and zh-Hans read zh-Hans,
while Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and reads English or the
next OS language, never Simplified) or the browser's language on the web.
while zh-Hant, zh-TW, zh-HK and zh-MO read zh-Hant; an explicit Hans such as zh-Hans-TW still
reads Simplified) or the browser's language on the web.
`pref.language {language}` sets it from the CLI, MCP and the control channel.
- **Regional overlays:** a regional catalog whose language has a catalog lists only what differs
(`es-419.toml` over `es.toml`); its UI strings, command labels and menu wordings read the
@@ -76,11 +76,14 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v
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, and a Chinese UI puts its Chinese face (Microsoft YaHei, PingFang SC, Noto Sans
CJK SC) ahead of the built-in Japanese one, so Han takes Simplified Chinese shapes; changing the
CJK SC) ahead of the built-in Japanese one, so Han takes Simplified Chinese shapes, while a
Traditional Chinese UI puts an installed Noto Sans CJK TC first, then Microsoft JhengHei, PingFang
TC and Heiti TC, so Han takes Traditional shapes; changing the
UI language lays the faces out again. 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 SC (+16 MB) and offers Japanese, Korean and Simplified Chinese; otherwise the language picker and
embeds Noto Sans CJK SC (+16 MB) and offers Japanese, Korean and Simplified Chinese; without a
Noto Sans CJK TC face built in it leaves Traditional Chinese out too; otherwise the language picker and
System leave them out. No font file is in this repo.
## Languages
@@ -88,7 +91,8 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v
| Language | Code | UI strings translated | Dialogs / tooltips / help | Script support | Native review | Status | Hours to full |
|---|---|---|---|---|---|---|---|
| English | en | 2,352 / 2,352 (100%, source) | yes | Latin | yes (source) | full | 0 |
| Simplified Chinese (Mandarin) | zh-Hans | 2,216 / 2,352 (94.2%, draft) | yes | CJK text and IME in documents; UI text in an installed Chinese face, ahead of Japanese ones (web: the `FIGCRAFT_EMBED_CJK` build); no Traditional Chinese catalog (zh-TW, zh-HK read English) | no | draft | 1–2 (after native review) |
| Simplified Chinese (Mandarin) | zh-Hans | 2,216 / 2,352 (94.2%, draft) | yes | CJK text and IME in documents; UI text in an installed Chinese face, ahead of Japanese ones (web: the `FIGCRAFT_EMBED_CJK` build) | no | draft | 1–2 (after native review) |
| Traditional Chinese (Taiwan) | zh-Hant | 2,647 / 2,647 (100%: 1,975 UI strings, 641 command labels, 31 menu wordings; draft, counted 2026-11-03) | yes | CJK text and IME in documents; UI text in an installed Noto Sans CJK TC, then Microsoft JhengHei, PingFang TC or Heiti TC (web: only with a built-in TC face); zh-TW, zh-HK, zh-MO and zh-Hant read it, an explicit Hans stays Simplified | no | draft | 1–2 (after native review) |
| Spanish | es, es-419 | 2,352 / 2,352 (100%, draft); es-419 overlays 104 Latin American wordings (draft) | yes | Latin | no | draft | 0–1 (after native review) |
| Hindi | hi | 0 (0%) | no | Devanagari shaping works in documents; UI font fallback to check | no | none | 4–7 |
| Arabic | ar | 1,857 / 1,857 UI strings, 620 command labels, 31 menu wordings (draft, counted 2026-10-28) | catalog complete; native review pending | Existing bidi/shaping and Arabic system fallback; web not offered (font budget excludes Arabic); panels do not mirror | no | draft | review + panel mirroring (not re-estimated) |
@@ -118,13 +122,14 @@ right-to-left mirroring of the panels is in Arabic's hours. Total: **29–51 h**
every string adds).
Priority: native reviews of the drafts and the es-419 overlay (every one of Figma's languages
and variants has one, plus Simplified Chinese, Indonesian, Vietnamese and Czech, which Figma doesn't ship), then
Hindi and Arabic. Traditional Chinese is not planned (no catalog; its users see English).
and variants has one, plus Simplified Chinese, Traditional Chinese, Indonesian, Vietnamese and Czech, which Figma doesn't ship), then
Hindi and Arabic.
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-11-03 | minor | Traditional Chinese (Taiwan) (`zh-Hant`, 繁體中文(台灣)) draft catalog: every UI string (1,975), command label (641) and menu wording (31), translated clean-room from the English source and marked as a draft awaiting native review. `zh-TW`, `zh-HK`, `zh-MO` and `zh-Hant` read it; an explicit Hans still reads Simplified Chinese. The desktop UI prefers an installed Noto Sans CJK TC, then Microsoft JhengHei, PingFang TC and Heiti TC; no font is bundled. No plural forms. Localization grade and estimates unchanged |
| 2026-10-30 | minor | Bosnian (`bs`, Bosanski) draft catalog: every UI string (1,906), command label (629) and menu wording (31), machine-drafted in Latin script with ijekavian forms and marked as a draft awaiting native review. Bosnian plurals: one for 1, 21, 31… (not 11), few for 2–4, 22–24… (not 12–14), other for the rest. Grade and hours unchanged (Bosnian is not one of the twelve key languages) |
| 2026-10-29 | minor | German terminology corrections (M10): gradient stops, OpenType stylistic sets and the Home-screen preference; partial review, catalog remains a draft. Nine existing translations corrected; keys, placeholders, coverage, grade and estimates unchanged |
| 2026-10-28 | minor | Arabic draft: 1,857 UI entries, 620 command labels and 31 menu wordings, with count-neutral plurals. AI-assisted, native review pending. Desktop uses the existing Arabic fallback and bidi; web requires an embedded Arabic face. No panel mirroring or parity-grade increase claimed |
+1 -1
View File
@@ -686,7 +686,7 @@ on; `key` resolves keys the way the canvas does. Commands behind keys:
- `pref.startOnHome {on?}`: the desktop app starts on the Home screen instead of in a new file
(off by default; files named on the command line still open, and `--control` starts in a new
file anyway, so control callers have a document).
- `pref.language {language: system|en|bs|cs|ru|es|es-419|fr|de|pt-BR|ja|ka|ko|sv|vi|zh-Hans|…}`: the app's interface language, applied live ("system":
- `pref.language {language: system|en|bs|cs|ru|es|es-419|fr|de|pt-BR|ja|ka|ko|sv|vi|zh-Hans|zh-Hant|…}`: 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.
+3 -2
View File
@@ -16,9 +16,9 @@ are one Opus 5.5 agent; most milestones split across 4–6 agents on disjoint cr
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: native reviews of the es, fr, de, pt-BR, ja,
ko, zh-Hans, id and cs drafts and the es-419 overlay (people; Russian and Turkish are reviewed),
ko, zh-Hans, zh-Hant, id and cs drafts and the es-419 overlay (people; Russian and Turkish are reviewed),
the entries added since each catalog was written, then the other key languages (Hindi,
Vietnamese): 3–7 h per language plus native review. Arabic now has a complete draft; native review and panel mirroring remain.
Vietnamese): 3–7 h per language plus native review. Arabic now has a complete draft; native review and panel mirroring remain. Traditional Chinese (Taiwan) has a complete draft too.
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
@@ -75,6 +75,7 @@ blockers.
| Date | Change | Summary |
|---|---|---|
| 2026-11-03 | minor | Traditional Chinese (Taiwan) (`zh-Hant`) draft catalog: 1,975 UI strings, 641 command labels and 31 menu wordings, clean-room from the English source; zh-TW, zh-HK, zh-MO and zh-Hant read it, explicit Hans stays Simplified; Noto Sans CJK TC then Taiwan system faces; no bundled font; native review pending. Numbers unchanged |
| 2026-10-31 | minor | M12: as the help describes, holding Cmd/Ctrl while moving a connector's end places it on any part of an object instead of snapping to a side. It keeps that spot as a fraction of the object's box, so it follows the object when it moves or resizes, and without the key an end snaps as before. `board.connector {fromAt, toAt}` and `board.reattach {at}` take it; saved only when set. Numbers unchanged |
| 2026-10-31 | patch | M11: a trackpad or touch tap sends its press and release in one frame, so a plugin panel's field committed in the frame its button was clicked and the value was lost: the Counter's Set count… kept the old count. The plugin now gets both, the field's value first. The `window.figcraft` docs name `screenshot`'s `data`. Numbers unchanged |
| 2026-10-31 | patch | M4: Dash and Custom strokes have a Dash cap (None, Round, Square) in the advanced stroke settings and `node.set {dashCap}`, on both ends of every dash on the canvas and in PNG, PDF and SVG. Unset, dashes keep taking the end point caps, so older files look the same; the saved file writes it only when set. Numbers unchanged |
+1
View File
@@ -648,6 +648,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
| Date | Change | Summary |
|---|---|---|
| 2026-11-03 | minor | Traditional Chinese (Taiwan) (`zh-Hant`) draft catalog: 1,975 UI strings, 641 command labels and 31 menu wordings, clean-room from the English source; zh-TW, zh-HK, zh-MO and zh-Hant read it, explicit Hans stays Simplified; Noto Sans CJK TC then Taiwan system faces; no bundled font; native review pending. Numbers unchanged |
| 2026-11-01 | patch | M4: bulleted lists draw the same bullet at every level of indentation ("Figma bullets will keep the same styling at all levels of indentation", Create bulleted and numbered lists); the deeper levels drew a white bullet and a small square, pinned as silent before. Numbered levels (1., a., i.) are unchanged. Found by a documentation audit of the text rules. Test: `tail_tests::list_numbering_by_level` (the bullets row). Numbers unchanged |
| 2026-11-01 | patch | M4: typing `1)` followed by a space at the start of a plain paragraph starts a numbered list, as `1.` already did ("Enter one of the creation characters below followed by a Space: 1. 1)", Create bulleted and numbered lists). Found by a documentation audit of the text rules. Test: `text_tail_tests::typing_a_creation_character_and_a_space_starts_a_list`. Numbers unchanged |
| 2026-11-01 | patch | M4: a link made with `text.link` (Cmd/Ctrl+K, Shift+Cmd/Ctrl+U) underlines the linked text unless it has a decoration of its own ("Links are styled with an underline by default. Use the keyboard shortcut to remove the underline", Add links to text); Underline (Cmd/Ctrl+U) takes the underline off and the link stays. Found by a documentation audit of the text rules. Test: `text_behaviour_tests::a_link_is_underlined_by_default_and_underline_takes_it_off`. Numbers unchanged |
+31 -4
View File
@@ -10,13 +10,14 @@ catalog reads English): a regional tag reads its language's catalog (`fr-CA` →
with only a regional catalog reads that one (`pt-PT` → `pt-BR`), and Latin American and US Spanish
(`es-MX`, `es-AR`, `es-CO`, `es-US`, `es-419`…) read `es-419`.
Chinese (`zh`, `zh-CN`, `zh-SG`, `zh-Hans`) reads `zh-Hans`; Traditional Chinese (`zh-Hant`,
`zh-TW`, `zh-HK`, `zh-MO`) has no catalog and reads English (or the next OS language), not
`zh-TW`, `zh-HK`, `zh-MO`) reads `zh-Hant`, and an explicit `Hans` (`zh-Hans-TW`) still reads
Simplified.
`pref.language {language}` does the same from the CLI, MCP and the control channel.
The catalogs: English (`en`, the source), and Spanish (`es`, with the Latin American overlay
`es-419`), French (`fr`), German (`de`), Dutch (`nl`), Brazilian Portuguese (`pt-BR`), Japanese
(`ja`), Korean (`ko`), Simplified Chinese (`zh-Hans`), Czech (`cs`), Indonesian (`id`), Finnish
(`ja`), Korean (`ko`), Simplified Chinese (`zh-Hans`), Traditional Chinese (`zh-Hant`), Czech
(`cs`), Indonesian (`id`), Finnish
(`fi`), Bengali (`bn`), Italian (`it`), Vietnamese (`vi`), Thai (`th`) and Bosnian (`bs`), machine-drafted and waiting for native speakers' review. Polish (`pl`,
Polski) was reviewed and approved by Lucas (@lukasz-sklad). Georgian (`ka`) was written with AI
assistance and reviewed by a native speaker, and stays a draft until a maintainer changes its status.
@@ -51,6 +52,9 @@ Enter for the key, control for a gamepad, resorte) and keeps Spanish's tú. Japa
concise, without です/ます endings, with full-width ()「」:、。 where Japanese UI text has them;
Korean labels end without 합니다 (instructions in -세요). Simplified Chinese is concise, with
full-width ():,、。“” in Chinese text and a space between Chinese and Latin words or numbers.
Traditional Chinese (Taiwan) is concise too, with full-width ():,、「」 and a space between
Chinese and Latin words or numbers, and Taiwan design terms (畫布, 圖層, 框架, 元件, 原型, 約束,
填色, 邊框, 效果, 匯出, 版本; Auto Layout stays English).
None of the three has plural forms. Turkish uses bare imperatives for buttons and menu items
(Kaydet, Sil) and the polite -in/-ın for instructions (seçin, tıklayın), puts the percent sign before
the number (%50) and keeps a noun singular after a count, so its plural entries give only `other`.
@@ -78,8 +82,12 @@ CJK and Thai text need their own fonts, which egui and Inter lack: the desktop a
one when something on screen needs it, so layer and page names in those scripts show whatever the
UI language (see `crates/ui-egui/src/cjk.rs`). The web build has no system fonts,
so Japanese, Korean and Chinese appear there only in a build with craft-fonts and
`FIGCRAFT_EMBED_CJK=1` (Noto Sans CJK SC, +16 MB); otherwise they are left out of the picker. A
Chinese UI puts its Chinese face first so Han takes Simplified Chinese shapes. Georgian is drawn in
`FIGCRAFT_EMBED_CJK=1` (Noto Sans CJK SC, +16 MB); otherwise they are left out of the picker, and
Traditional Chinese is left out unless that build has a Noto Sans CJK TC face. A
Chinese UI puts its Chinese face first so Han takes Simplified Chinese shapes, and a Traditional
Chinese UI puts an installed Noto Sans CJK TC first, then the system's Taiwan faces (Microsoft
JhengHei, PingFang TC, Heiti TC), so Han takes Traditional shapes. No font is added to this
repository. Georgian is drawn in
craft-fonts' Noto Sans Georgian, one of the built-in faces in `theme::install_fonts`, or else in an
installed font on the desktop. Web builds keep it only when the craft-fonts they're built with has
it (`crates/text/build.rs`); the commit releases pin doesn't yet (`docs/releasing.md`), so the web
@@ -104,6 +112,25 @@ that face out, so Arabic is not offered on the web yet. No font is added to
this repository. The interface's panels are not mirrored; this catalog does not claim a complete
right-to-left interface. Native review and representative in-app checks remain required.
## Traditional Chinese (Taiwan) draft
`zh-Hant.toml` adds 繁體中文(台灣) as a draft: 1,975 UI entries, 641 command labels and 31 menu
wordings (2,647 entries, measured 2026-11-03). It was translated clean-room from the English source
in `en.toml`; the Simplified Chinese catalog was not used as a source. It covers every English key,
so the completeness test keeps it that way.
Taiwan design terms: 畫布 (canvas), 圖層 (layer), 框架 (frame), 元件 (component), 原型 (prototype),
Auto Layout (kept in English), 約束 (constraints), 填色 (fill), 邊框 (stroke), 效果 (effects),
匯出 (export) and 版本 (version). Actions use concise Taiwan verbs (儲存, 刪除, 建立副本), and
quotes are 「」. Placeholders, command ids, keyboard glyphs and product names stay as in English.
Traditional Chinese has no plural forms, so plural entries give the same text for `one` and
`other`; `plural` never picks `one` for `zh-Hant`. The desktop UI adds an installed Noto Sans CJK
TC first, then Microsoft JhengHei, PingFang TC, Heiti TC and the other Taiwan faces; no font is
bundled. The web picker leaves Traditional Chinese out unless the build embeds a Noto Sans CJK TC
face, as it does for the other CJK catalogs. Native review remains open (draft), as do the in-app
checks of long labels in the right panel.
## Adding a language
1. Copy `crates/ui-egui/locales/es.toml` to `<code>.toml` (a BCP 47 code: `fr`, `pt-BR`, `ja`) and