feat: add multi-language (i18n) support with Japanese UI

This commit is contained in:
Mithril-3d committed 2026-10-11 12:52:39 +09:00
1 parent fc8b71956c
commit 3afd6ae447
11 files changed
+647 -80

No files matched your search

+1
View File
@@ -169,6 +169,7 @@ fn main() -> eframe::Result {
"SolveCraft",
options,
Box::new(move |cc| {
solvecraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx);
let mut app = SolveApp::new(Session::default(), services());
app.custom_titlebar = CUSTOM_TITLEBAR;
app.integrated_titlebar = cfg!(target_os = "macos");
+5 -4
View File
@@ -351,7 +351,8 @@ pub(crate) fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
x += 21.0;
}
let col = row.color.unwrap_or(if row.dim { t.text_dim } else { t.text });
let galley = ui.painter().layout_no_wrap(row.label.to_string(), FontId::proportional(12.5), col);
let label_display = crate::i18n::tr_dyn(row.label);
let galley = ui.painter().layout_no_wrap(label_display, FontId::proportional(12.5), col);
let lw = galley.size().x;
ui.painter().galley(pos2(x, cy - galley.size().y / 2.0), galley, col);
let mut bx = (x + lw + 6.0).min(r.right() - 16.0);
@@ -362,9 +363,9 @@ pub(crate) fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
}
crate::scenario::publish_handle(&format!("row:{}", row.label), r.center());
let tip = match hover_on {
1 => Some(if row.fold == Some(true) { "Collapse" } else { "Expand" }),
2 => Some(if row.eye == Some(true) { "Hide" } else { "Show" }),
3 => Some(if row.radio == Some(true) { "Active component" } else { "Activate" }),
1 => Some(if row.fold == Some(true) { crate::i18n::tr("Collapse") } else { crate::i18n::tr("Expand") }),
2 => Some(if row.eye == Some(true) { crate::i18n::tr("Hide") } else { crate::i18n::tr("Show") }),
3 => Some(if row.radio == Some(true) { crate::i18n::tr("Active component") } else { crate::i18n::tr("Activate") }),
_ => None,
};
let resp = match tip {
+8 -5
View File
@@ -1273,7 +1273,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
painter.circle_filled(c, 3.0, t.text_dim);
}
for (k, item) in radial.iter().enumerate() {
let galley = painter.layout_no_wrap(item.label.clone(), font.clone(), if item.enabled { t.text } else { t.text_dim });
let display_label = crate::i18n::tr(&item.label);
let galley = painter.layout_no_wrap(display_label.to_string(), font.clone(), if item.enabled { t.text } else { t.text_dim });
let size = vec2(galley.size().x + 40.0, 28.0);
let rr = slot_rect(c, k, size);
let resp = ui.interact(rr, ui.id().with(("radial", k)), Sense::click());
@@ -1295,7 +1296,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
picked = Some(item.clone());
}
if item.enabled {
resp.on_hover_text(item.shortcut.clone());
resp.on_hover_text(crate::i18n::tr(&item.shortcut).to_string());
}
}
});
@@ -1393,7 +1394,7 @@ pub fn list_row_lit(ui: &mut egui::Ui, item: &Item, lit: bool) -> Option<Item> {
ui.painter().text(
pos2(r.left() + 8.0, r.center().y + 2.0),
Align2::LEFT_CENTER,
item.label.to_uppercase(),
crate::i18n::tr(&item.label).to_uppercase(),
FontId::proportional(10.5),
t.text_dim,
);
@@ -1415,9 +1416,11 @@ pub fn list_row_lit(ui: &mut egui::Ui, item: &Item, lit: bool) -> Option<Item> {
);
}
let text = if item.enabled { t.text } else { t.text_dim.gamma_multiply(0.75) };
ui.painter().text(pos2(r.left() + 32.0, r.center().y), Align2::LEFT_CENTER, &item.label, FontId::proportional(13.0), text);
let display_label = crate::i18n::tr(&item.label);
ui.painter().text(pos2(r.left() + 32.0, r.center().y), Align2::LEFT_CENTER, display_label, FontId::proportional(13.0), text);
if !item.shortcut.is_empty() {
ui.painter().text(pos2(r.right() - 10.0, r.center().y), Align2::RIGHT_CENTER, &item.shortcut, FontId::proportional(11.5), t.text_dim);
let display_shortcut = crate::i18n::tr(&item.shortcut);
ui.painter().text(pos2(r.right() - 10.0, r.center().y), Align2::RIGHT_CENTER, display_shortcut, FontId::proportional(11.5), t.text_dim);
}
(item.enabled && resp.clicked()).then(|| item.clone())
}
+21 -10
View File
@@ -745,9 +745,10 @@ pub(crate) fn field(ui: &mut egui::Ui, s: &mut String) -> bool {
pub(crate) fn combo(ui: &mut egui::Ui, id: &str, labels: &[&str], sel: &mut usize) {
let row = ROW.with(|r| r.borrow().clone());
let r = egui::ComboBox::from_id_salt(id).selected_text(labels.get(*sel).copied().unwrap_or("")).width(FIELD_W).show_ui(ui, |ui| {
let selected = labels.get(*sel).copied().map(tr).unwrap_or("");
let r = egui::ComboBox::from_id_salt(id).selected_text(selected).width(FIELD_W).show_ui(ui, |ui| {
for (i, l) in labels.iter().enumerate() {
let item = ui.selectable_value(sel, i, *l);
let item = ui.selectable_value(sel, i, tr(l));
crate::scenario::publish_handle(&format!("dialog:{row}:{l}"), item.rect.center());
}
});
@@ -757,6 +758,11 @@ pub(crate) fn combo(ui: &mut egui::Ui, id: &str, labels: &[&str], sel: &mut usiz
thread_local! {
/// The label of the dialog row being laid out (its widgets publish `dialog:<label>` handles).
static ROW: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
static LANG: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
}
fn tr(text: &str) -> &str {
crate::i18n::tr(text)
}
/// A dialog row's widget, for scenarios and demos: the handle `dialog:<row label>`.
@@ -848,11 +854,12 @@ const LABEL_MAX_W: f32 = 150.0;
/// A row label in the dialog's label column (the grid sizes the column to its widest label).
pub(crate) fn row_label(ui: &mut egui::Ui, text: &str) {
ROW.with(|r| *r.borrow_mut() = text.to_string());
let full = egui::WidgetText::from(text).into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, egui::TextStyle::Body).size().x;
let display_text = tr(text);
let full = egui::WidgetText::from(display_text).into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, egui::TextStyle::Body).size().x;
let w = full.clamp(LABEL_W, LABEL_MAX_W);
ui.allocate_ui_with_layout(vec2(w, 22.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
ui.set_width(w);
ui.add(egui::Label::new(text).wrap());
ui.add(egui::Label::new(display_text).wrap());
});
}
@@ -1026,9 +1033,9 @@ fn input_rows(d: &mut Dialog, ui: &mut egui::Ui) {
// A chip: "↖ Select" (outlined) while empty, "N selected" (filled) once picked.
let filled = !inp.items.is_empty();
let text = if filled {
RichText::new(format!("↖ {} selected", inp.items.len())).color(Color32::WHITE)
RichText::new(format!("↖ {} {}", inp.items.len(), tr("selected"))).color(Color32::WHITE)
} else {
RichText::new("↖ Select").color(t.text)
RichText::new(format!("↖ {}", tr("Select"))).color(t.text)
};
let stroke = if active { Stroke::new(1.5, t.accent) } else { Stroke::new(1.0, t.dialog_border.gamma_multiply(1.6)) };
// Not in the Tab order: Tab moves between values.
@@ -1208,6 +1215,7 @@ fn auto_operation(app: &SolveApp, d: &mut Dialog) {
/// the view cube, sized to its content.
pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let Some(mut d) = app.dialog.take() else { return };
LANG.with(|l| *l.borrow_mut() = app.preferences.language.clone());
auto_operation(app, &mut d);
update_measure(app, &mut d);
update_fill_cells(app, &mut d);
@@ -1220,7 +1228,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let mut cancel = false;
let mut enter = false;
let wide = d.is_wide();
let heading = if d.editing.is_some() { format!("EDIT {}", title(&d.kind)) } else { title(&d.kind).to_string() };
let raw_title = title(&d.kind);
let heading = if d.editing.is_some() { format!("{} {}", tr("EDIT"), tr(raw_title)) } else { tr(raw_title).to_string() };
let frame = egui::Frame::window(&ctx.global_style())
.fill(t.dialog_bg)
.stroke(Stroke::new(1.0, t.dialog_border))
@@ -1807,17 +1816,19 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let buttons = if has_ok { 64.0 * 2.0 + ui.spacing().item_spacing.x } else { 64.0 };
crate::frame::right_aligned(ui, total, buttons, |ui| {
if has_ok {
let label = if matches!(d.kind, Kind::ConfirmDelete { .. }) { "Delete" } else { "OK" };
let raw_label = if matches!(d.kind, Kind::ConfirmDelete { .. }) { "Delete" } else { "OK" };
let label = tr(raw_label);
let b = egui::Button::new(RichText::new(label).color(if valid { Color32::WHITE } else { t.text_dim }))
.fill(if valid { t.accent } else { Color32::TRANSPARENT })
.min_size(vec2(64.0, 24.0));
let r = ui.add_enabled(valid, b);
crate::scenario::publish_handle(&format!("dialog:{label}"), r.rect.center());
crate::scenario::publish_handle(&format!("dialog:{raw_label}"), r.rect.center());
if r.clicked() {
ok = true;
}
}
let close = if matches!(d.kind, Kind::Params { .. } | Kind::Measure { .. }) { "Close" } else { "Cancel" };
let raw_close = if matches!(d.kind, Kind::Params { .. } | Kind::Measure { .. }) { "Close" } else { "Cancel" };
let close = tr(raw_close);
if ui.add(egui::Button::new(close).min_size(vec2(64.0, 24.0))).clicked() {
cancel = true;
}
+25 -7
View File
@@ -307,7 +307,14 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
t.app_bar
},
);
ui.painter().text(home.center(), Align2::CENTER_CENTER, "Home", FontId::proportional(13.0), if app.home.open { t.text } else { t.app_bar_text });
let home_text = app.tr("Home");
ui.painter().text(
home.center(),
Align2::CENTER_CENTER,
home_text,
FontId::proportional(13.0),
if app.home.open { t.text } else { t.app_bar_text },
);
crate::scenario::publish_handle("tab:home", home.center());
if response.clicked() {
app.home.show(app.session.revision);
@@ -342,7 +349,8 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
};
ui.painter().rect_filled(tr, egui::CornerRadius { nw: 5, ne: 5, sw: 0, se: 0 }, fill);
let ink = if active { t.text } else { t.app_bar_text };
let label = format!("{name}{}", if *dirty { " •" } else { "" });
let name_display = if name == "Untitled" { app.tr("Untitled") } else { name };
let label = format!("{name_display}{}", if *dirty { " •" } else { "" });
let g = ui.painter().layout_no_wrap(label, FontId::proportional(13.0), ink);
let text_w = (tr.width() - 30.0).max(10.0);
let clip = Rect::from_min_size(tr.min, vec2(text_w + 10.0, tr.height()));
@@ -447,23 +455,33 @@ pub fn prompt(app: &mut SolveApp, ctx: &egui::Context) {
quit_step(app);
let Some(i) = app.docs.closing else { return };
let name = tabs_info(app).get(i).map(|x| x.0.clone()).unwrap_or_default();
let name_display = if name == "Untitled" { crate::i18n::tr("Untitled") } else { &name };
let mut answer: Option<Answer> = None;
crate::frame::window(ctx, format!("Save changes to {name}?"), crate::frame::Width::Normal)
let title = if crate::i18n::current_lang() == "ja" {
format!("{name_display} への変更を保存しますか?")
} else {
format!("Save changes to {name}?")
};
crate::frame::window(ctx, title, crate::frame::Width::Normal)
.id(egui::Id::new("sc_close_prompt"))
.collapsible(false)
.resizable(false)
.anchor(Align2::CENTER_CENTER, vec2(0.0, 0.0))
.show(ctx, |ui| {
ui.label(format!("{name} has unsaved changes. If you don't save, they are lost."));
if crate::i18n::current_lang() == "ja" {
ui.label(format!("{name_display} には保存されていない変更があります。保存しない場合、変更内容は失われます。"));
} else {
ui.label(format!("{name} has unsaved changes. If you don't save, they are lost."));
}
ui.add_space(8.0);
ui.horizontal(|ui| {
if ui.button("Save").clicked() {
if ui.button(crate::i18n::tr("Save")).clicked() {
answer = Some(Answer::Save);
}
if ui.button("Don't Save").clicked() {
if ui.button(crate::i18n::tr("Don't Save")).clicked() {
answer = Some(Answer::DontSave);
}
if ui.button("Cancel").clicked() {
if ui.button(crate::i18n::tr("Cancel")).clicked() {
answer = Some(Answer::Cancel);
}
});
+46
View File
@@ -0,0 +1,46 @@
//! System font loading and fallback setup for multi-language (CJK/Japanese) support.
use std::sync::Arc;
/// Candidate font paths to look for on different operating systems.
const FONT_CANDIDATES: &[&str] = &[
// Windows
"C:\\Windows\\Fonts\\meiryo.ttc",
"C:\\Windows\\Fonts\\YuGothM.ttc",
"C:\\Windows\\Fonts\\YuGothR.ttc",
"C:\\Windows\\Fonts\\msgothic.ttc",
"C:\\Windows\\Fonts\\BIZ-UDGothicR.ttc",
// macOS
"/System/Library/Fonts/Hiragino Sans GB.ttc",
"/System/Library/Fonts/PingFang.ttc",
"/Library/Fonts/Arial Unicode.ttf",
// Linux
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
];
/// Configure font definitions with system CJK fallback if available.
pub fn setup_fonts(ctx: &egui::Context) {
let mut font_bytes: Option<Vec<u8>> = None;
for path in FONT_CANDIDATES {
if let Ok(bytes) = std::fs::read(path) {
font_bytes = Some(bytes);
break;
}
}
let Some(bytes) = font_bytes else {
return;
};
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert("cjk_fallback".to_owned(), Arc::new(egui::FontData::from_owned(bytes)));
fonts.families.entry(egui::FontFamily::Proportional).or_default().push("cjk_fallback".to_owned());
fonts.families.entry(egui::FontFamily::Monospace).or_default().push("cjk_fallback".to_owned());
ctx.set_fonts(fonts);
}
+424
View File
@@ -0,0 +1,424 @@
//! Multi-language (i18n) static translation dictionary for SolveCraft.
//!
//! Provides zero-overhead, completely offline local string translations.
/// Supported languages: (id, display_name).
pub const LANGUAGES: [(&str, &str); 2] = [("en", "English"), ("ja", "日本語")];
/// English to Japanese dictionary.
const JA_TRANSLATIONS: &[(&str, &str)] = &[
// --- Common & Window controls ---
("Preferences", "環境設定"),
("General", "一般"),
("Navigation", "ナビゲーション"),
("Keyboard Shortcuts", "キーボードショートカット"),
("Design", "デザイン"),
("Graphics", "グラフィックス"),
("Sketch", "スケッチ"),
("Selection", "選択"),
("Language", "言語"),
("Theme", "テーマ"),
("UI scale", "UIスケール"),
("Units of new designs", "新規デザインの単位"),
("Autosave every", "自動保存の間隔"),
("Length decimals", "長さの小数点桁数"),
("Angle decimals", "角度の小数点桁数"),
("Agent cursor", "AIエージェントカーソル"),
("Agent cursor speed", "AIエージェント速度"),
("Restore Defaults", "初期値に戻す"),
("Reset the settings on this page", "このページの設定をリセット"),
("OK", "OK"),
("Cancel", "キャンセル"),
("Apply", "適用"),
("Save", "保存"),
("Don't Save", "保存しない"),
("Untitled", "無題"),
("Dark", "ダーク"),
("Light", "ライト"),
("System", "システム"),
("Help", "ヘルプ"),
("About", "SolveCraft について"),
("Close", "閉じる"),
("Delete", "削除"),
("Expand", "展開"),
("Collapse", "折りたたむ"),
// --- Preferences Navigation & Design ---
("Pan, zoom, orbit", "パン、ズーム、オービット"),
("Reverse zoom direction", "ズーム方向を反転"),
("Zoom to the cursor", "カーソル位置へズーム"),
("Orbit around", "回転の中心"),
("View centre", "ビュー中心"),
("Point under the cursor", "カーソル下の点"),
("Animate view changes", "視点変更をアニメーション表示"),
("Animation speed", "アニメーション速度"),
("Default modelling orientation", "デフォルトのモデル方向"),
("Offer to stand up Y-up files", "Y-upファイルを立ち上げるか確認"),
("Visual style", "表示スタイル"),
// --- Application bar & Menus ---
("File", "ファイル"),
("Edit", "編集"),
("Create", "作成"),
("Modify", "修正"),
("Assemble", "アセンブリ"),
("Construct", "構築"),
("Inspect", "検査"),
("Insert", "挿入"),
("Select", "選択"),
("New Design", "新規デザイン"),
("Open...", "開く..."),
("Open", "開く"),
("Save", "保存"),
("Save As...", "名前を付けて保存..."),
("Export...", "エクスポート..."),
("Undo", "元に戻す"),
("Redo", "やり直す"),
("Cut", "切り取り"),
("Copy", "コピー"),
("Paste", "貼り付け"),
("Clear Recent", "最近使った項目をクリア"),
("Open Recent", "最近使った項目を開く"),
("Quit", "終了"),
// --- Toolbar tabs ---
("SOLID", "ソリッド"),
("SURFACE", "サーフェス"),
("MESH", "メッシュ"),
("SHEET METAL", "板金"),
("PLASTIC", "プラスチック"),
("SKETCH", "スケッチ"),
("CREATE", "作成"),
("MODIFY", "修正"),
("ASSEMBLE", "アセンブリ"),
("CONSTRUCT", "構築"),
("INSPECT", "検査"),
("INSERT", "挿入"),
("SELECT", "選択"),
("FINISH SKETCH", "スケッチを終了"),
("Finish Sketch", "スケッチを終了"),
// --- Sketch Palette ---
("SKETCH PALETTE", "スケッチ パレット"),
("Options", "オプション"),
("Linetype", "線種"),
("Normal", "標準"),
("Construction", "構築"),
("Centerline", "中心線"),
("Look At", "視点を合わせる"),
("Sketch Grid", "スケッチ グリッド"),
("Snap", "スナップ"),
("Slice", "スライス"),
("Show Profile", "プロファイルを表示"),
("Show Points", "点を表示"),
("Show Dimensions", "寸法を表示"),
("Show Constraints", "拘束を表示"),
("Show Projected Geometries", "投影ジオメトリを表示"),
("3D Sketch", "3D スケッチ"),
("Fully constrained", "完全拘束"),
("1 degree of freedom", "自由度 1"),
("degrees of freedom", "自由度"),
("Constraints conflict", "拘束が競合しています"),
// --- Dialog Titles (All Caps & Title Case) ---
("CREATE SKETCH", "スケッチを作成"),
("Create Sketch", "スケッチを作成"),
("EDIT", "編集"),
("EXTRUDE", "押し出し"),
("Extrude", "押し出し"),
("REVOLVE", "回転"),
("Revolve", "回転"),
("SWEEP", "スイープ"),
("Sweep", "スイープ"),
("LOFT", "ロフト"),
("Loft", "ロフト"),
("FILLET", "フィレット"),
("Fillet", "フィレット"),
("CHAMFER", "面取り"),
("Chamfer", "面取り"),
("SHELL", "シェル"),
("Shell", "シェル"),
("DRAFT", "勾配"),
("Draft", "勾配"),
("SCALE", "尺度"),
("Scale", "尺度"),
("COMBINE", "結合"),
("Combine", "結合"),
("SPLIT BODY", "ボディを分割"),
("Split Body", "ボディを分割"),
("MIRROR", "ミラー"),
("Mirror", "ミラー"),
("HOLE", "穴"),
("Hole", "穴"),
("THREAD", "ねじ"),
("Thread", "ねじ"),
("BOX", "直方体"),
("Box", "直方体"),
("CYLINDER", "円柱"),
("Cylinder", "円柱"),
("SPHERE", "球"),
("Sphere", "球"),
("TORUS", "トーラス"),
("Torus", "トーラス"),
("COIL", "コイル"),
("Coil", "コイル"),
("PIPE", "パイプ"),
("Pipe", "パイプ"),
("OFFSET PLANE", "オフセット平面"),
("Offset Plane", "オフセット平面"),
("PLANE AT ANGLE", "傾斜平面"),
("Plane at Angle", "傾斜平面"),
("MEASURE", "計測"),
("Measure", "計測"),
("SECTION ANALYSIS", "断面解析"),
("Section Analysis", "断面解析"),
("PARAMETERS", "パラメータ"),
("Parameters", "パラメータ"),
("MOVE", "移動"),
("Move", "移動"),
("RECTANGULAR PATTERN", "矩形パターン"),
("Rectangular Pattern", "矩形パターン"),
("CIRCULAR PATTERN", "円形パターン"),
("Circular Pattern", "円形パターン"),
("PATTERN ON PATH", "パス上のパターン"),
("Pattern on Path", "パス上のパターン"),
// --- Dialog Row Labels and Inputs ---
("Plane", "平面"),
("Profile", "プロファイル"),
("Profiles", "プロファイル"),
("Axis", "軸"),
("Axes", "軸"),
("Path", "パス"),
("Points", "点"),
("Objects", "オブジェクト"),
("Bodies", "ボディ"),
("Features", "フィーチャー"),
("Start", "開始"),
("Offset", "オフセット"),
("Profile Plane", "プロファイル平面"),
("Direction", "方向"),
("One Side", "片側"),
("Two Sides", "両側"),
("Symmetric", "対称"),
("Extent Type", "範囲タイプ"),
("Distance", "距離"),
("Distance 1", "距離 1"),
("Distance 2", "距離 2"),
("To Object", "オブジェクトまで"),
("All", "すべて"),
("Taper Angle", "テーパ角度"),
("Operation", "操作"),
("New Body", "新規ボディ"),
("Join", "結合"),
("Cut", "切り取り"),
("Intersect", "交差"),
("Angle", "角度"),
("Radius", "半径"),
("Radius Type", "半径タイプ"),
("Constant", "一定"),
("Chord Length", "弦長"),
("Variable", "可変"),
("Start Radius", "開始半径"),
("End Radius", "終了半径"),
("Flip", "反転"),
("Thickness", "厚さ"),
("Placement", "配置"),
("Single", "単一"),
("Multiple", "複数"),
("Hole Type", "穴タイプ"),
("Simple", "単純"),
("Counterbore", "座ぐり"),
("Countersink", "皿もみ"),
("Hole Tap Type", "タップタイプ"),
("Tapped", "タップ立て"),
("Drill Point", "先端"),
("Flat", "フラット"),
("Tip Angle", "先端角度"),
("Diameter", "直径"),
("Depth", "深さ"),
("Object Type", "オブジェクトタイプ"),
("Clear the selection", "選択をクリア"),
// --- Browser Tree ---
("Document Settings", "ドキュメントの設定"),
("Units", "単位"),
("Named Views", "名前付きビュー"),
("Origin", "原点"),
("Top", "上面"),
("Front", "正面"),
("Right", "右側面"),
("Home", "ホーム"),
// --- Sketch commands & tools ---
("Line", "線分"),
("Rectangle", "長方形"),
("Circle", "円"),
("Arc", "円弧"),
("Polygon", "ポリゴン"),
("Ellipse", "楕円"),
("Slot", "スロット"),
("Spline", "スプライン"),
("Conic Curve", "円錐曲線"),
("Point", "点"),
("Text", "テキスト"),
("Trim", "トリム"),
("Extend", "延長"),
("Break", "分割"),
("Sketch Scale", "スケッチ尺度"),
("Project", "投影"),
("Dimension", "寸法"),
("Sketch Dimension", "スケッチ寸法"),
("Horizontal/Vertical", "水平/垂直"),
("Coincident", "一致"),
("Tangent", "正接"),
("Equal", "等しい"),
("Parallel", "平行"),
("Perpendicular", "直交"),
("Fix/Unfix", "固定/固定解除"),
("Midpoint", "中点"),
("Concentric", "同心円"),
("Collinear", "同一直線"),
("Symmetry", "対称"),
("Curvature", "曲率"),
("Smooth", "スムーズ"),
// --- Workspace selector & environments ---
("DESIGN", "デザイン"),
("workspace", "ワークスペース"),
("Change workspace", "ワークスペースの変更"),
("Generative Design", "ジェネレーティブ デザイン"),
("Render", "レンダリング"),
("Animation", "アニメーション"),
("Simulation", "シミュレーション"),
("Manufacture", "製造"),
("Drawing", "図面"),
("coming later", "準備中"),
("current", "現在"),
// --- Browser Tree ---
("Document Settings", "ドキュメントの設定"),
("Units", "単位"),
("Named Views", "名前付きビュー"),
("Origin", "原点"),
("Top", "上面"),
("Front", "正面"),
("Right", "右側面"),
("Home", "ホーム"),
("TOP", "上面"),
("FRONT", "正面"),
("RIGHT", "右側面"),
("HOME", "ホーム"),
("Bodies", "ボディ"),
("Sketches", "スケッチ"),
("Canvases", "キャンバス"),
("Joints", "ジョイント"),
("Construction", "構築"),
("Collapse", "折りたたむ"),
("Expand", "展開"),
("Hide", "非表示"),
("Show", "表示"),
("Active component", "アクティブなコンポーネント"),
("Activate", "アクティブ化"),
];
thread_local! {
static CURRENT_LANG: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
}
/// Sets the current UI language for the thread.
pub fn set_current_lang(lang: &str) {
CURRENT_LANG.with(|l| {
*l.borrow_mut() = lang.to_string();
});
}
/// Gets the current UI language for the thread.
pub fn current_lang() -> String {
CURRENT_LANG.with(|l| l.borrow().clone())
}
/// Translate text using the thread's current language.
pub fn tr(text: &str) -> &str {
CURRENT_LANG.with(|l| {
let lang = l.borrow();
if lang.as_str() == "ja" { tr_for_lang("ja", text) } else { text }
})
}
/// Translate text using a specified language.
pub fn tr_for_lang<'a>(lang: &str, text: &'a str) -> &'a str {
if lang == "ja" {
for &(en, ja) in JA_TRANSLATIONS {
if en == text {
return ja;
}
}
}
text
}
/// Dynamically translate labels including patterned names like "Sketch1", "Body2", "Units: mm", etc.
pub fn tr_dyn(text: &str) -> String {
CURRENT_LANG.with(|l| {
let lang = l.borrow();
if lang.as_str() != "ja" {
return text.to_string();
}
for &(en, ja) in JA_TRANSLATIONS {
if en == text {
return ja.to_string();
}
}
if let Some(rest) = text.strip_prefix("Sketch")
&& !rest.is_empty()
&& rest.chars().all(|c| c.is_ascii_digit())
{
return format!("スケッチ{rest}");
}
if let Some(rest) = text.strip_prefix("Body")
&& !rest.is_empty()
&& rest.chars().all(|c| c.is_ascii_digit())
{
return format!("ボディ{rest}");
}
if let Some(rest) = text.strip_prefix("Component")
&& !rest.is_empty()
&& rest.chars().all(|c| c.is_ascii_digit())
{
return format!("コンポーネント{rest}");
}
if let Some(rest) = text.strip_prefix("Units: ") {
return format!("単位: {rest}");
}
if let Some(rest) = text.strip_prefix("Precision: ") {
return format!("精度: {rest}");
}
if let Some(rest) = text.strip_prefix("Parameters (") {
return format!("パラメータ ({rest}");
}
text.to_string()
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn translates_japanese() {
set_current_lang("ja");
assert_eq!(tr("Preferences"), "環境設定");
assert_eq!(tr("Extrude"), "押し出し");
assert_eq!(tr("CREATE SKETCH"), "スケッチを作成");
assert_eq!(tr("SKETCH PALETTE"), "スケッチ パレット");
assert_eq!(tr("Finish Sketch"), "スケッチを終了");
assert_eq!(tr("FINISH SKETCH"), "スケッチを終了");
assert_eq!(tr("Generative Design"), "ジェネレーティブ デザイン");
assert_eq!(tr("coming later"), "準備中");
assert_eq!(tr("Sketches"), "スケッチ");
assert_eq!(tr("NonExistentKey"), "NonExistentKey");
assert_eq!(tr_dyn("Sketch1"), "スケッチ1");
assert_eq!(tr_dyn("Body2"), "ボディ2");
assert_eq!(tr_dyn("Units: mm"), "単位: mm");
}
#[test]
fn keeps_english() {
set_current_lang("en");
assert_eq!(tr("Preferences"), "Preferences");
assert_eq!(tr("Extrude"), "Extrude");
assert_eq!(tr_dyn("Sketch1"), "Sketch1");
}
}
+8
View File
@@ -27,11 +27,13 @@ pub mod dialogs_surface;
pub mod dim_view;
pub mod documents;
pub mod drag_snap;
pub mod fonts;
pub mod frame;
pub mod gizmo;
pub mod gpu;
pub mod help;
pub mod home;
pub mod i18n;
pub mod icons;
pub mod inference;
pub mod keymap;
@@ -288,6 +290,11 @@ impl SolveApp {
}
}
/// Translate text using the active language preference.
pub fn tr<'a>(&self, text: &'a str) -> &'a str {
i18n::tr_for_lang(&self.preferences.language, text)
}
/// Draw the viewport on the GPU (eframe's wgpu render state, with the app's depth and
/// stencil buffer bits and MSAA sample count). Without it the viewport renders on the CPU.
pub fn set_wgpu(&mut self, rs: &egui_wgpu::RenderState, depth_bits: u8, stencil_bits: u8, samples: u32) {
@@ -813,6 +820,7 @@ impl SolveApp {
/// Lay out the whole window.
pub fn ui(&mut self, ui: &mut egui::Ui) {
let t0 = now_ms();
crate::i18n::set_current_lang(&self.preferences.language);
scenario::clear_handles();
toolbar::app_bar(self, ui);
file_load::show(self, ui.ctx());
+56 -17
View File
@@ -46,6 +46,8 @@ pub struct Prefs {
pub angle_decimals: u8,
/// `dark`, `light` or `system` (follow the operating system).
pub theme: String,
/// Interface language (`en`, `ja`).
pub language: String,
/// Interface scale (1 = 100 %).
pub ui_scale: f32,
/// The wheel zooms toward the point under the cursor (else toward the view's centre).
@@ -70,6 +72,7 @@ impl Default for Prefs {
length_decimals: 2,
angle_decimals: 1,
theme: "dark".into(),
language: "en".into(),
ui_scale: 1.0,
zoom_to_cursor: true,
look_at_anim: true,
@@ -157,6 +160,7 @@ fn defaults(app: &mut SolveApp, page: usize) {
match page {
0 => {
p.theme = d.theme;
p.language = d.language;
p.ui_scale = d.ui_scale;
p.default_units = d.default_units;
p.length_decimals = d.length_decimals;
@@ -376,7 +380,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let mut open = true;
let mut button: Option<&str> = None;
let before = app.preferences.clone();
crate::frame::window(ctx, "Preferences", crate::frame::Width::Wide)
crate::frame::window(ctx, app.tr("Preferences"), crate::frame::Width::Wide)
.id(egui::Id::new("sc_prefs"))
.open(&mut open)
.collapsible(false)
@@ -389,7 +393,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
ui.vertical(|ui| {
ui.set_width(140.0);
for (i, s) in PAGES.iter().enumerate() {
if ui.selectable_label(app.prefs_window.section == i, *s).clicked() {
if ui.selectable_label(app.prefs_window.section == i, app.tr(s)).clicked() {
app.prefs_window.section = i;
app.keymap.read_only = false;
}
@@ -399,24 +403,24 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
ui.vertical(|ui| {
// A fixed width, so the window keeps its size from page to page.
ui.set_width(540.0);
ui.label(RichText::new(PAGES.get(app.prefs_window.section).copied().unwrap_or_default()).strong().size(14.0));
ui.label(RichText::new(app.tr(PAGES.get(app.prefs_window.section).copied().unwrap_or_default())).strong().size(14.0));
ui.add_space(6.0);
page(app, ui, &t);
});
});
ui.separator();
ui.horizontal(|ui| {
if ui.button("Restore Defaults").on_hover_text("Reset the settings on this page").clicked() {
if ui.button(app.tr("Restore Defaults")).on_hover_text(app.tr("Reset the settings on this page")).clicked() {
button = Some("defaults");
}
crate::frame::right_aligned(ui, 700.0, 170.0, |ui| {
if ui.button("OK").clicked() {
if ui.button(app.tr("OK")).clicked() {
button = Some("ok");
}
if ui.button("Cancel").clicked() {
if ui.button(app.tr("Cancel")).clicked() {
button = Some("cancel");
}
if ui.button("Apply").clicked() {
if ui.button(app.tr("Apply")).clicked() {
button = Some("apply");
}
});
@@ -478,10 +482,19 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
}
egui::Grid::new(("sc_prefs_grid", app.prefs_window.section)).num_columns(2).spacing([16.0, 8.0]).show(ui, |ui| match app.prefs_window.section {
0 => {
ui.label("Theme");
ui.label(app.tr("Language"));
egui::ComboBox::from_id_salt("pref_language")
.selected_text(crate::i18n::LANGUAGES.iter().find(|(id, _)| *id == app.preferences.language).map_or("English", |x| x.1))
.show_ui(ui, |ui| {
for (id, label) in crate::i18n::LANGUAGES {
ui.selectable_value(&mut app.preferences.language, id.to_string(), label);
}
});
ui.end_row();
ui.label(app.tr("Theme"));
ui.horizontal(|ui| {
for (id, label) in THEMES {
if ui.radio(app.preferences.theme == id, label).clicked() {
if ui.radio(app.preferences.theme == id, app.tr(label)).clicked() {
app.preferences.theme = id.to_string();
if id != "system" {
app.ui.dark = id == "dark";
@@ -490,33 +503,34 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
}
});
ui.end_row();
ui.label("UI scale");
ui.label(app.tr("UI scale"));
ui.add(egui::Slider::new(&mut app.preferences.ui_scale, 0.75..=2.0).step_by(0.05).custom_formatter(|v, _| format!("{:.0} %", v * 100.0)));
ui.end_row();
ui.label("Units of new designs");
ui.label(app.tr("Units of new designs"));
egui::ComboBox::from_id_salt("pref_units").selected_text(app.preferences.default_units.clone()).show_ui(ui, |ui| {
for u in UNITS {
ui.selectable_value(&mut app.preferences.default_units, u.to_string(), u);
}
});
ui.end_row();
ui.label("Autosave every");
ui.label(app.tr("Autosave every"));
if ui.add(egui::DragValue::new(&mut app.autosave_minutes).range(0.0..=120.0).speed(0.5).suffix(" min")).changed() {
set_autosave(app);
}
ui.end_row();
note(ui, "0 turns it off");
ui.label("Length decimals");
ui.label(app.tr("Length decimals"));
ui.add(egui::DragValue::new(&mut app.preferences.length_decimals).range(0..=6));
ui.end_row();
ui.label("Angle decimals");
ui.label(app.tr("Angle decimals"));
ui.add(egui::DragValue::new(&mut app.preferences.angle_decimals).range(0..=6));
ui.end_row();
ui.label("Agent cursor");
ui.label(app.tr("Agent cursor"));
let speed_label = app.tr("Agent cursor speed");
let s = &mut app.ui.agent_cursor;
ui.checkbox(&mut s.show, "Show the pointer an AI agent moves (#35)");
ui.end_row();
ui.label("Agent cursor speed");
ui.label(speed_label);
ui.add_enabled_ui(s.show, |ui| {
crate::agent_cursor::speed_slider(ui, &mut s.speed);
});
@@ -693,6 +707,9 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
if !THEMES.iter().any(|(id, _)| *id == new.theme) || !["z", "y"].contains(&new.up_axis.as_str()) {
return Err("`theme` must be dark, light or system and `up_axis` z or y".into());
}
if !crate::i18n::LANGUAGES.iter().any(|(id, _)| *id == new.language) {
return Err("`language` must be en or ja".into());
}
if !(0.5..=2.5).contains(&new.ui_scale) || !(0.25..=4.0).contains(&new.look_at_speed) {
return Err("`ui_scale` must be 0.5…2.5 and `look_at_speed` 0.25…4".into());
}
@@ -778,11 +795,33 @@ mod tests {
// File and Help › Keyboard Shortcuts open the same dialog at its page.
app.keymap.open = true;
let ctx = egui::Context::default();
let _ = ctx.run_ui(Default::default(), |ui| show(&mut app, ui.ctx()));
let mut out = ctx.run_ui(Default::default(), |ui| show(&mut app, ui.ctx()));
out.textures_delta.clear();
assert!(app.prefs_window.open && app.prefs_window.section == PAGE_SHORTCUTS && !app.keymap.open);
assert!(control(&mut app, &json!({"open": "Plugins"})).is_err());
assert!(set(&mut app, &json!({"up_axis": "x"})).is_err());
assert!(set(&mut app, &json!({"theme": "sepia"})).is_err());
assert!(set(&mut app, &json!({"language": "fr"})).is_err());
}
#[test]
fn language_preference_switches_and_persists() {
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
assert_eq!(app.preferences.language, "en");
assert_eq!(app.tr("Preferences"), "Preferences");
assert_eq!(app.tr("Extrude"), "Extrude");
// Switch language to Japanese
set(&mut app, &json!({"language": "ja"})).unwrap();
assert_eq!(app.preferences.language, "ja");
assert_eq!(app.tr("Preferences"), "環境設定");
assert_eq!(app.tr("Extrude"), "押し出し");
// Preferences round-trip
let mut other = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
other.load_prefs(&app.prefs());
assert_eq!(other.preferences.language, "ja");
assert_eq!(other.tr("Preferences"), "環境設定");
}
/// A STEP file opened Z-up is offered to be stood up: a quarter turn about X, undoable.
+20 -11
View File
@@ -201,7 +201,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let mut finish = false;
let has_curves = !selected_curves(app).is_empty();
crate::frame::docked(
"SKETCH PALETTE",
app.tr("SKETCH PALETTE"),
egui::Id::new("sc_sketch_palette"),
anchor,
ctx.content_rect().bottom() - anchor.y - 8.0,
@@ -215,39 +215,47 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
if ui.add(egui::Button::new(RichText::new(if folded { "+" } else { "−" }).size(14.0)).frame(false)).clicked() {
FOLDED.with(|f| f.set(!folded));
}
ui.label(RichText::new("SKETCH PALETTE").strong().size(12.5));
ui.label(RichText::new(app.tr("SKETCH PALETTE")).strong().size(12.5));
});
if FOLDED.with(Cell::get) {
return;
}
let ofold = OPTIONS_FOLDED.with(Cell::get);
if ui.add(egui::Button::new(RichText::new(format!("{} Options", if ofold { "+" } else { "−" })).size(12.0)).frame(false)).clicked() {
if ui
.add(egui::Button::new(RichText::new(format!("{} {}", if ofold { "+" } else { "−" }, app.tr("Options"))).size(12.0)).frame(false))
.clicked()
{
OPTIONS_FOLDED.with(|f| f.set(!ofold));
}
if !ofold {
egui::Grid::new("sc_palette_grid").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label("Linetype");
ui.label(app.tr("Linetype"));
ui.horizontal(|ui| {
// With a selection: convert it; without: the mode new curves are drawn in.
let current = mode(app);
for (i, (label, tip)) in [("—", "Normal"), ("- -", "Construction"), ("-·-", "Centerline")].iter().enumerate() {
let on = !has_curves && current == i as u8;
let tip = if has_curves { format!("Make the selection {tip}") } else { format!("Draw {tip} curves") };
if ui.add(egui::Button::new(*label).selected(on).min_size(vec2(30.0, 20.0))).on_hover_text(tip).clicked() {
let tr_tip = app.tr(tip);
let tip_str = if has_curves {
format!("{} ({})", app.tr("Make the selection"), tr_tip)
} else {
format!("{} ({})", app.tr("Draw curves"), tr_tip)
};
if ui.add(egui::Button::new(*label).selected(on).min_size(vec2(30.0, 20.0))).on_hover_text(tip_str).clicked() {
linetype = Some(i as u8);
}
}
});
ui.end_row();
ui.label("Look At");
if ui.button("Look At").clicked() {
ui.label(app.tr("Look At"));
if ui.button(app.tr("Look At")).clicked() {
look = true;
}
ui.end_row();
for (k, label) in TOGGLES {
// The label is part of the checkbox: clicking it toggles too.
let mut on = shown(k);
if ui.checkbox(&mut on, label).changed() {
if ui.checkbox(&mut on, app.tr(label)).changed() {
set = Some((k.to_string(), on));
}
ui.end_row();
@@ -255,11 +263,12 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
});
}
ui.add_space(4.0);
ui.label(RichText::new(dof_line.as_str()).color(if ok { t.sketch_accent } else { t.text_dim }));
let tr_dof = app.tr(&dof_line);
ui.label(RichText::new(tr_dof).color(if ok { t.sketch_accent } else { t.text_dim }));
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.add_space(WIDTH - 100.0);
if ui.add(egui::Button::new(RichText::new("Finish Sketch").color(Color32::WHITE)).fill(t.accent)).clicked() {
if ui.add(egui::Button::new(RichText::new(app.tr("Finish Sketch")).color(Color32::WHITE)).fill(t.accent)).clicked() {
finish = true;
}
});
+33 -26
View File
@@ -68,26 +68,26 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
);
resp.on_hover_text(tip).clicked()
};
let file = click(ui, "folder", "File");
if click(ui, "save", "Save (Ctrl+S)") {
let file = click(ui, "folder", app.tr("File"));
if click(ui, "save", &format!("{} (Ctrl+S)", app.tr("Save"))) {
save(app);
}
if click(ui, "undo", "Undo (Ctrl+Z)") {
if click(ui, "undo", &format!("{} (Ctrl+Z)", app.tr("Undo"))) {
let _ = app.run("edit.undo", json!({}));
}
if click(ui, "redo", "Redo (Ctrl+Y)") {
if click(ui, "redo", &format!("{} (Ctrl+Y)", app.tr("Redo"))) {
let _ = app.run("edit.redo", json!({}));
}
if click(ui, "home", "Start page") {
if click(ui, "home", app.tr("Home")) {
let rev = app.session.revision;
app.home.show(rev);
}
let theme_tip = if app.ui.dark { "Light theme" } else { "Dark theme" };
let theme_tip = if app.ui.dark { app.tr("Light") } else { app.tr("Dark") };
if click(ui, if app.ui.dark { "sun" } else { "moon" }, theme_tip) {
app.ui.dark = !app.ui.dark;
app.preferences.theme = if app.ui.dark { "dark" } else { "light" }.into();
}
if click(ui, "help", "Help") {
if click(ui, "help", app.tr("Help")) {
app.help.menu = !app.help.menu;
let at = ui.ctx().input(|i| i.pointer.latest_pos()).map_or(300.0, |p| p.x);
ui.ctx().data_mut(|d| {
@@ -118,7 +118,9 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
}
icons::paint(ui.painter(), gear.shrink(4.0), "settings", t.app_bar_text, t.app_bar_text, t.app_bar);
crate::scenario::publish_handle("titlebar:preferences", gear.center());
if gresp.on_hover_text(if cfg!(target_os = "macos") { "Preferences (Cmd+,)" } else { "Preferences (Ctrl+,)" }).clicked() {
let prefs_tip =
if cfg!(target_os = "macos") { format!("{} (Cmd+,)", app.tr("Preferences")) } else { format!("{} (Ctrl+,)", app.tr("Preferences")) };
if gresp.on_hover_text(prefs_tip).clicked() {
crate::prefs::open_at(app, 0);
}
ui.painter().text(
@@ -155,17 +157,17 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
let item = |ui: &mut egui::Ui, label: &str, key: &str| {
ui.add(egui::Button::new(label).shortcut_text(key).min_size(vec2(180.0, 22.0))).clicked()
};
if item(ui, "New Design", "Ctrl+N") {
if item(ui, app.tr("New Design"), "Ctrl+N") {
crate::documents::new_design(app);
close = true;
}
if item(ui, "Open…", "Ctrl+O") {
if item(ui, &format!("{}…", app.tr("Open")), "Ctrl+O") {
if let Some(p) = app.services.pick_open.as_ref().and_then(|f| f()) {
app.open_path_async(&p);
}
close = true;
}
ui.menu_button("Open Recent", |ui| {
ui.menu_button(app.tr("Open Recent"), |ui| {
for path in app.home.recent.clone() {
let missing = !solvecraft_engine::io::vfs::exists(&path);
let label = format!("{path}{}", if missing { " (missing)" } else { "" });
@@ -176,7 +178,7 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
}
}
ui.separator();
if ui.button("Clear Recent").clicked() {
if ui.button(app.tr("Clear Recent")).clicked() {
app.home.recent.clear();
app.home.opened_at.clear();
close = true;
@@ -191,11 +193,11 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
app.start("file.insert_mesh");
close = true;
}
if item(ui, "Save", "Ctrl+S") {
if item(ui, app.tr("Save"), "Ctrl+S") {
save(app);
close = true;
}
if item(ui, "Save As…", "") {
if item(ui, &format!("{}…", app.tr("Save As")), "") {
save_as(app);
close = true;
}
@@ -217,16 +219,16 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
close = true;
}
ui.separator();
if item(ui, "Preferences…", "") {
if item(ui, &format!("{}…", app.tr("Preferences")), "") {
app.prefs_window.open = true;
close = true;
}
if item(ui, "Keyboard Shortcuts…", "") {
if item(ui, &format!("{}…", app.tr("Keyboard Shortcuts")), "") {
app.keymap.open = true;
app.keymap.read_only = false;
close = true;
}
if item(ui, "Quit", "") {
if item(ui, app.tr("Quit"), "") {
crate::documents::request_quit(app);
close = true;
}
@@ -282,13 +284,13 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
painter.text(
pos2(ws.center().x - 5.0, ws.top() + 52.0),
Align2::CENTER_CENTER,
crate::toolbar_custom::WORKSPACE,
app.tr(crate::toolbar_custom::WORKSPACE),
FontId::proportional(12.0),
t.text,
);
caret(&painter, pos2(ws.center().x + 27.0, ws.top() + 52.0), t.text);
painter.text(pos2(ws.center().x, ws.top() + 69.0), Align2::CENTER_CENTER, "workspace", FontId::proportional(10.0), t.text_dim);
if wresp.on_hover_text("Change workspace").clicked() {
painter.text(pos2(ws.center().x, ws.top() + 69.0), Align2::CENTER_CENTER, app.tr("workspace"), FontId::proportional(10.0), t.text_dim);
if wresp.on_hover_text(app.tr("Change workspace")).clicked() {
crate::context_menu::open_for(app, ws.left_bottom(), crate::context_menu::Target::Workspace);
}
crate::scenario::publish_rect("toolbar", r);
@@ -300,7 +302,8 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
}
tabs.extend(TABS.iter());
for tab in tabs {
let w = 12.0 + tab.len() as f32 * 7.4;
let display_tab = app.tr(tab);
let w = 12.0 + (display_tab.len() as f32 * 7.4).max(28.0);
let tr = Rect::from_min_size(pos2(x, tabs_y), vec2(w, 22.0));
let active = app.ui.tab == tab;
let resp = ui.interact(tr, ui.id().with(("tab", tab)), Sense::click());
@@ -317,7 +320,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
} else {
t.text_dim
};
painter.text(tr.center(), Align2::CENTER_CENTER, tab, FontId::proportional(12.0), col);
painter.text(tr.center(), Align2::CENTER_CENTER, display_tab, FontId::proportional(12.0), col);
if resp.clicked() {
app.ui.tab = tab.to_string();
}
@@ -384,9 +387,10 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
};
let icon = if c.id == "sketch.finish" { "finish" } else { c.icon };
icons::paint(&painter, br.shrink(6.0), icon, ink, fill, acc);
let tr_label = app.tr(c.label);
let tip = match &info.disabled_reason {
Some(why) => format!("{}\n{why}", c.label),
None => format!("{}{}", c.label, c.shortcut.map(|k| format!(" ({k})")).unwrap_or_default()),
Some(why) => format!("{}\n{why}", tr_label),
None => format!("{}{}", tr_label, c.shortcut.map(|k| format!(" ({k})")).unwrap_or_default()),
};
if resp.on_hover_text(tip).clicked() && info.enabled {
let id = c.id;
@@ -399,7 +403,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
}
// Panel label with drop-down.
let lr = Rect::from_min_size(pos2(px, top + 42.0), vec2(width, 18.0));
let label = panel.to_string();
let label = app.tr(panel).to_string();
let lresp = ui.interact(lr, ui.id().with(("panel", panel)), Sense::click());
crate::scenario::publish_handle(&format!("panel:{tab}:{panel}"), lr.center());
if lresp.hovered() && enabled_panel {
@@ -437,7 +441,10 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
let (ir, _) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::hover());
icons::paint(ui.painter(), ir, c.icon, t.icon, t.icon_fill, t.accent);
let b = ui.add_enabled(info.enabled, egui::Button::new(c.label).frame(false).shortcut_text(c.shortcut.unwrap_or("")));
let b = ui.add_enabled(
info.enabled,
egui::Button::new(app.tr(c.label)).frame(false).shortcut_text(c.shortcut.unwrap_or("")),
);
crate::scenario::publish_handle(&format!("toolbar:{}", c.id), b.rect.center());
if b.secondary_clicked() {
menu = Some((b.interact_pointer_pos().unwrap_or(b.rect.left_bottom()), c.id));