feat: add multi-language (i18n) support with Japanese UI
This commit is contained in:
1 parent
fc8b71956c
commit
3afd6ae447
11 files changed
+647
-80
No files matched your search
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user