diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 75663b9c..b50898f1 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -660,3 +660,6 @@ Video Video De-Interlace… Odstranit prokládání… NTSC Colors Barvy NTSC + RGB: RGB: + CMYK: CMYK: + CMYK CMYK diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 417ca3bc..b1ee42cd 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3929,3 +3929,5 @@ axis Both Ambos Duplication Duplicación Interpolation Interpolación @msg Imported library {name} Biblioteca {name} importada + RGB: RGB: + CMYK: CMYK: diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index cb2c985f..2c28a858 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3940,3 +3940,5 @@ axis Both Les deux Duplication Duplication Interpolation Interpolation @msg Imported library {name} Bibliothèque {name} importée + RGB: RGB: + CMYK: CMYK: diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 417d4dcb..8f34f6f9 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3929,3 +3929,5 @@ axis Both Entrambi Duplication Duplicazione Interpolation Interpolazione @msg Imported library {name} Libreria {name} importata + RGB: RGB: + CMYK: CMYK: diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 05952222..35e36051 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -3921,3 +3921,5 @@ theme Appearance 外観 Duplication 複製 Interpolation 補間 @msg Imported library {name} ライブラリ {name} を読み込みました + RGB: RGB: + CMYK: CMYK: diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index f43c6f97..a4624126 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -254,6 +254,13 @@ pub fn lang_from_tag(tag: &str) -> Option { cands.iter().find_map(|c| Lang::from_code(c)) } +/// True when `tag` names the generic `C` / `POSIX` locale (or a variant like `C.UTF-8`), which on +/// macOS means "no particular language" rather than English: the system language list decides. +fn is_c_locale(tag: &str) -> bool { + let base = tag.split(['.', '@']).next().unwrap_or("").to_ascii_lowercase(); + matches!(base.as_str(), "c" | "posix") +} + /// Work out the system language on a background thread, so the first frame doesn't wait for the /// locale probe (which runs `reg.exe` on Windows and `defaults` on macOS). Call it at startup; a /// frame that needs the language before the probe finishes waits for it. A no-op on wasm. @@ -295,6 +302,11 @@ fn detect_system_lang() -> Lang { // variables. `LANGUAGE` is a colon-separated priority list. for var in ["VECTORCRAFT_LOCALE", "LC_ALL", "LC_MESSAGES", "LANG", "LANGUAGE"] { let Some(v) = std::env::var(var).ok().filter(|v| !v.is_empty()) else { continue }; + // `C` / `POSIX` (and variants like `C.UTF-8`, `POSIX.UTF-8`) mean "no particular locale": + // don't force English, fall through to the OS preferred-languages list below. + if v.split(':').all(is_c_locale) { + continue; + } if let Some(l) = v.split(':').find_map(lang_from_tag) { return l; } diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 2d5b50bc..71ff51be 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2721,3 +2721,5 @@ Video Vídeo De-Interlace… Desentrelaçar… NTSC Colors Cores NTSC + RGB: RGB: + CMYK: CMYK: diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 33af9534..4b4215a2 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3927,3 +3927,5 @@ axis Both Оба Duplication Дублированием Interpolation Интерполяцией @msg Imported library {name} Импортирована библиотека {name} + RGB: RGB: + CMYK: CMYK: diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 02ab73ee..dc1f6ffa 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3912,3 +3912,5 @@ axis Both Обидві Duplication Дублюванням Interpolation Інтерполяцією @msg Imported library {name} Імпортовано бібліотеку {name} + RGB: RGB: + CMYK: CMYK: diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index fd307645..94597715 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3340,3 +3340,5 @@ axis Both 兩者 Even Fields 偶數圖場 Duplication 複製 Interpolation 內插 + RGB: RGB: + CMYK: CMYK: diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index 87df16d8..ec8ef59a 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -424,7 +424,7 @@ pub(crate) fn label_or_name(s: &str, built_in: bool) -> &str { /// command feeds); returns the one clicked, for the caller to apply. pub(crate) fn recent_colors_row(app: &VectorcraftApp, ui: &mut Ui) -> Option { let t = Tokens::get(ui.ctx()); - crate::widgets::subheader(ui, "Recent Colors"); + crate::widgets::subheader(ui, tl!("Recent Colors")); let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::hover()); ui.painter().rect_stroke(r, 0.0, egui::Stroke::new(1.0, t.input_border), egui::StrokeKind::Inside); let mut chosen = None; diff --git a/crates/ui-egui/src/panels/separations.rs b/crates/ui-egui/src/panels/separations.rs index c6bc2dc3..b1a9c918 100644 --- a/crates/ui-egui/src/panels/separations.rs +++ b/crates/ui-egui/src/panels/separations.rs @@ -50,7 +50,7 @@ fn plates_section(app: &mut VectorcraftApp, ui: &mut Ui) { run(app, "view.separationsPreview", json!({"plates": plates.iter().map(|p| p.name.clone()).collect::>()})); } }); - ui.label(egui::RichText::new("CMYK").size(12.0).color(if on { t.text } else { t.text_dim })); + ui.label(egui::RichText::new(tl!("CMYK")).size(12.0).color(if on { t.text } else { t.text_dim })); }); for p in &plates { ui.horizontal(|ui| { @@ -86,13 +86,13 @@ fn settings_section(app: &mut VectorcraftApp, ui: &mut Ui) { let st = cms::active_settings(); widgets::section_header(ui, tl!("Color Settings")); ui.horizontal(|ui| { - ui.label("RGB:"); + ui.label(tl!("RGB:")); if let Some(n) = profile_dropdown(ui, "cms-rgb", &st.rgb, ProfileKind::Rgb, None) { run(app, "edit.colorSettings", json!({"rgb": n})); } }); ui.horizontal(|ui| { - ui.label("CMYK:"); + ui.label(tl!("CMYK:")); if let Some(n) = profile_dropdown(ui, "cms-cmyk", &st.cmyk, ProfileKind::Cmyk, None) { run(app, "edit.colorSettings", json!({"cmyk": n})); } diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 67a6f84b..825e8978 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -255,9 +255,53 @@ pub fn install_fonts(ctx: &egui::Context) { mono.extend(fallback); fonts.families.insert(FontFamily::Name(FONT_MONO.into()), mono); add_craft_fonts(&mut fonts); + // System CJK fallback (PingFang / Heiti / Noto…) when craft-fonts are absent. + if let Some((name, data)) = system_cjk_font() { + fonts.font_data.insert(name.clone(), Arc::new(data)); + for stack in fonts.families.values_mut() { + stack.push(name.clone()); + } + } ctx.set_fonts(fonts); } +/// A CJK font already installed on the system, as a last-resort fallback. +fn system_cjk_font() -> Option<(String, FontData)> { + #[cfg(not(target_arch = "wasm32"))] + { + let paths: &[&str] = if cfg!(target_os = "macos") { + &[ + "/System/Library/Fonts/PingFang.ttc", + "/System/Library/Fonts/STHeiti Medium.ttc", + "/System/Library/Fonts/STHeiti Light.ttc", + "/System/Library/Fonts/Hiragino Sans GB.ttc", + "/System/Library/Fonts/Supplemental/Songti.ttc", + "/Library/Fonts/Arial Unicode.ttf", + ] + } else if cfg!(target_os = "windows") { + &[r"C:\Windows\Fonts\msyh.ttc", r"C:\Windows\Fonts\simhei.ttf", r"C:\Windows\Fonts\simsun.ttc"] + } else { + &[ + "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", + "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", + "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", + ] + }; + for path in paths { + if let Ok(bytes) = std::fs::read(path) { + let mut data = FontData::from_owned(bytes); + data.index = 0; + return Some(("system-cjk".to_string(), data)); + } + } + None + } + #[cfg(target_arch = "wasm32")] + { + None + } +} + /// The egui name of a craft-fonts face. pub(crate) fn craft_font_name(f: &vectorcraft_text::CraftFont) -> String { format!("craft-fonts {} {}", f.family, f.style) diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 6a968ddb..6cb9da82 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -598,10 +598,10 @@ pub fn fill_stroke_proxy(ui: &mut Ui, fill: &Paint, stroke: &Paint, mixed: (bool None }; ProxyClicks { - fill: fill_resp.on_hover_text("Fill (X), double-click for the Color Picker").clicked(), - stroke: stroke_resp.on_hover_text("Stroke (X), double-click for the Color Picker").clicked(), - swap: swap.on_hover_text("Swap Fill and Stroke (Shift+X)").clicked(), - default: def.on_hover_text("Default Fill and Stroke (D)").clicked(), + fill: fill_resp.on_hover_text(tl!("Fill (X), double-click for the Color Picker")).clicked(), + stroke: stroke_resp.on_hover_text(tl!("Stroke (X), double-click for the Color Picker")).clicked(), + swap: swap.on_hover_text(tl!("Swap Fill and Stroke (Shift+X)")).clicked(), + default: def.on_hover_text(tl!("Default Fill and Stroke (D)")).clicked(), pick, } } @@ -1668,7 +1668,7 @@ pub fn harmony_wheel(ui: &mut Ui, id: &str, size: f32, colors: &[Color], base: O ui.add_space(6.0); ui.horizontal(|ui| { ui.add_space(((ui.available_width() - size - 76.0) / 2.0).max(0.0)); - ui.add_sized([70.0, 22.0], egui::Label::new(egui::RichText::new("Brightness").color(t.text_dim))); + ui.add_sized([70.0, 22.0], egui::Label::new(egui::RichText::new(tl!("Brightness")).color(t.text_dim))); if let (Some(b), Some([h, s, _])) = (base, base_hsb) && let (Some(nv), _) = color_slider(ui, (id, "brightness"), v, size, &|x| crate::panels::c32(&Color::from_hsb(h, s, x))) {