From 45f92277cb1de51031eb62c9020388dc8b65de62 Mon Sep 17 00:00:00 2001 From: Noly <715285974@qq.com> Date: Wed, 14 Oct 2026 11:00:39 +0800 Subject: [PATCH] i18n: complete Simplified Chinese catalog, Chinese font fallback and a coverage xtask (#930) Adds a complete Simplified Chinese (zh) catalog covering every English message, a font fallback for Chinese text, and `cargo xtask i18n --check`, which checks that the complete catalogs (ja, zh) carry every English message and that every command label is translatable. Co-authored-by: Noly1990 <20500931+Noly1990@users.noreply.github.com> --- crates/ui-egui/src/fonts.rs | 112 ++- crates/ui-egui/src/i18n.rs | 87 ++- crates/ui-egui/src/i18n_menu_tests.rs | 129 ++++ crates/ui-egui/src/i18n_ui_tests.rs | 39 ++ crates/ui-egui/src/lib.rs | 10 +- crates/ui-egui/src/locales/de.json | 365 +++++++++- crates/ui-egui/src/locales/en.json | 78 ++- crates/ui-egui/src/locales/ja.json | 78 ++- crates/ui-egui/src/locales/ko.json | 367 +++++++++- crates/ui-egui/src/locales/zh.json | 954 ++++++++++++++++++++++++++ crates/ui-egui/src/palette.rs | 4 +- crates/ui-egui/src/prefs.rs | 10 +- crates/ui-egui/src/toolbar.rs | 5 +- docs/control-protocol.md | 2 +- docs/localization.md | 57 +- xtask/src/i18n.rs | 232 +++++++ xtask/src/main.rs | 8 + 17 files changed, 2485 insertions(+), 52 deletions(-) create mode 100644 crates/ui-egui/src/i18n_menu_tests.rs create mode 100644 crates/ui-egui/src/locales/zh.json create mode 100644 xtask/src/i18n.rs diff --git a/crates/ui-egui/src/fonts.rs b/crates/ui-egui/src/fonts.rs index 7eb78e5d..2accc482 100644 --- a/crates/ui-egui/src/fonts.rs +++ b/crates/ui-egui/src/fonts.rs @@ -10,12 +10,17 @@ use std::sync::Arc; /// Name of the CJK fallback entry in [`egui::FontDefinitions`]. pub const CJK_FALLBACK: &str = "cjk_fallback"; -/// Name of the Hangul fallback entry in [`egui::FontDefinitions`], after [`CJK_FALLBACK`]. +/// Name of the Chinese fallback entry in [`egui::FontDefinitions`], after [`CJK_FALLBACK`]. +/// A Japanese-only CJK font (Meiryo, Hiragino Gothic) lacks simplified characters, so +/// Chinese falls through to this one per glyph. +pub const CHINESE_FALLBACK: &str = "chinese_fallback"; + +/// Name of the Hangul fallback entry in [`egui::FontDefinitions`], last. pub const HANGUL_FALLBACK: &str = "hangul_fallback"; /// UI languages whose text needs the CJK font. The web app's bundled font covers Japanese only; /// add Chinese or Korean here together with a font that covers them. -pub const CJK_LANGUAGES: &[&str] = &["ja"]; +pub const CJK_LANGUAGES: &[&str] = &["ja", "zh", "ko"]; /// Whether the UI language needs a CJK font (the web app fetches one only then). pub fn needs_cjk_font(language: &str) -> bool { @@ -113,6 +118,34 @@ pub fn is_cjk_font_name(file_name: &str) -> bool { CJK_FONT_KEYWORDS.iter().any(|k| name.contains(k)) } +/// Lower-case file-name fragments that identify fonts covering simplified Chinese. Deliberately +/// separate from [`CJK_FONT_KEYWORDS`]: many Japanese fonts (Hiragino Gothic, Meiryo, Yu Gothic) +/// match those but lack simplified characters, so a Chinese fallback searches these instead. +pub const CHINESE_FONT_KEYWORDS: &[&str] = &[ + "pingfang", + "hiragino sans gb", + "stheiti", + "songti", + "arial unicode", + "msyh", + "yahei", + "simhei", + "simsun", + "wqy", + "wenquanyi", + "notosanscjk", + "notosanssc", + "droidsansfallback", + "sourcehanss", + "siyuan", +]; + +/// Whether a font file name looks like a font covering simplified Chinese. +pub fn is_chinese_font_name(file_name: &str) -> bool { + let name = file_name.to_ascii_lowercase(); + CHINESE_FONT_KEYWORDS.iter().any(|k| name.contains(k)) +} + /// Session state of the web app's on-demand CJK font. Pure state so it can be tested off the web: /// the shell does the fetch when [`CjkFontLoader::tick`] asks for it. #[derive(Debug, Default)] @@ -256,6 +289,30 @@ const HANGUL_FONT_CANDIDATES: &[&str] = &[ "/usr/share/fonts/nanum/NanumGothic.ttf", ]; +/// Fonts covering simplified Chinese, appended after the CJK fallback so Chinese renders even +/// when the CJK font found is a Japanese one without simplified characters (Hiragino Gothic, +/// Meiryo, Yu Gothic). +#[cfg(not(target_arch = "wasm32"))] +const CHINESE_FONT_CANDIDATES: &[&str] = &[ + // Windows: Microsoft YaHei + "C:\\Windows\\Fonts\\msyh.ttc", + "C:\\Windows\\Fonts\\msyh.ttf", + // macOS: PingFang SC, Hiragino Sans GB, Songti SC, STHeiti, Arial Unicode MS + "/System/Library/Fonts/PingFang.ttc", + "/System/Library/Fonts/Hiragino Sans GB.ttc", + "/System/Library/Fonts/STHeiti Light.ttc", + "/System/Library/Fonts/STHeiti Medium.ttc", + "/System/Library/Fonts/Supplemental/Songti.ttc", + "/System/Library/Fonts/Supplemental/Arial Unicode.ttf", + "/Library/Fonts/Arial Unicode.ttf", + // Linux: Noto Sans CJK SC + "/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/google-noto-cjk/NotoSansCJK-Regular.ttc", + "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf", +]; + #[cfg(not(target_arch = "wasm32"))] fn try_read_font(path: &std::path::Path) -> Option> { if let Ok(bytes) = std::fs::read(path) @@ -267,9 +324,9 @@ fn try_read_font(path: &std::path::Path) -> Option> { } } -/// Recursively search for matching CJK font files up to a bounded depth. +/// Recursively search for matching font files up to a bounded depth, testing names with `matches`. #[cfg(not(target_arch = "wasm32"))] -fn scan_dir_for_font(dir: &std::path::Path, depth_remaining: usize) -> Option> { +fn scan_dir_for_font(dir: &std::path::Path, depth_remaining: usize, matches: fn(&str) -> bool) -> Option> { if depth_remaining == 0 { return None; } @@ -282,7 +339,7 @@ fn scan_dir_for_font(dir: &std::path::Path, depth_remaining: usize) -> Option Option Option> { } if let Ok(local) = std::env::var("LOCALAPPDATA") { let user_fonts = std::path::PathBuf::from(local).join("Microsoft").join("Windows").join("Fonts"); - if let Some(bytes) = scan_dir_for_font(&user_fonts, 2) { + if let Some(bytes) = scan_dir_for_font(&user_fonts, 2, is_cjk_font_name) { return Some(bytes); } } @@ -346,7 +403,7 @@ fn find_system_cjk_font() -> Option> { { let dirs = ["/System/Library/Fonts", "/System/Library/Fonts/Supplemental", "/Library/Fonts"]; for d in dirs { - if let Some(bytes) = scan_dir_for_font(std::path::Path::new(d), 2) { + if let Some(bytes) = scan_dir_for_font(std::path::Path::new(d), 2, is_cjk_font_name) { return Some(bytes); } } @@ -356,13 +413,13 @@ fn find_system_cjk_font() -> Option> { { let search_dirs = [std::path::PathBuf::from("/usr/share/fonts"), std::path::PathBuf::from("/usr/local/share/fonts")]; for dir in &search_dirs { - if let Some(bytes) = scan_dir_for_font(dir, 3) { + if let Some(bytes) = scan_dir_for_font(dir, 3, is_cjk_font_name) { return Some(bytes); } } if let Ok(home) = std::env::var("HOME") { let user_fonts = std::path::PathBuf::from(home).join(".local/share/fonts"); - if let Some(bytes) = scan_dir_for_font(&user_fonts, 2) { + if let Some(bytes) = scan_dir_for_font(&user_fonts, 2, is_cjk_font_name) { return Some(bytes); } } @@ -371,13 +428,21 @@ fn find_system_cjk_font() -> Option> { None } -/// Configure font definitions with system CJK and Hangul fallbacks where available. +/// Configure font definitions with system CJK, Chinese and Hangul fallbacks where available. #[cfg(not(target_arch = "wasm32"))] pub fn setup_fonts(ctx: &egui::Context) { let mut fonts = egui::FontDefinitions::default(); let mut found = false; - if let Some(cjk) = find_system_cjk_font().and_then(|bytes| with_cjk_fallback(fonts.clone(), bytes)) { - fonts = cjk; + let cjk = find_system_cjk_font(); + if let Some(bytes) = cjk.clone().and_then(|bytes| with_cjk_fallback(fonts.clone(), bytes)) { + fonts = bytes; + found = true; + } + // Chinese after the CJK fallback: a Japanese-only CJK font lacks simplified characters, + // so Chinese falls through to this one per glyph. + let chinese = find_system_chinese_font().filter(|bytes| cjk.as_ref() != Some(bytes)); + if let Some(with_chinese) = chinese.and_then(|bytes| with_fallback(fonts.clone(), CHINESE_FALLBACK, bytes)) { + fonts = with_chinese; found = true; } let hangul = HANGUL_FONT_CANDIDATES.iter().find_map(|path| try_read_font(std::path::Path::new(path))); @@ -390,6 +455,27 @@ pub fn setup_fonts(ctx: &egui::Context) { } } +/// Find a font covering simplified Chinese in the known candidate paths or by scanning the +/// system font directories for [`CHINESE_FONT_KEYWORDS`] names. +#[cfg(not(target_arch = "wasm32"))] +fn find_system_chinese_font() -> Option> { + for path in CHINESE_FONT_CANDIDATES { + if let Some(bytes) = try_read_font(std::path::Path::new(path)) { + return Some(bytes); + } + } + #[cfg(target_os = "macos")] + { + let dirs = ["/System/Library/Fonts", "/System/Library/Fonts/Supplemental", "/Library/Fonts"]; + for d in dirs { + if let Some(bytes) = scan_dir_for_font(std::path::Path::new(d), 2, is_chinese_font_name) { + return Some(bytes); + } + } + } + None +} + /// On web, setup_fonts does not perform synchronous disk reads. /// Use asynchronous loading through [`solidcraft_web`] or host scripts. #[cfg(target_arch = "wasm32")] diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 7a4b46c3..283a774c 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -35,7 +35,8 @@ static ENGLISH: Catalog = Catalog::new("en", "English", include_str!("locales/en static JAPANESE: Catalog = Catalog::new("ja", "日本語", include_str!("locales/ja.json")); static GERMAN: Catalog = Catalog::new("de", "Deutsch", include_str!("locales/de.json")); static KOREAN: Catalog = Catalog::new("ko", "한국어", include_str!("locales/ko.json")); -pub const LOCALES: &[&Catalog] = &[&ENGLISH, &JAPANESE, &GERMAN, &KOREAN]; +static CHINESE: Catalog = Catalog::new("zh", "中文", include_str!("locales/zh.json")); +pub const LOCALES: &[&Catalog] = &[&ENGLISH, &JAPANESE, &GERMAN, &KOREAN, &CHINESE]; pub fn is_supported(id: &str) -> bool { LOCALES.iter().any(|catalog| catalog.id == id) @@ -180,6 +181,90 @@ mod tests { result } + /// Every English text `label()` can be asked to translate, read straight from the catalogs. + fn english_texts() -> BTreeSet<&'static str> { + ENGLISH.messages().values().map(String::as_str).collect() + } + + /// Catalogs delivered complete. English is the source catalog; Japanese was completed + /// upstream and Chinese lands complete here. German and Korean are still partial (they + /// predate the coverage guards), so their gaps are reported by `cargo xtask i18n` + /// without failing the build, while nothing in the two complete catalogs may regress. + const COMPLETE: &[&str] = &["ja", "zh"]; + + /// `text()` falls back to English silently, so a message a complete locale never received is + /// shown in English inside an otherwise translated UI, with nothing in the build to say so. + /// This is the guard against that: adding a key to `en.json` and forgetting a complete + /// catalog fails here. + #[test] + fn every_complete_catalog_carries_every_english_message() { + let english: Messages = serde_json::from_str(ENGLISH.source).unwrap(); + for catalog in LOCALES.iter().filter(|c| COMPLETE.contains(&c.id)) { + let messages: Messages = serde_json::from_str(catalog.source).unwrap(); + let missing: Vec<&str> = english.keys().filter(|id| !messages.contains_key(*id)).map(String::as_str).collect(); + assert!(missing.is_empty(), "{} is missing {} English message(s): {}", catalog.id, missing.len(), missing.join(", ")); + } + } + + /// A toolbar tab or panel reaches the UI as a bare source string (`toolbar::TABS`, + /// `CommandSpec::tab` / `.panel`) and is resolved only by `label()`'s English-text lookup, so + /// a heading missing from `en.json` can never translate in any locale. There are a dozen of + /// them, so all of them are required rather than ratcheted. + #[test] + fn every_toolbar_tab_and_panel_is_in_the_english_catalog() { + let texts = english_texts(); + let mut wanted: Vec<&str> = crate::toolbar::TABS.to_vec(); + wanted.push(crate::toolbar_custom::WORKSPACE); + for spec in solidcraft_engine::command_specs() { + wanted.push(spec.tab); + wanted.push(spec.panel); + } + let mut missing: Vec<&str> = wanted.into_iter().filter(|t| !t.is_empty() && !texts.contains(t)).collect(); + missing.sort_unstable(); + missing.dedup(); + assert!(missing.is_empty(), "toolbar tabs/panels absent from en.json: {missing:?}"); + } + + /// Every command label the engine can show must exist as an English catalog message. + /// + /// `label()` resolves source text coming from the engine by looking that exact English string + /// up in `en.json`, so a label the catalog does not carry stays English in *every* locale — + /// nothing fails, the row just looks untranslated. The whole registry now has entries (their + /// ids are `cmd.`), so this is a hard requirement rather than a budget: a new + /// command added without a catalog entry fails here. + #[test] + fn every_command_label_is_in_the_english_catalog() { + let texts = english_texts(); + let mut untranslated: Vec<&str> = solidcraft_engine::command_specs().iter().map(|c| c.label).filter(|label| !texts.contains(label)).collect(); + untranslated.sort_unstable(); + untranslated.dedup(); + assert!( + untranslated.is_empty(), + "{} command label(s) have no English catalog entry, so they would stay English in every locale: {untranslated:?}", + untranslated.len() + ); + } + + /// The whole engine registry now reaches the catalogs. Sample command labels from areas that + /// had no entry at all before (sheet metal, inspect, joints, motion) must resolve to their + /// translation rather than echo the English source text — `label()` returns its argument when + /// it finds no English message, so a dropped entry looks like a working English UI. + #[test] + fn newly_catalogued_command_labels_translate() { + let cases = [ + ("zh", "法兰", "斑马纹分析", "运动研究"), + ("ja", "フランジ", "ゼブラ解析", "モーション スタディ"), + ("de", "Flansch", "Zebra-Analyse", "Bewegungsuntersuchung"), + ("ko", "플랜지", "얼룩말줄무늬 분석", "운동 연구"), + ]; + for (lang, flange, zebra, motion) in cases { + let locale = Locale::for_language(lang); + for (source, expected) in [("Flange", flange), ("Zebra Analysis", zebra), ("Motion Study", motion)] { + assert_eq!(locale.label(source), expected, "{lang}: {source}"); + } + } + } + #[test] fn catalogs_have_valid_ids_and_matching_placeholders() { let english: Messages = serde_json::from_str(ENGLISH.source).unwrap(); diff --git a/crates/ui-egui/src/i18n_menu_tests.rs b/crates/ui-egui/src/i18n_menu_tests.rs new file mode 100644 index 00000000..e22d0ad8 --- /dev/null +++ b/crates/ui-egui/src/i18n_menu_tests.rs @@ -0,0 +1,129 @@ +//! The context menu and the browser build `Item`s out of English literals, and +//! `Locale::label()` can only translate a literal that exists as an English +//! message. A label the catalog has never seen comes straight back as English +//! and stays English in every locale, with nothing in the build to say so. +//! This reads every menu the app can open and fails on the ones it cannot +//! translate. + +use std::collections::BTreeSet; + +use crate::browser; +use crate::context_menu::{self, Item, Target}; +use crate::{Services, SolveApp}; +use serde_json::json; +use solidcraft_engine::Session; + +fn sample_app() -> SolveApp { + let mut s = Session::default(); + s.run_script(&solidcraft_engine::sample::script()).expect("sample script"); + SolveApp::new(s, Services::default()) +} + +/// Every English text `label()` can be asked to translate. +fn english_texts() -> BTreeSet { + const EN: &str = include_str!("locales/en.json"); + let messages: serde_json::Map = serde_json::from_str(EN).expect("en.json is valid JSON"); + messages.values().filter_map(|v| v.as_str().map(str::to_string)).collect() +} + +/// Separators carry no text; section headings are real text and are checked. +fn translatable(items: &[Item]) -> impl Iterator { + items.iter().map(|i| i.label.as_str()).filter(|l| !l.is_empty()) +} + +fn take(app: &SolveApp, out: &mut BTreeSet) { + let state = app.session.model.state(); + let bodies: Vec = state.bodies.iter().map(|b| b.name.clone()).collect(); + let features: Vec = app.session.doc.features.iter().map(|f| f.id).collect(); + let components: Vec = app.session.doc.components.iter().map(|c| c.id).collect(); + let groups: Vec = app.session.doc.browser_groups.iter().map(|g| g.id).collect(); + + for target in [Target::Viewport, Target::Origin, Target::Units, Target::Toolbar, Target::Workspace, Target::NamedViews] { + take_items(out, &context_menu::items(app, &target)); + } + for name in &bodies { + take_items(out, &context_menu::items(app, &Target::Body { name: name.clone() })); + } + for name in ["Named View1", "Front detail"] { + take_items(out, &context_menu::items(app, &Target::NamedView { name: name.to_string() })); + } + for id in &features { + take_items(out, &context_menu::items(app, &Target::Sketch { id: *id })); + } + for id in &components { + take_items(out, &context_menu::items(app, &Target::Component { id: *id })); + for folder in ["bodies", "sketches", "construction"] { + take_items(out, &context_menu::items(app, &Target::Folder { component: *id, folder: folder.to_string() })); + take_items(out, &browser::folder_items(app, *id, folder)); + } + } + for id in &groups { + take_items(out, &context_menu::items(app, &Target::Group { id: *id })); + take_items(out, &browser::group_items(app, *id)); + } + for spec in solidcraft_engine::command_specs().iter() { + let target = Target::ToolbarCommand { id: spec.id.to_string() }; + take_items(out, &context_menu::items(app, &target)); + } +} + +fn take_items(out: &mut BTreeSet, items: &[Item]) { + for label in translatable(items) { + out.insert(label.to_string()); + } +} + +/// A label the catalog has never seen can never translate, and the item's own +/// text is the only witness. Every menu label the app can draw must therefore +/// exist as an English message. The sample design's names are data, not text, +/// and are left alone. +#[test] +fn every_menu_label_is_in_the_english_catalog() { + let mut app = sample_app(); + let mut seen = BTreeSet::new(); + take(&app, &mut seen); + + // The labels that depend on state, so their other branch is seen too. + let names: Vec = app.session.model.state().bodies.iter().map(|b| b.name.clone()).collect(); + app.ui.show_origin = false; + take(&app, &mut seen); + for name in &names { + app.ui.locked_bodies.push(name.clone()); + app.session.visibility.hidden_bodies.push(name.clone()); + } + take(&app, &mut seen); + app.ui.locked_bodies.clear(); + app.session.visibility.hidden_bodies.clear(); + take(&app, &mut seen); + + // A menu that stops yielding labels would empty `seen` and turn the + // whole test into a tautology, so the sweep has to stay broad. + assert!(seen.len() > 40, "the sweep only reached {} label(s)", seen.len()); + + let texts = english_texts(); + let mut missing: Vec<&str> = seen + .iter() + // Unit symbols are the same word in every language: `mm` is `mm` in + // Chinese and in German, so they are not prose to translate. + .filter(|l| !texts.contains(l.as_str()) && !crate::prefs::UNITS.contains(&l.as_str())) + .map(String::as_str) + .collect(); + missing.sort_unstable(); + assert!(missing.is_empty(), "{} menu label(s) have no English catalog entry, so they stay English in every locale: {:?}", missing.len(), missing); +} + +/// The control channel and scripts call `run()` outside a render language scope, so the +/// "command done" echo must follow the saved preference rather than the thread-local scope +/// (otherwise its template falls back to English: "撤销 done" instead of "撤销 完成"). +#[test] +fn command_done_echo_follows_the_language_preference_without_a_render_scope() { + let mut app = SolveApp::new(Session::default(), Services::default()); + app.preferences.language = "zh".into(); + app.run("solid.box", json!({ "length": 5, "width": 5, "height": 5 })).expect("box"); + let echo = app.session.log.last().expect("the echo is logged"); + let zh = crate::i18n::Locale::for_language("zh"); + let expected = zh.format("ui.command_done", &[("command", zh.label(crate::command_label("solid.box")))]); + assert_eq!(echo, &expected); + assert!(echo.contains("完成"), "{echo}"); + assert!(!echo.ends_with("done"), "{echo}"); +} diff --git a/crates/ui-egui/src/i18n_ui_tests.rs b/crates/ui-egui/src/i18n_ui_tests.rs index 213835c0..52858441 100644 --- a/crates/ui-egui/src/i18n_ui_tests.rs +++ b/crates/ui-egui/src/i18n_ui_tests.rs @@ -54,6 +54,45 @@ fn language_preference_cancel_restores_previous_locale() { assert_eq!(h.app.tr("ui.preferences"), "Preferences"); } +/// The chrome a user sees before touching anything: the browser heading and the SELECT panel. +/// Both were English literals in the drawing code, and a broken message id falls back to +/// English silently, so check the word actually rendered in each language. +#[test] +fn window_chrome_renders_in_the_active_language() { + for (lang, browser, filter, faces) in [ + ("zh", "浏览器", "选择筛选", "面"), + ("ja", "ブラウザ", "選択フィルター", "面"), + ("de", "BROWSER", "Auswahlfilter", "Flächen"), + ("ko", "브라우저", "선택 필터", "면"), + ] { + let mut h = Harness::new(); + h.app.preferences.language = lang.into(); + h.app.home.open = false; + h.frames(2); + // The browser heading is on screen from the start; the selection filter lives in the + // SELECT panel, which is not the one shown by default. + assert!(h.widget(browser).is_some(), "{lang}: the browser heading must show {browser:?}"); + let select = scenario::handle_at("panel:SOLID:SELECT").expect("the SELECT panel is on the SOLID tab"); + h.step(&json!({"click": [select.x, select.y]})).unwrap(); + h.frames(2); + assert!(h.widget(filter).is_some(), "{lang}: the selection filter must show {filter:?}"); + assert!(h.widget(faces).is_some(), "{lang}: the Faces checkbox must show {faces:?}"); + } +} + +/// The command palette's hint embeds a JSON example, so its braces must not be mistaken for a +/// placeholder name by the catalog's parser, and the example must survive interpolation whole. +#[test] +fn command_palette_hint_keeps_its_json_example_intact() { + let mut h = Harness::new(); + h.app.preferences.language = "zh".into(); + h.app.home.open = false; + h.frames(2); + let hint = crate::i18n::format("ui.command_palette_hint", &[("example", "Extrude {\"distance\": 20}")]); + assert!(hint.contains("Extrude {\"distance\": 20}"), "the example is inserted literally: {hint:?}"); + assert!(!hint.contains("{example}"), "the placeholder is substituted: {hint:?}"); +} + #[test] fn translated_dialog_buttons_do_not_wrap() { fn cancel_rows(shape: &egui::Shape) -> Option { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 9d539c31..16383d08 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -40,6 +40,8 @@ mod hole_placement_ui_tests; pub mod home; pub mod i18n; #[cfg(test)] +mod i18n_menu_tests; +#[cfg(test)] mod i18n_ui_tests; pub mod icons; pub mod inference; @@ -494,7 +496,13 @@ impl SolveApp { } match &r { Ok(_) => { - self.session.echo(format!("{} done", command_label(id))); + // Programmatic callers (the control channel, scripts) run outside a render + // language scope, so resolve the echo from the saved preference instead of the + // thread-local scope. The command's own label is translated too, so the echo + // reads the same way the toolbar button it came from does. + let language = i18n::Locale::for_language(&self.preferences.language); + let label = language.label(command_label(id)); + self.session.echo(language.format("ui.command_done", &[("command", label)])); if matches!(id, "file.new" | "doc.open" | "doc.recover") { self.sketch_choices = sketch_families::Choices::default(); dim_view::cancel(); diff --git a/crates/ui-egui/src/locales/de.json b/crates/ui-egui/src/locales/de.json index fcb5770f..1a838b1a 100644 --- a/crates/ui-egui/src/locales/de.json +++ b/crates/ui-egui/src/locales/de.json @@ -65,14 +65,15 @@ "ui.combine": "Kombinieren", "ui.coming_later": "bald verfügbar", "ui.concentric": "Konzentrisch", + "ui.configurations": "Konfigurationen", "ui.conic_curve": "Kegelschnitt", "ui.constant": "Konstant", "ui.constraints_conflict": "Bedingungen konfliktreich", "ui.construct": "Konstruktion", "ui.construction": "Konstruktionslinie", "ui.copy": "Kopieren", - "ui.corner": "Ecke", "ui.corners": "Ecken", + "ui.corner": "Ecke", "ui.counterbore": "Tiefenbohrung", "ui.countersink": "Senkung", "ui.create": "Erstellen", @@ -112,6 +113,7 @@ "ui.extent_type": "Ausdehnungstyp", "ui.extrude": "Extrudieren", "ui.factor": "Faktor", + "ui.feature_history": "Feature-Verlauf", "ui.feature_options": "Funktionsoptionen", "ui.features": "Funktionen", "ui.file": "Datei", @@ -249,8 +251,8 @@ "ui.profile_plane": "Profilebene", "ui.profiles": "Profile", "ui.project": "Projizieren", - "ui.quantity": "Anzahl", "ui.quantity_2": "Anzahl 2", + "ui.quantity": "Anzahl", "ui.quit": "Beenden", "ui.radius": "Radius", "ui.radius_type": "Radius-Typ", @@ -273,7 +275,6 @@ "ui.shell": "Hülle", "ui.show": "Einblenden", "ui.show_constraints": "Bedingungen", - "ui.show_construction_geometries": "Konstruktionsgeometrien", "ui.show_dimensions": "Maße", "ui.show_points": "Punkte anzeigen", "ui.show_profile": "Profil anzeigen", @@ -291,8 +292,8 @@ "ui.slot": "Langloch", "ui.smooth": "Glatt", "ui.snap": "Einrasten", - "ui.spacing": "Abstand", "ui.spacing_2": "Abstand 2", + "ui.spacing": "Abstand", "ui.sphere": "Sphäre", "ui.spline": "Spline", "ui.split_body": "Körper trennen", @@ -309,11 +310,13 @@ "ui.theme": "Design", "ui.thickness": "Dicke", "ui.thread": "Gewinde", + "ui.timeline_bottom": "Timeline unten", + "ui.timeline_left_note": "Im linken Layout wird die Rücksetzmarke über dem Teile- und Szenenbrowser eingeblendet.", + "ui.timeline_left_sidebar": "Linke Seitenleiste", "ui.tip_angle": "Spitzenwinkel", "ui.to_object": "Bis Objekt", "ui.top": "Oben", "ui.torus": "Torus", - "ui.total_angle": "Gesamtwinkel", "ui.trim": "Kürzen", "ui.two_sides": "Beidseitig", "ui.ui_scale": "UI-Skalierung", @@ -325,9 +328,359 @@ "ui.view_centre": "Ansichtszentrum", "ui.visual_style": "Ansichtsstil", "ui.workspace": "Arbeitsbereich", - "ui.x_distance": "X-Abstand", "ui.y_distance": "Y-Abstand", + "ui.x_distance": "X-Abstand", "ui.zoom_to_the_cursor": "Zum Cursor zoomen", + "ui.animateJoint": "Gelenk animieren", + "ui.calibrate": "Kalibrieren", + "ui.customizeToolbar": "Symbolleiste anpassen…", + "ui.editJointLimits": "Gelenkgrenzen bearbeiten", + "ui.exportDxf": "Als DXF exportieren…", + "ui.findInTimeline": "In der Zeitleiste suchen", + "ui.findInWindow": "Im Fenster suchen", + "ui.fit": "Einpassen", + "ui.groupSelected": "Auswahl gruppieren", + "ui.hideAxes": "Achsen ausblenden", + "ui.hideDimension": "Bemaßung ausblenden", + "ui.hideOrigin": "Ursprung ausblenden", + "ui.hidePlanes": "Ebenen ausblenden", + "ui.hideProfile": "Profil ausblenden", + "ui.homeView": "Startansicht", + "ui.isolate": "Isolieren", + "ui.lock": "Sperren", + "ui.newSketchGroup": "Neue Skizzengruppe", + "ui.normalConstruction": "Normal / Konstruktion", + "ui.orbit": "Drehen", + "ui.heading_position": "POSITION", + "ui.pan": "Verschieben", + "ui.pinToShortcuts": "An Verknüpfungen anheften", + "ui.pinToToolbar": "An Symbolleiste anheften", + "ui.properties": "Eigenschaften", + "ui.removeSection": "Schnitt entfernen", + "ui.removeFromToolbar": "Aus der Symbolleiste entfernen", + "ui.repeat": "Wiederholen", + "ui.report_a_bug": "Fehler melden", + "ui.restore": "Wiederherstellen", + "ui.heading_setup": "EINRICHTEN", + "ui.selectOther": "Andere auswählen…", + "ui.selectTangentChain": "Tangentialkette auswählen", + "ui.showAll": "Alle einblenden", + "ui.showAllBodies": "Alle Volumenkörper einblenden", + "ui.showAxes": "Achsen einblenden", + "ui.showDimension": "Bemaßung einblenden", + "ui.showOrigin": "Ursprung einblenden", + "ui.showPlanes": "Ebenen einblenden", + "ui.suppress": "Unterdrücken", + "ui.command_palette_hint": "Befehl eingeben, z. B. {example} — oder S drücken, um zu suchen", + "ui.unground": "Fixierung lösen", + "ui.unisolate": "Isolierung aufheben", + "ui.unlock": "Entsperren", + "ui.unsuppress": "Unterdrückung aufheben", + "ui.updateToCurrentView": "Auf aktuelle Ansicht aktualisieren", + "ui.zoom": "Zoomen", + "ui.command_done": "{command} fertig", + "cmd.appearance_assign": "Erscheinungsbild", + "cmd.appearance_library": "Erscheinungsbild-Bibliothek", + "cmd.appearance_list": "Erscheinungsbilder auflisten", + "cmd.body_rename": "Körper umbenennen", + "cmd.browser_group": "Neue Gruppe", + "cmd.browser_move": "Browser-Elemente verschieben", + "cmd.browser_order": "Browser-Elemente ordnen", + "cmd.browser_rename_group": "Gruppe umbenennen", + "cmd.browser_ungroup": "Gruppierung aufheben", + "cmd.browser_visibility": "Ein-/Ausblenden", + "cmd.canvas_calibrate": "Zeichenfläche kalibrieren", + "cmd.canvas_decal": "Dekal", + "cmd.canvas_delete": "Zeichenfläche löschen", + "cmd.canvas_edit": "Zeichenfläche bearbeiten", + "cmd.canvas_insert": "Zeichenfläche", + "cmd.component_activate": "Komponente aktivieren", + "cmd.component_break_link": "Verknüpfung trennen", + "cmd.component_capture_position": "Position erfassen", + "cmd.component_create": "Neue Komponente", + "cmd.component_delete": "Komponente löschen", + "cmd.component_from_bodies": "Komponenten aus Körpern erstellen", + "cmd.component_insert_design": "Konstruktion einfügen", + "cmd.component_links": "Verknüpfungen auflisten", + "cmd.component_list": "Komponenten auflisten", + "cmd.component_move_bodies": "Körper in Komponente verschieben", + "cmd.component_move_sketches": "Skizzen in Komponente verschieben", + "cmd.component_paste_new": "Neu einfügen", + "cmd.component_properties": "Physikalische Eigenschaften der Komponente", + "cmd.component_rename": "Komponente umbenennen", + "cmd.component_revert_position": "Position zurücksetzen", + "cmd.component_update_links": "Verknüpfungen aktualisieren", + "cmd.config_activate": "Konfiguration aktivieren", + "cmd.config_column": "Konfigurationsspalte", + "cmd.config_configure": "Konfigurieren", + "cmd.config_preview": "Konfiguration vorschauen", + "cmd.config_row": "Konfigurationszeile", + "cmd.config_rule": "Konfigurationsregel", + "cmd.config_rules": "Konfigurationsregeln", + "cmd.config_table": "Konfigurationstabelle anzeigen", + "cmd.construct_axis_edge": "Achse durch Kante", + "cmd.construct_axis_normal_to_face": "Achse senkrecht zur Fläche", + "cmd.construct_axis_through_cylinder": "Achse durch Zylinder/Kegel/Torus", + "cmd.construct_axis_two_planes": "Achse durch zwei Ebenen", + "cmd.construct_axis_two_points": "Achse durch zwei Punkte", + "cmd.construct_plane_along_path": "Ebene entlang Pfad", + "cmd.construct_plane_midplane": "Mittenebene", + "cmd.construct_plane_perpendicular": "Rechtebene", + "cmd.construct_plane_tangent": "Tangentialebene", + "cmd.construct_plane_three_points": "Ebene durch drei Punkte", + "cmd.construct_plane_two_edges": "Ebene durch zwei Kanten", + "cmd.construct_point_along_path": "Punkt entlang Pfad", + "cmd.construct_point_center": "Punkt im Mittelpunkt von Kreis/Kugel/Torus", + "cmd.construct_point_edge_plane": "Punkt an Kante und Ebene", + "cmd.construct_point_three_planes": "Punkt aus drei Ebenen", + "cmd.construct_point_two_edges": "Punkt aus zwei Kanten", + "cmd.construct_point_vertex": "Punkt an Scheitelpunkt", + "cmd.contact_check": "Kontakt prüfen", + "cmd.contact_create": "Neuer Kontaktsatz", + "cmd.contact_disable_all": "Kontakt deaktivieren", + "cmd.contact_edit": "Kontaktsatz bearbeiten", + "cmd.contact_enable_all": "Alle Kontakte aktivieren", + "cmd.contact_enable_sets": "Kontaktsätze aktivieren", + "cmd.contact_list": "Kontaktsätze auflisten", + "cmd.diagnostics_crash_report": "Absturzbericht", + "cmd.diagnostics_crash_reports": "Absturzberichte", + "cmd.diagnostics_crash_reports_clear": "Absturzberichte löschen", + "cmd.diagnostics_crash_test": "Absturztest", + "cmd.doc_recover": "Konstruktion wiederherstellen", + "cmd.doc_recovery_discard": "Wiederhergestellte Konstruktion verwerfen", + "cmd.doc_recovery_list": "Wiederherstellbare Konstruktionen", + "cmd.document_inspect": "Konstruktion prüfen", + "cmd.document_units": "Einheiten der Konstruktion ändern", + "cmd.edit_checkpoint": "Prüfpunkt", + "cmd.edit_restore_checkpoint": "Prüfpunkt wiederherstellen", + "cmd.engine_commands": "Befehle auflisten", + "cmd.explode_create": "Explosionsdarstellung", + "cmd.explode_delete": "Explosionsdarstellung löschen", + "cmd.explode_list": "Explosionsdarstellungen auflisten", + "cmd.explode_show": "Explosionsdarstellung anzeigen", + "cmd.expr_evaluate": "Ausdruck auswerten", + "cmd.face_delete": "Fläche löschen", + "cmd.file_export": "Exportieren", + "cmd.file_insert_mesh": "Netz einfügen", + "cmd.file_insert_step": "STEP einfügen", + "cmd.file_save_mesh": "Als Netz speichern", + "cmd.inspect_accessibility": "Zugänglichkeitsanalyse", + "cmd.inspect_center_of_mass": "Massenschwerpunkt", + "cmd.inspect_curvature_comb": "Krümmungskamm-Analyse", + "cmd.inspect_curvature_map": "Krümmungskartenanalyse", + "cmd.inspect_draft": "Auswinkelanalyse", + "cmd.inspect_environment_map": "Umgebungskartenanalyse", + "cmd.inspect_interference": "Eingriff", + "cmd.inspect_isocurve": "Isokurvenanalyse", + "cmd.inspect_minimum_radius": "Mindestradiusanalyse", + "cmd.inspect_zebra": "Zebra-Analyse", + "cmd.joint_as_built": "Fertigungsgelenk", + "cmd.joint_create": "Gelenk", + "cmd.joint_delete": "Gelenk löschen", + "cmd.joint_drive": "Gelenke antreiben", + "cmd.joint_edit": "Gelenk bearbeiten", + "cmd.joint_limits": "Gelenkgrenzen", + "cmd.joint_list": "Gelenke auflisten", + "cmd.joint_motion_link": "Bewegungsverknüpfung", + "cmd.joint_origin": "Gelenkursprung", + "cmd.joint_rigid_group": "Starrgruppe", + "cmd.joint_solve": "Gelenke lösen", + "cmd.material_assign": "Physikalisches Material", + "cmd.material_create": "Neues physikalisches Material", + "cmd.material_delete": "Physisches Material löschen", + "cmd.material_edit": "Physisches Material bearbeiten", + "cmd.material_list": "Materialien auflisten", + "cmd.mesh_convert": "Netz konvertieren", + "cmd.model_edges": "Kanten auflisten", + "cmd.model_faces": "Flächen auflisten", + "cmd.model_threads": "Gewinde auflisten", + "cmd.motion_delete": "Bewegungsuntersuchung löschen", + "cmd.motion_export": "Bewegungsuntersuchung exportieren", + "cmd.motion_list": "Bewegungsuntersuchungen auflisten", + "cmd.motion_play": "Bewegungsuntersuchung abspielen", + "cmd.motion_study": "Bewegungsuntersuchung", + "cmd.occurrence_copy": "Einfügen (Instanz)", + "cmd.occurrence_ground": "Festsetzen", + "cmd.occurrence_move": "Instanz verschieben", + "cmd.occurrence_properties": "Physikalische Eigenschaften der Instanz", + "cmd.parameters_add": "Benutzerparameter hinzufügen", + "cmd.parameters_change": "Parameter ändern", + "cmd.parameters_comment": "Parameter kommentieren", + "cmd.parameters_complete": "Ausdruck vervollständigen", + "cmd.parameters_delete": "Parameter löschen", + "cmd.parameters_export": "Parameter exportieren", + "cmd.parameters_favorite": "Favorisierter Parameter", + "cmd.parameters_graph": "Parametergraph", + "cmd.parameters_import": "Parameter importieren", + "cmd.parameters_list": "Parameter auflisten", + "cmd.parameters_rename": "Parameter umbenennen", + "cmd.parameters_users": "Parameterverwendungen", + "cmd.parts_fastener": "Verbindungselement einfügen", + "cmd.parts_insert": "Bauteil einfügen", + "cmd.parts_library": "Standardbauteile", + "cmd.parts_resize": "Bauteilgröße ändern", + "cmd.plastic_assign_rule": "Kunststoffregel zuweisen", + "cmd.plastic_boss": "Erhöhung", + "cmd.plastic_lip": "Lippe", + "cmd.plastic_manage_rules": "Kunststoffregeln verwalten", + "cmd.plastic_rest": "Auflage", + "cmd.plastic_rules": "Kunststoffregeln auflisten", + "cmd.plastic_snap_fit": "Rastverbindung", + "cmd.select_clear": "Auswahl aufheben", + "cmd.sheet_convert": "In Blech umwandeln", + "cmd.sheet_export_bend_table": "Biegungstabelle exportieren", + "cmd.sheet_export_dxf": "Flachmuster als DXF exportieren", + "cmd.sheet_flange": "Flansch", + "cmd.sheet_flat_pattern": "Flachmuster erstellen", + "cmd.sheet_fold": "Falten", + "cmd.sheet_hem": "Umkantung", + "cmd.sheet_manage_rules": "Blechregeln", + "cmd.sheet_refold": "Wieder falten", + "cmd.sheet_rules": "Blechregeln auflisten", + "cmd.sheet_unfold": "Entfalten", + "cmd.sketch_arc_center_point": "Mittelpunkt-Kreisbogen", + "cmd.sketch_arc_tangent": "Tangentialbogen", + "cmd.sketch_arc_three_point": "3-Punkt-Kreisbogen", + "cmd.sketch_auto_constrain": "Auto-Bedingung", + "cmd.sketch_auto_constrain_from_datum": "Auto-Bedingung von Bezug", + "cmd.sketch_auto_project": "Kanten automatisch auf Referenz projizieren", + "cmd.sketch_blend_curve": "Übergangskurve", + "cmd.sketch_chain": "Kette", + "cmd.sketch_chamfer_distance_angle": "Abschrägung mit Abstand und Winkel", + "cmd.sketch_chamfer_equal_distance": "Abschrägung mit gleichem Abstand", + "cmd.sketch_chamfer_two_distance": "Abschrägung mit zwei Abständen", + "cmd.sketch_circle_center": "Kreis aus Mittelpunkt und Durchmesser", + "cmd.sketch_circle_three_point": "3-Punkt-Kreis", + "cmd.sketch_circle_three_tangent": "3-Tangenten-Kreis", + "cmd.sketch_circle_two_point": "2-Punkt-Kreis", + "cmd.sketch_circle_two_tangent": "2-Tangenten-Kreis", + "cmd.sketch_constrainer": "Skizzen-Bedingung", + "cmd.sketch_constraint_fix": "Fixieren/Lösen", + "cmd.sketch_constraint_midpoint": "Mittelpunkt", + "cmd.sketch_construction": "Normal/Konstruktion", + "cmd.sketch_delete": "Skizzenentitäten löschen", + "cmd.sketch_dimension_text": "Maßtext verschieben", + "cmd.sketch_drag": "Skizzengeometrie ziehen", + "cmd.sketch_drag_end": "Skizzen-Ziehen beenden", + "cmd.sketch_draw_dimensioned": "Skizzengeometrie mit Maßen zeichnen", + "cmd.sketch_edit": "Skizze bearbeiten", + "cmd.sketch_edit_text": "Text bearbeiten", + "cmd.sketch_export_dxf": "Als DXF speichern", + "cmd.sketch_finish_auto_constrain": "Mit Auto-Bedingung fertigstellen", + "cmd.sketch_fit_curves_to_section": "Kurven an Netzquerschnitt anpassen", + "cmd.sketch_glyphs": "Bedingungssymbole", + "cmd.sketch_include_3d": "3D-Geometrie einbeziehen", + "cmd.sketch_insert_dxf": "DXF einfügen", + "cmd.sketch_insert_svg": "SVG einfügen", + "cmd.sketch_inspect": "Skizze prüfen", + "cmd.sketch_intersection_curve": "Schnittkurve", + "cmd.sketch_isoparametric_curve": "Isoparametrische Kurve", + "cmd.sketch_line3d": "3D-Linie", + "cmd.sketch_line_midpoint": "Linie über den Mittelpunkt", + "cmd.sketch_move": "Skizzenentitäten verschieben/kopieren", + "cmd.sketch_move3d": "3D-Punkt verschieben", + "cmd.sketch_move_point": "Skizzenpunkt ziehen", + "cmd.sketch_offset_break": "Versatzverknüpfung trennen", + "cmd.sketch_options": "Skizzenpalettenoptionen", + "cmd.sketch_point3d": "3D-Punkt", + "cmd.sketch_polygon_circumscribed": "Umgeschriebenes Polygon", + "cmd.sketch_polygon_edge": "Polygon über eine Kante", + "cmd.sketch_polygon_inscribed": "Eingeschriebenes Polygon", + "cmd.sketch_project_to_surface": "Auf Fläche projizieren", + "cmd.sketch_rectangle_center": "Rechteck vom Mittelpunkt", + "cmd.sketch_rectangle_three_point": "3-Punkt-Rechteck", + "cmd.sketch_rectangle_two_point": "2-Punkt-Rechteck", + "cmd.sketch_redefine": "Skizzierebene neu definieren", + "cmd.sketch_slot_arc_center": "Mittelpunkt-Bogenschlitz", + "cmd.sketch_slot_arc_three_point": "3-Punkt-Bogenschlitz", + "cmd.sketch_slot_center_point": "Mittelpunktschlitz", + "cmd.sketch_slot_center_to_center": "Schlitz von Mittelpunkt zu Mittelpunkt", + "cmd.sketch_slot_overall": "Gesamtschlitz", + "cmd.sketch_snap": "Einrastinferenz", + "cmd.sketch_solve": "Skizze lösen", + "cmd.sketch_spline3d": "3D-Spline", + "cmd.sketch_spline_control_point": "Spline mit Kontrollpunkten", + "cmd.sketch_spline_control_point_5": "Spline mit Kontrollpunkten (Grad 5)", + "cmd.sketch_spline_fit_point": "Spline mit Fitpunkten", + "cmd.sketch_spun_profile": "Gedrehtes Profil", + "cmd.sketch_toggle_driven": "Abhängiges Maß umschalten", + "cmd.solid_align": "Ausrichten", + "cmd.solid_boundary_fill": "Begrenzungsfüllung", + "cmd.solid_bounding_solid": "Begrenzender Körper", + "cmd.solid_emboss": "Prägen", + "cmd.solid_move": "Verschieben/Kopieren", + "cmd.solid_offset_face": "Fläche versetzen", + "cmd.solid_press_pull": "Drücken/Ziehen", + "cmd.solid_remove": "Entfernen", + "cmd.solid_replace_face": "Fläche ersetzen", + "cmd.solid_rib": "Rippe", + "cmd.solid_split_face": "Fläche teilen", + "cmd.solid_web": "Gewebe", + "cmd.surface_patch": "Patch", + "cmd.surface_stitch": "Nähen", + "cmd.surface_thicken": "Dicke vergeben", + "cmd.surface_extrude": "Flächen-Extrusion", + "cmd.surface_revolve": "Flächen-Rotation", + "cmd.surface_sweep": "Flächen-Sweep", + "cmd.surface_loft": "Flächen-Loft", + "cmd.surface_ruled": "Regelfläche", + "cmd.surface_boundary": "Begrenzungsfläche", + "cmd.surface_offset": "Offsetfläche", + "cmd.surface_blend": "Verbindungsfläche", + "cmd.surface_fillet": "Flächenverrundung", + "cmd.surface_untrim": "Trimmen aufheben", + "cmd.surface_unstitch": "Verbund aufheben", + "cmd.surface_reverse_normal": "Normale umkehren", + "cmd.inspect_continuity": "Kontinuitätsprüfung", + "cmd.timeline_compute_all": "Alles berechnen", + "cmd.timeline_copy": "Merkmale kopieren", + "cmd.timeline_dependents": "Merkmalsabhängigkeiten", + "cmd.timeline_inspect": "Merkmal prüfen", + "cmd.timeline_paste": "Merkmale einfügen", + "cmd.timeline_redefine": "Merkmal bearbeiten", + "cmd.timeline_rename": "Umbenennen", + "cmd.timeline_reorder": "Merkmal neu ordnen", + "cmd.timeline_roll_to": "Verlaufsmarkierung verschieben", + "cmd.timeline_rollback": "Zeitleistenmarkierung verschieben", + "cmd.timeline_suppress": "Merkmal unterdrücken", + "cmd.view_apply": "Benannte Ansicht wiederherstellen", + "cmd.view_delete": "Benannte Ansicht löschen", + "cmd.view_list": "Benannte Ansichten auflisten", + "cmd.view_rename": "Benannte Ansicht umbenennen", + "cmd.view_save": "Neue benannte Ansicht", + "ui.export_ellipsis_keymap": "Exportieren…", + "ui.heading_configure": "KONFIGURIEREN", + "ui.heading_constraints": "RANDBEDINGUNGEN", + "ui.heading_utilities": "WERKZEUGE", + "BROWSER": "BROWSER", + "Drive Joint": "Gelenk antreiben", + "Edit Profile Sketch": "Profile-Skizze bearbeiten", + "Find in Browser": "Im Browser suchen", + "Report a bug — review a screenshot and open a GitHub draft": "Fehler melden — Screenshot prüfen und einen GitHub-Entwurf öffnen", + "Reset Toolbar": "Symbolleiste zurücksetzen", + "Select bodies (a face picks its body)": "Körper auswählen (eine Fläche wählt ihren Körper)", + "Selection filter": "Auswahlfilter", + "Revert Position": "Position zurücksetzen", + "Construction": "Konstruktionslinie", + "Faces": "Flächen", + "Edges": "Kanten", + "Vertices": "Scheitelpunkte", + "Sketch geometry and profiles": "Skizzengeometrie und Profile", + "Sketch curves": "Skizzenkurven", + "Sketch points": "Skizzenpunkte", + "Sketch dimensions": "Skizzenmaße", + "Sketch constraints": "Skizzenbedingungen", + "Flat": "Flach", + "cmd.render_backends": "Render-Backends", + "cmd.view_navigate": "Ansicht navigieren", + "timeline.group": "Neuen Merkmalsordner anlegen", + "timeline.ungroup": "Merkmalsordner entfernen", + "timeline.rename_group": "Merkmalsordner umbenennen", + "timeline.move_to_group": "Merkmale in Ordner verschieben", + "timeline.move_many": "Zeitleistenelemente verschieben", + "timeline.reorder_many": "Merkmale neu ordnen", + "ui.show_construction_geometries": "Konstruktionsgeometrien", + "ui.total_angle": "Gesamtwinkel", "sketch_text.builtin_font": "{family} (integriert)", "sketch_text.font_search": "Schriftarten suchen", "sketch_text.loading_fonts": "Installierte Schriftarten werden gesucht…", diff --git a/crates/ui-egui/src/locales/en.json b/crates/ui-egui/src/locales/en.json index d7ff60a9..ba894edd 100644 --- a/crates/ui-egui/src/locales/en.json +++ b/crates/ui-egui/src/locales/en.json @@ -245,6 +245,19 @@ "cmd.surface_patch": "Patch", "cmd.surface_stitch": "Stitch", "cmd.surface_thicken": "Thicken", + "cmd.surface_extrude": "Surface Extrude", + "cmd.surface_revolve": "Surface Revolve", + "cmd.surface_sweep": "Surface Sweep", + "cmd.surface_loft": "Surface Loft", + "cmd.surface_ruled": "Ruled Surface", + "cmd.surface_boundary": "Boundary Surface", + "cmd.surface_offset": "Offset Surface", + "cmd.surface_blend": "Blend Surface", + "cmd.surface_fillet": "Surface Fillet", + "cmd.surface_untrim": "Untrim", + "cmd.surface_unstitch": "Unstitch", + "cmd.surface_reverse_normal": "Reverse Normal", + "cmd.inspect_continuity": "Continuity Analysis", "cmd.timeline_compute_all": "Compute All", "cmd.timeline_copy": "Copy Features", "cmd.timeline_dependents": "Feature Dependents", @@ -326,6 +339,7 @@ "ui.combine": "Combine", "ui.coming_later": "coming later", "ui.concentric": "Concentric", + "ui.configurations": "Configurations", "ui.conic_curve": "Conic Curve", "ui.constant": "Constant", "ui.constraints_conflict": "Constraints conflict", @@ -376,6 +390,7 @@ "ui.extent_type": "Extent Type", "ui.extrude": "Extrude", "ui.factor": "Factor", + "ui.feature_history": "Feature history", "ui.feature_options": "Feature Options", "ui.features": "Features", "ui.file": "File", @@ -556,7 +571,6 @@ "ui.shell": "Shell", "ui.show": "Show", "ui.show_constraints": "Constraints", - "ui.show_construction_geometries": "Construction Geometries", "ui.show_dimensions": "Dimensions", "ui.show_points": "Show Points", "ui.show_profile": "Show Profile", @@ -597,11 +611,13 @@ "ui.theme": "Theme", "ui.thickness": "Thickness", "ui.thread": "Thread", + "ui.timeline_bottom": "Bottom timeline", + "ui.timeline_left_note": "The left layout shows the rollback bar above the parts and scene browser.", + "ui.timeline_left_sidebar": "Left sidebar", "ui.tip_angle": "Tip Angle", "ui.to_object": "To Object", "ui.top": "Top", "ui.torus": "Torus", - "ui.total_angle": "Total angle", "ui.trackpad_pan_tip": "Trackpad gestures pan (off: they zoom like the wheel); mouse wheel zooms, thumb wheel pans sideways, pinch always zooms", "ui.trim": "Trim", "ui.two_finger_scroll_pans": "Two-finger scroll pans", @@ -862,6 +878,62 @@ "Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).", "cmd.render_backends": "Render Backends", "cmd.view_navigate": "Navigate View", + "ui.animateJoint": "Animate Joint", + "ui.calibrate": "Calibrate", + "ui.customizeToolbar": "Customize Toolbar…", + "ui.editJointLimits": "Edit Joint Limits", + "ui.exportDxf": "Export DXF…", + "ui.findInTimeline": "Find in Timeline", + "ui.findInWindow": "Find in Window", + "ui.fit": "Fit", + "ui.groupSelected": "Group Selected", + "ui.hideAxes": "Hide Axes", + "ui.hideDimension": "Hide Dimension", + "ui.hideOrigin": "Hide Origin", + "ui.hidePlanes": "Hide Planes", + "ui.hideProfile": "Hide Profile", + "ui.homeView": "Home View", + "ui.isolate": "Isolate", + "ui.lock": "Lock", + "ui.newSketchGroup": "New Sketch Group", + "ui.normalConstruction": "Normal / Construction", + "ui.orbit": "Orbit", + "ui.heading_position": "POSITION", + "ui.pan": "Pan", + "ui.pinToShortcuts": "Pin to Shortcuts", + "ui.pinToToolbar": "Pin to Toolbar", + "ui.properties": "Properties", + "ui.removeSection": "Remove Section", + "ui.removeFromToolbar": "Remove from Toolbar", + "ui.repeat": "Repeat", + "ui.report_a_bug": "Report a bug", + "ui.restore": "Restore", + "ui.heading_setup": "SETUP", + "ui.selectOther": "Select Other…", + "ui.selectTangentChain": "Select Tangent Chain", + "ui.showAll": "Show All", + "ui.showAllBodies": "Show All Bodies", + "ui.showAxes": "Show Axes", + "ui.showDimension": "Show Dimension", + "ui.showOrigin": "Show Origin", + "ui.showPlanes": "Show Planes", + "ui.suppress": "Suppress", + "ui.command_palette_hint": "Type a command, e.g. {example} — or press S to search", + "ui.unground": "Unground", + "ui.unisolate": "Unisolate", + "ui.unlock": "Unlock", + "ui.unsuppress": "Unsuppress", + "ui.updateToCurrentView": "Update to Current View", + "ui.zoom": "Zoom", + "ui.command_done": "{command} done", + "timeline.group": "New Feature Folder", + "timeline.ungroup": "Remove Feature Folders", + "timeline.rename_group": "Rename Feature Folder", + "timeline.move_to_group": "Move Features to Folder", + "timeline.move_many": "Move Timeline Items", + "timeline.reorder_many": "Reorder Features", + "ui.show_construction_geometries": "Construction Geometries", + "ui.total_angle": "Total angle", "sketch_text.builtin_font": "{family} (built-in)", "sketch_text.font_search": "Search fonts", "sketch_text.loading_fonts": "Looking for installed fonts…", @@ -879,4 +951,4 @@ "Line Spacing": "Line Spacing", "Path": "Path", "sketch_text.on_path": "The text follows its path: the curve places it" -} \ No newline at end of file +} diff --git a/crates/ui-egui/src/locales/ja.json b/crates/ui-egui/src/locales/ja.json index 4e4938ec..7986b9d4 100644 --- a/crates/ui-egui/src/locales/ja.json +++ b/crates/ui-egui/src/locales/ja.json @@ -245,6 +245,19 @@ "cmd.surface_patch": "パッチ", "cmd.surface_stitch": "ステッチ", "cmd.surface_thicken": "厚み付け", + "cmd.surface_extrude": "サーフェス押し出し", + "cmd.surface_revolve": "サーフェス回転", + "cmd.surface_sweep": "サーフェススイープ", + "cmd.surface_loft": "サーフェスロフト", + "cmd.surface_ruled": "ルールドサーフェス", + "cmd.surface_boundary": "境界サーフェス", + "cmd.surface_offset": "オフセットサーフェス", + "cmd.surface_blend": "ブレンドサーフェス", + "cmd.surface_fillet": "サーフェスフィレット", + "cmd.surface_untrim": "トリム解除", + "cmd.surface_unstitch": "縫い合わせ解除", + "cmd.surface_reverse_normal": "法線を反転", + "cmd.inspect_continuity": "連続性解析", "cmd.timeline_compute_all": "すべて再計算", "cmd.timeline_copy": "フィーチャーをコピー", "cmd.timeline_dependents": "従属フィーチャー", @@ -326,6 +339,7 @@ "ui.combine": "結合", "ui.coming_later": "準備中", "ui.concentric": "同心", + "ui.configurations": "構成", "ui.conic_curve": "円錐曲線", "ui.constant": "一定", "ui.constraints_conflict": "拘束が競合しています", @@ -376,6 +390,7 @@ "ui.extent_type": "範囲タイプ", "ui.extrude": "押し出し", "ui.factor": "倍率", + "ui.feature_history": "フィーチャー履歴", "ui.feature_options": "フィーチャーオプション", "ui.features": "フィーチャー", "ui.file": "ファイル", @@ -556,7 +571,6 @@ "ui.shell": "シェル", "ui.show": "表示", "ui.show_constraints": "拘束", - "ui.show_construction_geometries": "構築ジオメトリ", "ui.show_dimensions": "寸法", "ui.show_points": "点を表示", "ui.show_profile": "プロファイルを表示", @@ -597,11 +611,13 @@ "ui.theme": "テーマ", "ui.thickness": "厚さ", "ui.thread": "ねじ", + "ui.timeline_bottom": "下部タイムライン", + "ui.timeline_left_note": "左レイアウトでは、ロールバックバーがパーツとシーンブラウザーの上に表示されます。", + "ui.timeline_left_sidebar": "左サイドバー", "ui.tip_angle": "先端角度", "ui.to_object": "オブジェクトまで", "ui.top": "上面", "ui.torus": "トーラス", - "ui.total_angle": "合計角度", "ui.trackpad_pan_tip": "トラックパッドのジェスチャーでパンします(オフの場合はマウスホイールと同様にズームします)。マウスホイールはズーム、サイドホイールは横方向のパン、ピンチ操作は常にズームになります。", "ui.trim": "トリム", "ui.two_finger_scroll_pans": "2本指スクロールでパン", @@ -862,6 +878,62 @@ "Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "その他を選択: モデル上で左ボタンを長押し(または右クリック › その他を選択…)するとカーソル下の要素が一覧表示されます。同じ場所を再度クリックすると次の要素が選択されます。中ボタンをダブルクリックするとホームビューに戻ります(環境設定 › ナビゲーション)。", "cmd.render_backends": "レンダリング バックエンド", "cmd.view_navigate": "ビューナビゲーション", + "ui.animateJoint": "ジョイントをアニメート", + "ui.calibrate": "校正", + "ui.customizeToolbar": "ツールバーをカスタマイズ…", + "ui.editJointLimits": "ジョイント制限を編集", + "ui.exportDxf": "DXF をエクスポート…", + "ui.findInTimeline": "タイムラインで検索", + "ui.findInWindow": "ウィンドウで検索", + "ui.fit": "フィット", + "ui.groupSelected": "選択をグループ化", + "ui.hideAxes": "軸を非表示", + "ui.hideDimension": "寸法を非表示", + "ui.hideOrigin": "原点を非表示", + "ui.hidePlanes": "平面を非表示", + "ui.hideProfile": "プロファイルを非表示", + "ui.homeView": "ホームビュー", + "ui.isolate": "分離表示", + "ui.lock": "ロック", + "ui.newSketchGroup": "新しいスケッチグループ", + "ui.normalConstruction": "通常 / コンストラクション", + "ui.orbit": "回転", + "ui.heading_position": "配置", + "ui.pan": "パン", + "ui.pinToShortcuts": "ショートカットに固定", + "ui.pinToToolbar": "ツールバーに固定", + "ui.properties": "プロパティ", + "ui.removeSection": "セクションを解除", + "ui.removeFromToolbar": "ツールバーから削除", + "ui.repeat": "繰り返し", + "ui.report_a_bug": "不具合を報告", + "ui.restore": "復元", + "ui.heading_setup": "セットアップ", + "ui.selectOther": "その他を選択…", + "ui.selectTangentChain": "接線チェーンを選択", + "ui.showAll": "すべて表示", + "ui.showAllBodies": "すべてのボディを表示", + "ui.showAxes": "軸を表示", + "ui.showDimension": "寸法を表示", + "ui.showOrigin": "原点を表示", + "ui.showPlanes": "平面を表示", + "ui.suppress": "抑制", + "ui.command_palette_hint": "コマンドを入力(例: {example})— または S キーで検索", + "ui.unground": "固定を解除", + "ui.unisolate": "分離表示を解除", + "ui.unlock": "ロック解除", + "ui.unsuppress": "抑制を解除", + "ui.updateToCurrentView": "現在のビューに更新", + "ui.zoom": "ズーム", + "ui.command_done": "{command} 完了", + "timeline.group": "フィーチャーフォルダを新規作成", + "timeline.ungroup": "フィーチャーフォルダを削除", + "timeline.rename_group": "フィーチャーフォルダの名前を変更", + "timeline.move_to_group": "フィーチャーをフォルダへ移動", + "timeline.move_many": "タイムライン項目を移動", + "timeline.reorder_many": "フィーチャーの順序を変更", + "ui.show_construction_geometries": "構築ジオメトリ", + "ui.total_angle": "合計角度", "sketch_text.builtin_font": "{family}(内蔵)", "sketch_text.font_search": "フォントを検索", "sketch_text.loading_fonts": "インストール済みフォントを検索中…", @@ -879,4 +951,4 @@ "Line Spacing": "行間", "Path": "パス", "sketch_text.on_path": "文字はパスに沿います: 位置は曲線で決まります" -} \ No newline at end of file +} diff --git a/crates/ui-egui/src/locales/ko.json b/crates/ui-egui/src/locales/ko.json index 21326205..a672300a 100644 --- a/crates/ui-egui/src/locales/ko.json +++ b/crates/ui-egui/src/locales/ko.json @@ -65,14 +65,15 @@ "ui.combine": "결합", "ui.coming_later": "추후 지원 예정", "ui.concentric": "동심", + "ui.configurations": "구성", "ui.conic_curve": "원뿔 곡선", "ui.constant": "상수", "ui.constraints_conflict": "구속 조건 충돌", "ui.construct": "구성", "ui.construction": "구성선", "ui.copy": "복사", - "ui.corner": "코너", "ui.corners": "코너", + "ui.corner": "코너", "ui.counterbore": "카운터보어", "ui.countersink": "카운터싱크", "ui.create": "만들기", @@ -112,6 +113,7 @@ "ui.extent_type": "범위 유형", "ui.extrude": "돌출", "ui.factor": "배율", + "ui.feature_history": "피처 기록", "ui.feature_options": "피처 옵션", "ui.features": "피처", "ui.file": "파일", @@ -249,8 +251,8 @@ "ui.profile_plane": "프로파일 평면", "ui.profiles": "프로파일", "ui.project": "투영", - "ui.quantity": "수량", "ui.quantity_2": "수량 2", + "ui.quantity": "수량", "ui.quit": "종료", "ui.radius": "반지름", "ui.radius_type": "반지름 유형", @@ -273,7 +275,6 @@ "ui.shell": "쉘", "ui.show": "표시", "ui.show_constraints": "구속 조건", - "ui.show_construction_geometries": "구성 형상", "ui.show_dimensions": "치수", "ui.show_points": "점 표시", "ui.show_profile": "프로파일 표시", @@ -291,8 +292,8 @@ "ui.slot": "슬롯", "ui.smooth": "부드럽게", "ui.snap": "스냅", - "ui.spacing": "간각", - "ui.spacing_2": "간각 2", + "ui.spacing_2": "간격 2", + "ui.spacing": "간격", "ui.sphere": "구", "ui.spline": "스플라인", "ui.split_body": "바디 분할", @@ -309,11 +310,13 @@ "ui.theme": "테마", "ui.thickness": "두께", "ui.thread": "나사산", + "ui.timeline_bottom": "하단 타임라인", + "ui.timeline_left_note": "왼쪽 레이아웃에서는 롤백 바가 부품 및 장면 브라우저 위에 표시됩니다.", + "ui.timeline_left_sidebar": "왼쪽 사이드바", "ui.tip_angle": "선단 각도", "ui.to_object": "객체까지", "ui.top": "상면", "ui.torus": "토러스", - "ui.total_angle": "전진 각도", "ui.trim": "트림", "ui.two_sides": "양방향", "ui.ui_scale": "UI 배율", @@ -325,9 +328,359 @@ "ui.view_centre": "뷰 중심", "ui.visual_style": "시각 스타일", "ui.workspace": "작업 공간", - "ui.x_distance": "X 거리", "ui.y_distance": "Y 거리", + "ui.x_distance": "X 거리", "ui.zoom_to_the_cursor": "커서 위치로 확대/축소", + "ui.animateJoint": "조인트 애니메이션", + "ui.calibrate": "캘리브레이션", + "ui.customizeToolbar": "도구 모음 사용자 지정…", + "ui.editJointLimits": "조인트 제한 편집", + "ui.exportDxf": "DXF 내보내기…", + "ui.findInTimeline": "타임라인에서 찾기", + "ui.findInWindow": "창에서 찾기", + "ui.fit": "맞춤", + "ui.groupSelected": "선택 항목 그룹화", + "ui.hideAxes": "축 숨기기", + "ui.hideDimension": "치수 숨기기", + "ui.hideOrigin": "원점 숨기기", + "ui.hidePlanes": "평면 숨기기", + "ui.hideProfile": "윤곽 숨기기", + "ui.homeView": "홈 보기", + "ui.isolate": "분리 표시", + "ui.lock": "잠금", + "ui.newSketchGroup": "새 스케치 그룹", + "ui.normalConstruction": "일반 / 생성", + "ui.orbit": "회전", + "ui.heading_position": "배치", + "ui.pan": "이동", + "ui.pinToShortcuts": "바로 가기에 고정", + "ui.pinToToolbar": "도구 모음에 고정", + "ui.properties": "속성", + "ui.removeSection": "단면 제거", + "ui.removeFromToolbar": "도구 모음에서 제거", + "ui.repeat": "반복", + "ui.report_a_bug": "버그 신고", + "ui.restore": "복원", + "ui.heading_setup": "설정", + "ui.selectOther": "기타 선택…", + "ui.selectTangentChain": "접선 체인 선택", + "ui.showAll": "모두 표시", + "ui.showAllBodies": "모두 표시", + "ui.showAxes": "축 표시", + "ui.showDimension": "치수 표시", + "ui.showOrigin": "원점 표시", + "ui.showPlanes": "평면 표시", + "ui.suppress": "비활성화", + "ui.command_palette_hint": "명령 입력(예: {example}) — 또는 S 키를 눌러 검색", + "ui.unground": "고정 해제", + "ui.unisolate": "분리 표시 해제", + "ui.unlock": "잠금 해제", + "ui.unsuppress": "비활성화 해제", + "ui.updateToCurrentView": "현재 보기로 업데이트", + "ui.zoom": "확대/축소", + "ui.command_done": "{command} 완료", + "cmd.appearance_assign": "외관", + "cmd.appearance_library": "외관 라이브러리", + "cmd.appearance_list": "외관 목록", + "cmd.body_rename": "바디 이름 변경", + "cmd.browser_group": "새 그룹", + "cmd.browser_move": "브라우저 항목 이동", + "cmd.browser_order": "브라우저 항목 정렬", + "cmd.browser_rename_group": "그룹 이름 변경", + "cmd.browser_ungroup": "그룹 해제", + "cmd.browser_visibility": "표시/숨기기", + "cmd.canvas_calibrate": "캔버스 보정", + "cmd.canvas_decal": "데칼", + "cmd.canvas_delete": "캔버스 삭제", + "cmd.canvas_edit": "캔버스 편집", + "cmd.canvas_insert": "캔버스", + "cmd.component_activate": "구성 요소 활성화", + "cmd.component_break_link": "링크 끊기", + "cmd.component_capture_position": "위치 캡처", + "cmd.component_create": "새 구성 요소", + "cmd.component_delete": "구성 요소 삭제", + "cmd.component_from_bodies": "바디로 구성 요소 만들기", + "cmd.component_insert_design": "설계 삽입", + "cmd.component_links": "링크 목록", + "cmd.component_list": "구성 요소 목록", + "cmd.component_move_bodies": "바디를 구성 요소로 이동", + "cmd.component_move_sketches": "스케치를 구성 요소로 이동", + "cmd.component_paste_new": "새로 붙여넣기", + "cmd.component_properties": "구성 요소 물리 속성", + "cmd.component_rename": "구성 요소 이름 변경", + "cmd.component_revert_position": "위치 복원", + "cmd.component_update_links": "링크 업데이트", + "cmd.config_activate": "구성 활성화", + "cmd.config_column": "구성 열", + "cmd.config_configure": "구성", + "cmd.config_preview": "구성 미리 보기", + "cmd.config_row": "구성 행", + "cmd.config_rule": "구성 규칙", + "cmd.config_rules": "구성 규칙", + "cmd.config_table": "구성 표시", + "cmd.construct_axis_edge": "모서리를 통과하는 축", + "cmd.construct_axis_normal_to_face": "면에 수직인 축", + "cmd.construct_axis_through_cylinder": "원기둥/원뿔/토러스를 통과하는 축", + "cmd.construct_axis_two_planes": "두 평면을 통과하는 축", + "cmd.construct_axis_two_points": "두 점을 통과하는 축", + "cmd.construct_plane_along_path": "경로를 따르는 평면", + "cmd.construct_plane_midplane": "중간 평면", + "cmd.construct_plane_perpendicular": "수직 평면", + "cmd.construct_plane_tangent": "접선 평면", + "cmd.construct_plane_three_points": "세 점을 통과하는 평면", + "cmd.construct_plane_two_edges": "두 모서리를 통과하는 평면", + "cmd.construct_point_along_path": "경로를 따르는 점", + "cmd.construct_point_center": "원/구/토러스 중심의 점", + "cmd.construct_point_edge_plane": "모서리와 평면의 점", + "cmd.construct_point_three_planes": "세 평면이 만나는 점", + "cmd.construct_point_two_edges": "두 모서리가 만나는 점", + "cmd.construct_point_vertex": "꼭짓점의 점", + "cmd.contact_check": "접촉 확인", + "cmd.contact_create": "새 접촉 집합", + "cmd.contact_disable_all": "접촉 비활성화", + "cmd.contact_edit": "접촉 집합 편집", + "cmd.contact_enable_all": "모든 접촉 활성화", + "cmd.contact_enable_sets": "접촉 집합 활성화", + "cmd.contact_list": "접촉 집합 목록", + "cmd.diagnostics_crash_report": "충돌 보고서", + "cmd.diagnostics_crash_reports": "충돌 보고서", + "cmd.diagnostics_crash_reports_clear": "충돌 보고서 삭제", + "cmd.diagnostics_crash_test": "충돌 테스트", + "cmd.doc_recover": "설계 복구", + "cmd.doc_recovery_discard": "복구된 설계 폐기", + "cmd.doc_recovery_list": "복구 가능한 설계", + "cmd.document_inspect": "설계 검사", + "cmd.document_units": "설계 단위 변경", + "cmd.edit_checkpoint": "체크포인트", + "cmd.edit_restore_checkpoint": "체크포인트 복원", + "cmd.engine_commands": "명령 목록", + "cmd.explode_create": "분해 보기", + "cmd.explode_delete": "분해 보기 삭제", + "cmd.explode_list": "분해 보기 목록", + "cmd.explode_show": "분해 보기 표시", + "cmd.expr_evaluate": "식 평가", + "cmd.face_delete": "면 삭제", + "cmd.file_export": "내보내기", + "cmd.file_insert_mesh": "메쉬 삽입", + "cmd.file_insert_step": "STEP 삽입", + "cmd.file_save_mesh": "메쉬로 저장", + "cmd.inspect_accessibility": "접근성 분석", + "cmd.inspect_center_of_mass": "무게중심", + "cmd.inspect_curvature_comb": "곡률빗 분석", + "cmd.inspect_curvature_map": "곡률맵 분석", + "cmd.inspect_draft": "블록각 분석", + "cmd.inspect_environment_map": "환경 맵 분석", + "cmd.inspect_interference": "간섭", + "cmd.inspect_isocurve": "등가곡선 분석", + "cmd.inspect_minimum_radius": "최소 반경 분석", + "cmd.inspect_zebra": "얼룩말줄무늬 분석", + "cmd.joint_as_built": "조립 상태 관절", + "cmd.joint_create": "관절", + "cmd.joint_delete": "관절 삭제", + "cmd.joint_drive": "관절 구동", + "cmd.joint_edit": "관절 편집", + "cmd.joint_limits": "관절 제한", + "cmd.joint_list": "관절 목록", + "cmd.joint_motion_link": "모션 링크", + "cmd.joint_origin": "관절 원점", + "cmd.joint_rigid_group": "강체 그룹", + "cmd.joint_solve": "관절 해석", + "cmd.material_assign": "물리 재질", + "cmd.material_create": "새 물리 재질", + "cmd.material_delete": "물리 재질 삭제", + "cmd.material_edit": "물리 재질 편집", + "cmd.material_list": "재질 목록", + "cmd.mesh_convert": "메쉬 변환", + "cmd.model_edges": "모서리 목록", + "cmd.model_faces": "면 목록", + "cmd.model_threads": "나사산 목록", + "cmd.motion_delete": "운동 연구 삭제", + "cmd.motion_export": "운동 연구 내보내기", + "cmd.motion_list": "운동 연구 목록", + "cmd.motion_play": "운동 연구 재생", + "cmd.motion_study": "운동 연구", + "cmd.occurrence_copy": "붙여넣기(인스턴스)", + "cmd.occurrence_ground": "고정", + "cmd.occurrence_move": "인스턴스 이동", + "cmd.occurrence_properties": "인스턴스 물리 속성", + "cmd.parameters_add": "사용자 매개 변수 추가", + "cmd.parameters_change": "매개 변수 변경", + "cmd.parameters_comment": "매개 변수에 설명 추가", + "cmd.parameters_complete": "식 완성", + "cmd.parameters_delete": "매개 변수 삭제", + "cmd.parameters_export": "매개 변수 내보내기", + "cmd.parameters_favorite": "즐겨찾는 매개 변수", + "cmd.parameters_graph": "매개 변수 그래프", + "cmd.parameters_import": "매개 변수 가져오기", + "cmd.parameters_list": "매개 변수 목록", + "cmd.parameters_rename": "매개 변수 이름 변경", + "cmd.parameters_users": "매개 변수 사용처", + "cmd.parts_fastener": "체결 부품 삽입", + "cmd.parts_insert": "부품 삽입", + "cmd.parts_library": "표준 부품", + "cmd.parts_resize": "부품 크기 변경", + "cmd.plastic_assign_rule": "플라스틱 규칙 지정", + "cmd.plastic_boss": "보스", + "cmd.plastic_lip": "리프", + "cmd.plastic_manage_rules": "플라스틱 규칙 관리", + "cmd.plastic_rest": "레스트", + "cmd.plastic_rules": "플라스틱 규칙 목록", + "cmd.plastic_snap_fit": "스냅핏", + "cmd.select_clear": "선택 지우기", + "cmd.sheet_convert": "판금으로 변환", + "cmd.sheet_export_bend_table": "벤딩 표 내보내기", + "cmd.sheet_export_dxf": "전개도를 DXF로 내보내기", + "cmd.sheet_flange": "플랜지", + "cmd.sheet_flat_pattern": "전개도 만들기", + "cmd.sheet_fold": "접기", + "cmd.sheet_hem": "헴", + "cmd.sheet_manage_rules": "판금 규칙", + "cmd.sheet_refold": "다시 접기", + "cmd.sheet_rules": "판금 규칙 목록", + "cmd.sheet_unfold": "펼치기", + "cmd.sketch_arc_center_point": "중심점 호", + "cmd.sketch_arc_tangent": "접선 호", + "cmd.sketch_arc_three_point": "3점 호", + "cmd.sketch_auto_constrain": "자동 구속", + "cmd.sketch_auto_constrain_from_datum": "기준으로 자동 구속", + "cmd.sketch_auto_project": "참조에 모서리 자동 투영", + "cmd.sketch_blend_curve": "블렌드 곡선", + "cmd.sketch_chain": "연속", + "cmd.sketch_chamfer_distance_angle": "거리와 각도 모따기", + "cmd.sketch_chamfer_equal_distance": "등거리 모따기", + "cmd.sketch_chamfer_two_distance": "두 거리 모따기", + "cmd.sketch_circle_center": "중심 지름 원", + "cmd.sketch_circle_three_point": "3점 원", + "cmd.sketch_circle_three_tangent": "3접선 원", + "cmd.sketch_circle_two_point": "2점 원", + "cmd.sketch_circle_two_tangent": "2접선 원", + "cmd.sketch_constrainer": "스케치 구속기", + "cmd.sketch_constraint_fix": "고정/고정 해제", + "cmd.sketch_constraint_midpoint": "중간점", + "cmd.sketch_construction": "일반/구성선", + "cmd.sketch_delete": "스케치 개체 삭제", + "cmd.sketch_dimension_text": "치수 텍스트 이동", + "cmd.sketch_drag": "스케치 기하 드래그", + "cmd.sketch_drag_end": "스케치 드래그 종료", + "cmd.sketch_draw_dimensioned": "치수가 있는 스케치 개체 그리기", + "cmd.sketch_edit": "스케치 편집", + "cmd.sketch_edit_text": "텍스트 편집", + "cmd.sketch_export_dxf": "DXF로 저장", + "cmd.sketch_finish_auto_constrain": "자동 구속으로 완료", + "cmd.sketch_fit_curves_to_section": "메시 단면에 곡선 맞추기", + "cmd.sketch_glyphs": "구속 기호", + "cmd.sketch_include_3d": "3D 기하 포함", + "cmd.sketch_insert_dxf": "DXF 삽입", + "cmd.sketch_insert_svg": "SVG 삽입", + "cmd.sketch_inspect": "스케치 검사", + "cmd.sketch_intersection_curve": "교차 곡선", + "cmd.sketch_isoparametric_curve": "등매개 곡선", + "cmd.sketch_line3d": "3D 선", + "cmd.sketch_line_midpoint": "중점 선", + "cmd.sketch_move": "스케치 개체 이동/복사", + "cmd.sketch_move3d": "3D 점 이동", + "cmd.sketch_move_point": "스케치 점 드래그", + "cmd.sketch_offset_break": "오프셋 연결 끊기", + "cmd.sketch_options": "스케치 팔레트 옵션", + "cmd.sketch_point3d": "3D 점", + "cmd.sketch_polygon_circumscribed": "외접 다각형", + "cmd.sketch_polygon_edge": "변 다각형", + "cmd.sketch_polygon_inscribed": "내접 다각형", + "cmd.sketch_project_to_surface": "곡면으로 투영", + "cmd.sketch_rectangle_center": "중심 사각형", + "cmd.sketch_rectangle_three_point": "3점 사각형", + "cmd.sketch_rectangle_two_point": "2점 사각형", + "cmd.sketch_redefine": "스케치 평면 재정의", + "cmd.sketch_slot_arc_center": "중심점 호 슬롯", + "cmd.sketch_slot_arc_three_point": "3점 호 슬롯", + "cmd.sketch_slot_center_point": "중심점 슬롯", + "cmd.sketch_slot_center_to_center": "중심에서 중심 슬롯", + "cmd.sketch_slot_overall": "전장 슬롯", + "cmd.sketch_snap": "스냅 추론", + "cmd.sketch_solve": "스케치 해석", + "cmd.sketch_spline3d": "3D 스플라인", + "cmd.sketch_spline_control_point": "제어점 스플라인", + "cmd.sketch_spline_control_point_5": "제어점 스플라인(5차)", + "cmd.sketch_spline_fit_point": "맞춤점 스플라인", + "cmd.sketch_spun_profile": "회전 프로파일", + "cmd.sketch_toggle_driven": "종속 치수 전환", + "cmd.solid_align": "정렬", + "cmd.solid_boundary_fill": "경계 채우기", + "cmd.solid_bounding_solid": "둘러싸는 솔리드", + "cmd.solid_emboss": "엠보스", + "cmd.solid_move": "이동/복사", + "cmd.solid_offset_face": "면 오프셋", + "cmd.solid_press_pull": "밀기/당기기", + "cmd.solid_remove": "제거", + "cmd.solid_replace_face": "면 교체", + "cmd.solid_rib": "리브", + "cmd.solid_split_face": "면 분할", + "cmd.solid_web": "웹", + "cmd.surface_patch": "패치", + "cmd.surface_stitch": "재봉", + "cmd.surface_thicken": "두께 주기", + "cmd.surface_extrude": "서피스 돌출", + "cmd.surface_revolve": "서피스 회전", + "cmd.surface_sweep": "서피스 스윕", + "cmd.surface_loft": "서피스 로프트", + "cmd.surface_ruled": "룰드 서피스", + "cmd.surface_boundary": "바운더리 서피스", + "cmd.surface_offset": "오프셋 서피스", + "cmd.surface_blend": "블렌드 서피스", + "cmd.surface_fillet": "서피스 필렛", + "cmd.surface_untrim": "트림 해제", + "cmd.surface_unstitch": "스티치 해제", + "cmd.surface_reverse_normal": "법선 반전", + "cmd.inspect_continuity": "연속성 분석", + "cmd.timeline_compute_all": "모두 다시 계산", + "cmd.timeline_copy": "특성 복사", + "cmd.timeline_dependents": "특성 종속 항목", + "cmd.timeline_inspect": "특성 검사", + "cmd.timeline_paste": "특성 붙여넣기", + "cmd.timeline_redefine": "특성 편집", + "cmd.timeline_rename": "이름 변경", + "cmd.timeline_reorder": "특성 순서 변경", + "cmd.timeline_roll_to": "이력 마커 이동", + "cmd.timeline_rollback": "타임라인 마커 이동", + "cmd.timeline_suppress": "특성 비활성화", + "cmd.view_apply": "이름 있는 보기 복원", + "cmd.view_delete": "이름 있는 보기 삭제", + "cmd.view_list": "이름 있는 보기 목록", + "cmd.view_rename": "이름 있는 보기 이름 변경", + "cmd.view_save": "새 이름 있는 보기", + "ui.export_ellipsis_keymap": "내보내기…", + "ui.heading_configure": "구성", + "ui.heading_constraints": "구속 조건", + "ui.heading_utilities": "유틸리티", + "BROWSER": "브라우저", + "Drive Joint": "조인트 구동", + "Edit Profile Sketch": "윤곽 스케치 편집", + "Find in Browser": "브라우저에서 찾기", + "Report a bug — review a screenshot and open a GitHub draft": "버그 신고 — 스크린샷을 검토하고 GitHub 초안 열기", + "Reset Toolbar": "도구 모음 재설정", + "Select bodies (a face picks its body)": "바디 선택(면을 클릭하면 해당 바디 선택)", + "Selection filter": "선택 필터", + "Revert Position": "위치 복원", + "Construction": "구성선", + "Faces": "면", + "Edges": "모서리", + "Vertices": "꼭짓점", + "Sketch geometry and profiles": "스케치 기하 및 프로파일", + "Sketch curves": "스케치 곡선", + "Sketch points": "스케치 점", + "Sketch dimensions": "스케치 치수", + "Sketch constraints": "스케치 구속 조건", + "Flat": "플랫", + "cmd.render_backends": "렌더링 백엔드", + "cmd.view_navigate": "보기 탐색", + "timeline.group": "새 특성 폴더", + "timeline.ungroup": "특성 폴더 제거", + "timeline.rename_group": "특성 폴더 이름 변경", + "timeline.move_to_group": "특성을 폴더로 이동", + "timeline.move_many": "타임라인 항목 이동", + "timeline.reorder_many": "특성 순서 변경", + "ui.show_construction_geometries": "구성 형상", + "ui.total_angle": "전진 각도", "sketch_text.builtin_font": "{family}(내장)", "sketch_text.font_search": "글꼴 검색", "sketch_text.loading_fonts": "설치된 글꼴을 찾는 중…", diff --git a/crates/ui-egui/src/locales/zh.json b/crates/ui-egui/src/locales/zh.json new file mode 100644 index 00000000..e6353c54 --- /dev/null +++ b/crates/ui-egui/src/locales/zh.json @@ -0,0 +1,954 @@ +{ + "browser.body_name": "实体{number}", + "browser.component_name": "组件{number}", + "browser.parameters": "参数 ({count})", + "browser.precision": "精度: {precision}", + "browser.sketch_name": "草图{number}", + "browser.units": "单位: {units}", + "cmd.appearance_assign": "外观", + "cmd.appearance_library": "外观库", + "cmd.appearance_list": "列出外观", + "cmd.body_rename": "重命名实体", + "cmd.browser_group": "新建组", + "cmd.browser_move": "移动浏览器项目", + "cmd.browser_order": "排列浏览器项目", + "cmd.browser_rename_group": "重命名组", + "cmd.browser_ungroup": "取消编组", + "cmd.browser_visibility": "显示/隐藏", + "cmd.canvas_calibrate": "校准画布", + "cmd.canvas_decal": "贴花", + "cmd.canvas_delete": "删除画布", + "cmd.canvas_edit": "编辑画布", + "cmd.canvas_insert": "画布", + "cmd.component_activate": "激活组件", + "cmd.component_break_link": "断开链接", + "cmd.component_capture_position": "捕获位置", + "cmd.component_create": "新建组件", + "cmd.component_delete": "删除组件", + "cmd.component_from_bodies": "由实体创建组件", + "cmd.component_insert_design": "插入设计", + "cmd.component_links": "列出链接", + "cmd.component_list": "列出组件", + "cmd.component_move_bodies": "将实体移至组件", + "cmd.component_move_sketches": "将草图移至组件", + "cmd.component_paste_new": "粘贴为新对象", + "cmd.component_properties": "组件物理属性", + "cmd.component_rename": "重命名组件", + "cmd.component_revert_position": "恢复位置", + "cmd.component_update_links": "更新链接", + "cmd.config_activate": "激活配置", + "cmd.config_column": "配置列", + "cmd.config_configure": "配置", + "cmd.config_preview": "预览配置", + "cmd.config_row": "配置行", + "cmd.config_rule": "配置规则", + "cmd.config_rules": "配置规则", + "cmd.config_table": "显示配置表", + "cmd.construct_axis_edge": "过边的轴", + "cmd.construct_axis_normal_to_face": "垂直于面的轴", + "cmd.construct_axis_through_cylinder": "过圆柱/圆锥/圆环的轴", + "cmd.construct_axis_two_planes": "过两个平面的轴", + "cmd.construct_axis_two_points": "过两点的轴", + "cmd.construct_plane_along_path": "沿路径的平面", + "cmd.construct_plane_midplane": "中平面", + "cmd.construct_plane_perpendicular": "垂直平面", + "cmd.construct_plane_tangent": "相切平面", + "cmd.construct_plane_three_points": "过三点的平面", + "cmd.construct_plane_two_edges": "过两条边的平面", + "cmd.construct_point_along_path": "沿路径的点", + "cmd.construct_point_center": "圆/球/圆环中心的点", + "cmd.construct_point_edge_plane": "边与平面交点", + "cmd.construct_point_three_planes": "过三个平面的点", + "cmd.construct_point_two_edges": "过两条边的点", + "cmd.construct_point_vertex": "顶点处的点", + "cmd.contact_check": "检查接触", + "cmd.contact_create": "新建接触集", + "cmd.contact_disable_all": "禁用接触", + "cmd.contact_edit": "编辑接触集", + "cmd.contact_enable_all": "启用全部接触", + "cmd.contact_enable_sets": "启用接触集", + "cmd.contact_list": "列出接触集", + "cmd.diagnostics_crash_report": "崩溃报告", + "cmd.diagnostics_crash_reports": "崩溃报告", + "cmd.diagnostics_crash_reports_clear": "删除崩溃报告", + "cmd.diagnostics_crash_test": "崩溃测试", + "cmd.doc_recover": "恢复设计", + "cmd.doc_recovery_discard": "丢弃已恢复的设计", + "cmd.doc_recovery_list": "可恢复的设计", + "cmd.document_inspect": "检查设计", + "cmd.document_units": "更改设计单位", + "cmd.edit_checkpoint": "检查点", + "cmd.edit_restore_checkpoint": "恢复检查点", + "cmd.engine_commands": "列出命令", + "cmd.explode_create": "爆炸图", + "cmd.explode_delete": "删除爆炸图", + "cmd.explode_list": "列出爆炸图", + "cmd.explode_show": "显示爆炸图", + "cmd.expr_evaluate": "计算表达式", + "cmd.face_delete": "删除面", + "cmd.file_export": "导出", + "cmd.file_insert_mesh": "插入网格", + "cmd.file_insert_step": "插入 STEP", + "cmd.file_save_mesh": "另存为网格", + "cmd.inspect_accessibility": "可达性分析", + "cmd.inspect_center_of_mass": "质心", + "cmd.inspect_curvature_comb": "曲率梳分析", + "cmd.inspect_curvature_map": "曲率图分析", + "cmd.inspect_draft": "拔模分析", + "cmd.inspect_environment_map": "环境贴图分析", + "cmd.inspect_interference": "干涉", + "cmd.inspect_isocurve": "等参线分析", + "cmd.inspect_minimum_radius": "最小半径分析", + "cmd.inspect_zebra": "斑马纹分析", + "cmd.joint_as_built": "现状接头", + "cmd.joint_create": "接头", + "cmd.joint_delete": "删除接头", + "cmd.joint_drive": "驱动接头", + "cmd.joint_edit": "编辑接头", + "cmd.joint_limits": "接头限制", + "cmd.joint_list": "列出接头", + "cmd.joint_motion_link": "运动关联", + "cmd.joint_origin": "接头原点", + "cmd.joint_rigid_group": "刚性组", + "cmd.joint_solve": "解算接头", + "cmd.material_assign": "物理材质", + "cmd.material_create": "新建物理材质", + "cmd.material_delete": "删除物理材质", + "cmd.material_edit": "编辑物理材质", + "cmd.material_list": "列出材质", + "cmd.mesh_convert": "转换网格", + "cmd.model_edges": "列出边", + "cmd.model_faces": "列出面", + "cmd.model_threads": "列出螺纹", + "cmd.motion_delete": "删除运动研究", + "cmd.motion_export": "导出运动研究", + "cmd.motion_list": "列出运动研究", + "cmd.motion_play": "播放运动研究", + "cmd.motion_study": "运动研究", + "cmd.occurrence_copy": "粘贴(实例)", + "cmd.occurrence_ground": "固定", + "cmd.occurrence_move": "移动实例", + "cmd.occurrence_properties": "实例物理属性", + "cmd.parameters_add": "添加用户参数", + "cmd.parameters_change": "更改参数", + "cmd.parameters_comment": "注释参数", + "cmd.parameters_complete": "补全表达式", + "cmd.parameters_delete": "删除参数", + "cmd.parameters_export": "导出参数", + "cmd.parameters_favorite": "收藏参数", + "cmd.parameters_graph": "参数关系图", + "cmd.parameters_import": "导入参数", + "cmd.parameters_list": "列出参数", + "cmd.parameters_rename": "重命名参数", + "cmd.parameters_users": "参数使用者", + "cmd.parts_fastener": "插入紧固件", + "cmd.parts_insert": "插入零件", + "cmd.parts_library": "标准零件", + "cmd.parts_resize": "更改零件尺寸", + "cmd.plastic_assign_rule": "指定塑料规则", + "cmd.plastic_boss": "凸台", + "cmd.plastic_lip": "凸缘", + "cmd.plastic_manage_rules": "管理塑料规则", + "cmd.plastic_rest": "垫块", + "cmd.plastic_rules": "列出塑料规则", + "cmd.plastic_snap_fit": "卡扣", + "cmd.select_clear": "清除选择", + "cmd.sheet_convert": "转换为钣金", + "cmd.sheet_export_bend_table": "导出折弯表", + "cmd.sheet_export_dxf": "将展开图导出为 DXF", + "cmd.sheet_flange": "法兰", + "cmd.sheet_flat_pattern": "创建展开图", + "cmd.sheet_fold": "折叠", + "cmd.sheet_hem": "卷边", + "cmd.sheet_manage_rules": "钣金规则", + "cmd.sheet_refold": "重新折弯", + "cmd.sheet_rules": "列出钣金规则", + "cmd.sheet_unfold": "展开", + "cmd.sketch_arc_center_point": "中心点圆弧", + "cmd.sketch_arc_tangent": "相切圆弧", + "cmd.sketch_arc_three_point": "三点圆弧", + "cmd.sketch_auto_constrain": "自动约束", + "cmd.sketch_auto_constrain_from_datum": "基于基准自动约束", + "cmd.sketch_auto_project": "自动投影边到参考", + "cmd.sketch_blend_curve": "混合曲线", + "cmd.sketch_chain": "连续链", + "cmd.sketch_chamfer_distance_angle": "距离和角度倒角", + "cmd.sketch_chamfer_equal_distance": "等距倒角", + "cmd.sketch_chamfer_two_distance": "双距离倒角", + "cmd.sketch_circle_center": "圆心直径圆", + "cmd.sketch_circle_three_point": "三点圆", + "cmd.sketch_circle_three_tangent": "三切线圆", + "cmd.sketch_circle_two_point": "两点圆", + "cmd.sketch_circle_two_tangent": "两切线圆", + "cmd.sketch_constrainer": "草图约束器", + "cmd.sketch_constraint_fix": "固定/解除固定", + "cmd.sketch_constraint_midpoint": "中点", + "cmd.sketch_construction": "普通/构造", + "cmd.sketch_delete": "删除草图图元", + "cmd.sketch_dimension_text": "移动尺寸文字", + "cmd.sketch_drag": "拖动草图几何", + "cmd.sketch_drag_end": "结束草图拖动", + "cmd.sketch_draw_dimensioned": "绘制带尺寸的草图图元", + "cmd.sketch_edit": "编辑草图", + "cmd.sketch_edit_text": "编辑文字", + "cmd.sketch_export_dxf": "另存为 DXF", + "cmd.sketch_finish_auto_constrain": "使用自动约束完成", + "cmd.sketch_fit_curves_to_section": "将曲线拟合到网格截面", + "cmd.sketch_glyphs": "约束符号", + "cmd.sketch_include_3d": "包含三维几何", + "cmd.sketch_insert_dxf": "插入 DXF", + "cmd.sketch_insert_svg": "插入 SVG", + "cmd.sketch_inspect": "检查草图", + "cmd.sketch_intersection_curve": "相交曲线", + "cmd.sketch_isoparametric_curve": "等参数曲线", + "cmd.sketch_line3d": "三维直线", + "cmd.sketch_line_midpoint": "中点线", + "cmd.sketch_move": "移动/复制草图图元", + "cmd.sketch_move3d": "移动三维点", + "cmd.sketch_move_point": "拖动草图点", + "cmd.sketch_offset_break": "断开偏移关联", + "cmd.sketch_options": "草图选项板选项", + "cmd.sketch_point3d": "三维点", + "cmd.sketch_polygon_circumscribed": "外切多边形", + "cmd.sketch_polygon_edge": "边长多边形", + "cmd.sketch_polygon_inscribed": "内接多边形", + "cmd.sketch_project_to_surface": "投影到曲面", + "cmd.sketch_rectangle_center": "中心矩形", + "cmd.sketch_rectangle_three_point": "三点矩形", + "cmd.sketch_rectangle_two_point": "两点矩形", + "cmd.sketch_redefine": "重定义草图平面", + "cmd.sketch_slot_arc_center": "中心点圆弧槽", + "cmd.sketch_slot_arc_three_point": "三点圆弧槽", + "cmd.sketch_slot_center_point": "中心点槽", + "cmd.sketch_slot_center_to_center": "中心距槽", + "cmd.sketch_slot_overall": "全长槽", + "cmd.sketch_snap": "捕捉推断", + "cmd.sketch_solve": "解算草图", + "cmd.sketch_spline3d": "三维样条曲线", + "cmd.sketch_spline_control_point": "控制点样条曲线", + "cmd.sketch_spline_control_point_5": "控制点样条曲线(5 次)", + "cmd.sketch_spline_fit_point": "拟合点样条曲线", + "cmd.sketch_spun_profile": "旋转轮廓", + "cmd.sketch_toggle_driven": "切换被驱动尺寸", + "cmd.solid_align": "对齐", + "cmd.solid_boundary_fill": "边界填充", + "cmd.solid_bounding_solid": "包围实体", + "cmd.solid_emboss": "压印", + "cmd.solid_move": "移动/复制", + "cmd.solid_offset_face": "偏移面", + "cmd.solid_press_pull": "按压拖动", + "cmd.solid_remove": "移除", + "cmd.solid_replace_face": "替换面", + "cmd.solid_rib": "筋板", + "cmd.solid_split_face": "拆分面", + "cmd.solid_web": "网格筋", + "cmd.surface_patch": "补面", + "cmd.surface_stitch": "缝合", + "cmd.surface_thicken": "加厚", + "cmd.surface_extrude": "曲面拉伸", + "cmd.surface_revolve": "曲面旋转", + "cmd.surface_sweep": "曲面扫掠", + "cmd.surface_loft": "曲面放样", + "cmd.surface_ruled": "直纹曲面", + "cmd.surface_boundary": "边界曲面", + "cmd.surface_offset": "偏移曲面", + "cmd.surface_blend": "混合曲面", + "cmd.surface_fillet": "曲面圆角", + "cmd.surface_untrim": "取消修剪", + "cmd.surface_unstitch": "取消缝合", + "cmd.surface_reverse_normal": "反转法向", + "cmd.inspect_continuity": "连续性分析", + "cmd.timeline_compute_all": "全部重新計算", + "cmd.timeline_copy": "复制特征", + "cmd.timeline_dependents": "特征依存项", + "cmd.timeline_inspect": "检查特征", + "cmd.timeline_paste": "粘贴特征", + "cmd.timeline_redefine": "编辑特征", + "cmd.timeline_rename": "重命名", + "cmd.timeline_reorder": "重排特征", + "cmd.timeline_roll_to": "滚动历史标记", + "cmd.timeline_rollback": "移动时间线标记", + "cmd.timeline_suppress": "抑制特征", + "cmd.view_apply": "还原命名视图", + "cmd.view_delete": "删除命名视图", + "cmd.view_list": "列出命名视图", + "cmd.view_rename": "重命名命名视图", + "cmd.view_save": "新建命名视图", + "dialog.custom_axis": "↖ 自定义轴", + "dialog.edit_title": "编辑 {title}", + "dialog.select": "↖ 选择", + "dialog.selected_count": "↖ 已选择 {count} 个", + "document.save_changes": "保存对 {name} 的更改?", + "document.unsaved_changes": "{name} 有未保存的更改。如果不保存,将丢失这些更改。", + "settings.msaa_applies_on_restart": "抗锯齿将在下次启动 SolidCraft 时生效", + "settings.msaa_reset": "{setting}: {requested}× 不受此图形设备支持;已重置为 {reset}× (支持: {supported})。", + "settings.msaa_unsupported": "此图形设备不支持", + "settings.reset_intro": "这些设置在此计算机上无法正常工作,会导致 SolidCraft 无法启动,因此已更改:", + "settings.reset_title": "设置已更改", + "sketch.degrees_of_freedom": "{count} 个自由度", + "sketch.draw_curves": "绘制为 {linetype}", + "sketch.make_selection": "将选择转换为 {linetype}", + "ui.1_degree_of_freedom": "1 个自由度", + "ui.3d_mouse_speed": "3D 鼠标速度", + "ui.3d_mouse_windows": "3D 鼠标(Windows)", + "ui.3d_sketch": "3D 草图", + "ui.about": "关于", + "ui.activate": "激活", + "ui.active_component": "活动组件", + "ui.agent_cursor": "代理光标", + "ui.agent_cursor_speed": "代理光标速度", + "ui.all": "全部", + "ui.angle": "角度", + "ui.angle_decimals": "角度小数位", + "ui.animate_view_changes": "视图变化动画", + "ui.animation": "动画", + "ui.animation_speed": "动画速度", + "ui.anti_aliasing": "抗锯齿", + "ui.apply": "应用", + "ui.arc": "圆弧", + "ui.assemble": "装配", + "ui.autosave_every": "自动保存间隔", + "ui.axes": "轴", + "ui.axis": "轴", + "ui.base_point": "基点", + "ui.bodies": "实体", + "ui.box": "长方体", + "ui.break": "打断", + "ui.cancel": "取消", + "ui.canvases": "画布", + "ui.center": "中心", + "ui.centerline": "中心线", + "ui.chain": "链", + "ui.chamfer": "倒角", + "ui.chamfer_type": "倒角类型", + "ui.change_workspace": "更改工作区", + "ui.chord_length": "弦长", + "ui.circle": "圆", + "ui.circular_pattern": "圆形阵列", + "ui.clear": "清除", + "ui.clear_recent": "清除最近", + "ui.clear_the_selection": "清除选择", + "ui.click_selects_whole_bodies": "单击选择整个实体", + "ui.click_then_press_key": "单击,然后按新按键", + "ui.close": "关闭", + "ui.coil": "螺旋", + "ui.coincident": "重合", + "ui.collapse": "折叠", + "ui.collinear": "共线", + "ui.combine": "组合", + "ui.coming_later": "即将推出", + "ui.concentric": "同心", + "ui.configurations": "配置", + "ui.conic_curve": "圆锥曲线", + "ui.constant": "恒定", + "ui.constraints_conflict": "约束冲突", + "ui.construct": "构造", + "ui.construction": "构造", + "ui.context_menu": "上下文菜单", + "ui.copy": "复制", + "ui.corners": "角点", + "ui.corner": "角", + "ui.counterbore": "沉头孔", + "ui.countersink": "锥形沉孔", + "ui.create": "创建", + "ui.create_copy": "创建副本", + "ui.create_sketch": "创建草图", + "ui.current": "当前", + "ui.curvature": "曲率", + "ui.curves": "曲线", + "ui.cut": "剪切", + "ui.cylinder": "圆柱体", + "ui.dark": "深色", + "ui.default_modelling_orientation": "默认建模方向", + "ui.degrees_of_freedom": "自由度", + "ui.delete": "删除", + "ui.depth": "深度", + "ui.design": "设计", + "ui.diameter": "直径", + "ui.dimension": "尺寸", + "ui.direction": "方向", + "ui.distance": "距离", + "ui.distance_1": "距离 1", + "ui.distance_2": "距离 2", + "ui.document_settings": "文档设置", + "ui.don_t_save": "不保存", + "ui.draft": "拔模", + "ui.drawing": "绘图", + "ui.drill_point": "钻尖", + "ui.edit": "编辑", + "ui.ellipse": "椭圆", + "ui.end_radius": "结束半径", + "ui.entities": "实体", + "ui.equal": "相等", + "ui.expand": "展开", + "ui.export_ellipsis": "导出...", + "ui.export_ellipsis_keymap": "导出…", + "ui.export_keymap_tip": "将按键、S 框收藏和工具栏导出为文件", + "ui.extend": "延伸", + "ui.extent_type": "延伸类型", + "ui.extrude": "拉伸", + "ui.factor": "系数", + "ui.feature_history": "特征历史", + "ui.feature_options": "特征选项", + "ui.features": "特征", + "ui.file": "文件", + "ui.fillet": "圆角", + "ui.filter_commands_or_keys": "筛选命令或按键", + "ui.finish_sketch": "完成草图", + "ui.fix_unfix": "固定/取消固定", + "ui.flat": "平面", + "ui.flip": "翻转", + "ui.front": "前视", + "ui.fully_constrained": "完全约束", + "ui.general": "常规", + "ui.graphics": "图形", + "ui.ground_shadow": "地面阴影", + "ui.heading_add_ins": "加载项", + "ui.heading_assemble": "装配", + "ui.heading_box": "长方体", + "ui.heading_chamfer": "倒角", + "ui.heading_circular_pattern": "圆形阵列", + "ui.heading_coil": "螺旋", + "ui.heading_combine": "组合", + "ui.heading_configure": "配置", + "ui.heading_constraints": "约束", + "ui.heading_construct": "构造", + "ui.heading_create": "创建", + "ui.heading_create_sketch": "创建草图", + "ui.heading_cylinder": "圆柱体", + "ui.heading_design": "设计", + "ui.heading_draft": "拔模", + "ui.heading_edit": "编辑", + "ui.heading_extrude": "拉伸", + "ui.heading_fillet": "圆角", + "ui.heading_finish_sketch": "完成草图", + "ui.heading_front": "前视", + "ui.heading_hole": "孔", + "ui.heading_home": "主页", + "ui.heading_insert": "插入", + "ui.heading_inspect": "检查", + "ui.heading_loft": "放样", + "ui.heading_make": "制作", + "ui.heading_measure": "测量", + "ui.heading_mesh": "网格", + "ui.heading_mirror": "镜像", + "ui.heading_modify": "修改", + "ui.heading_move": "移动", + "ui.heading_offset": "偏移", + "ui.heading_move_copy": "移动/复制", + "ui.heading_offset_plane": "偏移平面", + "ui.heading_ok": "确定", + "ui.heading_parameters": "参数", + "ui.heading_pattern_on_path": "路径阵列", + "ui.heading_pipe": "管道", + "ui.heading_plane_at_angle": "角度平面", + "ui.heading_plastic": "塑料", + "ui.heading_prepare": "准备", + "ui.heading_rectangular_pattern": "矩形阵列", + "ui.heading_revolve": "旋转", + "ui.heading_right": "右视", + "ui.heading_scale": "缩放", + "ui.heading_section_analysis": "截面分析", + "ui.heading_select": "选择", + "ui.heading_sheet_metal": "钣金", + "ui.heading_shell": "抽壳", + "ui.heading_sketch": "草图", + "ui.heading_sketch_palette": "草图面板", + "ui.heading_sketch_scale": "草图缩放", + "ui.heading_solid": "实体", + "ui.heading_sphere": "球体", + "ui.heading_split_body": "分割实体", + "ui.heading_surface": "曲面", + "ui.heading_sweep": "扫掠", + "ui.heading_thread": "螺纹", + "ui.heading_top": "上视", + "ui.heading_torus": "圆环体", + "ui.heading_utilities": "工具", + "ui.heading_utility": "实用工具", + "ui.help": "帮助", + "ui.hide": "隐藏", + "ui.hole": "孔", + "ui.hole_tap_type": "孔攻丝类型", + "ui.hole_type": "孔类型", + "ui.home": "主页", + "ui.horizontal_vertical": "水平/垂直", + "ui.import_ellipsis": "导入…", + "ui.insert": "插入", + "ui.inspect": "检查", + "ui.intersect": "相交", + "ui.join": "连接", + "ui.joints": "关节", + "ui.keyboard_shortcuts": "键盘快捷键", + "ui.language": "语言", + "ui.length_decimals": "长度小数位", + "ui.light": "浅色", + "ui.line": "直线", + "ui.linetype": "线型", + "ui.loft": "放样", + "ui.look_at": "朝向", + "ui.look_at_anim_desc": "轴视图、注视、主视图与适合窗口", + "ui.manufacture": "制造", + "ui.measure": "测量", + "ui.midpoint": "中点", + "ui.mirror": "镜像", + "ui.mirror_line": "镜像线", + "ui.modify": "修改", + "ui.move": "移动", + "ui.multiple": "多个", + "ui.named_views": "命名视图", + "ui.navigation": "导航", + "ui.needs_design": "{command}: 请先打开一个设计", + "ui.new_body": "新建实体", + "ui.new_design": "新建设计", + "ui.normal": "法向", + "ui.object_type": "对象类型", + "ui.objects": "对象", + "ui.offer_to_stand_up_y_up_files": "提供站立 Y-up 文件", + "ui.offset": "偏移", + "ui.offset_plane": "偏移平面", + "ui.one_side": "单侧", + "ui.open": "打开", + "ui.open_ellipsis": "打开...", + "ui.open_preferences": "打开首选项", + "ui.open_recent": "打开最近", + "ui.operation": "操作", + "ui.options": "选项", + "ui.orbit_around": "环绕", + "ui.origin": "原点", + "ui.palette_points": "点", + "ui.palette_profile": "轮廓", + "ui.pan_zoom_orbit": "平移、缩放、环绕", + "ui.parallel": "平行", + "ui.parameters": "参数", + "ui.paste": "粘贴", + "ui.path": "路径", + "ui.pattern_on_path": "路径阵列", + "ui.perpendicular": "垂直", + "ui.perspective_view": "透视图", + "ui.pipe": "管道", + "ui.placement": "放置", + "ui.plane": "平面", + "ui.plane_at_angle": "角度平面", + "ui.point": "点", + "ui.point_under_the_cursor": "光标下的点", + "ui.points": "点", + "ui.polygon": "多边形", + "ui.preferences": "首选项", + "ui.press_a_key": "按一个键…", + "ui.profile": "轮廓", + "ui.profile_plane": "轮廓平面", + "ui.profiles": "轮廓", + "ui.project": "投影", + "ui.project_face_edges_sketch": "将面的边投影到新草图", + "ui.quantity_2": "数量 2", + "ui.quantity": "数量", + "ui.quit": "退出", + "ui.radial_context_menu": "环形上下文菜单", + "ui.radius": "半径", + "ui.radius_type": "半径类型", + "ui.rectangle": "矩形", + "ui.rectangular_pattern": "矩形阵列", + "ui.redo": "重做", + "ui.render": "渲染", + "ui.renderer": "渲染器", + "ui.reset": "重置", + "ui.reset_all": "全部重置", + "ui.reset_the_settings_on_this_page": "重置此页面上的设置", + "ui.restore_defaults": "恢复默认值", + "ui.reverse_zoom_direction": "反转缩放方向", + "ui.revolve": "旋转", + "ui.right": "右视", + "ui.save": "保存", + "ui.save_as": "另存为", + "ui.save_as_ellipsis": "另存为...", + "ui.scale": "缩放", + "ui.section_analysis": "截面分析", + "ui.select": "选择", + "ui.select_other_hint": "“选择其他”:在模型上按住左键(或右键单击 › 选择其他…)列出光标下的所有对象;在同一位置再次单击可选择该处的下一个对象。", + "ui.selection": "选择", + "ui.shell": "抽壳", + "ui.show": "显示", + "ui.show_constraints": "约束", + "ui.show_dimensions": "尺寸", + "ui.show_points": "显示点", + "ui.show_profile": "显示轮廓", + "ui.show_projected_geometries": "投影几何", + "ui.simple": "简单", + "ui.simulation": "仿真", + "ui.single": "单个", + "ui.sketch": "草图", + "ui.sketch_dimension": "草图尺寸", + "ui.sketch_grid": "草图网格", + "ui.sketch_scale": "草图比例", + "ui.sketch_status": "草图状态", + "ui.sketches": "草图", + "ui.slice": "切片", + "ui.slot": "槽", + "ui.smooth": "平滑", + "ui.smooth_wheel_zoom": "平滑滚轮缩放", + "ui.smooth_wheel_zoom_tip": "关闭后每个滚轮事件立即生效,不做平滑延迟", + "ui.snap": "捕捉", + "ui.space_mouse_hint": "同时移动和旋转视图;草图中仅支持平移和缩放。", + "ui.space_mouse_navigate": "使用 SpaceNavigator / SpaceMouse 导航", + "ui.spacing_2": "间距 2", + "ui.spacing": "间距", + "ui.sphere": "球体", + "ui.spline": "样条曲线", + "ui.split_body": "分割实体", + "ui.stand_up_tip": "当 Z 朝上的 STEP 或 IGES 文件打开后,提示绕 X 轴旋转四分之一周", + "ui.start": "开始", + "ui.start_radius": "起始半径", + "ui.sweep": "扫掠", + "ui.symmetric": "对称", + "ui.symmetry": "对称", + "ui.system": "系统", + "ui.tangent": "相切", + "ui.taper_angle": "锥角", + "ui.tapped": "攻丝", + "ui.text": "文本", + "ui.theme": "主题", + "ui.thickness": "厚度", + "ui.thread": "螺纹", + "ui.timeline_bottom": "底部时间轴", + "ui.timeline_left_note": "左侧布局下,回退条显示在零件和场景浏览器的上方。", + "ui.timeline_left_sidebar": "左侧边栏", + "ui.tip_angle": "尖端角度", + "ui.to_object": "到对象", + "ui.top": "上视", + "ui.torus": "圆环体", + "ui.trackpad_pan_tip": "触控板手势用于平移(关闭:像滚轮一样缩放);鼠标滚轮缩放、拇指滚轮横向平移、双指捏合始终缩放", + "ui.trim": "修剪", + "ui.two_finger_scroll_pans": "双指滚动平移", + "ui.two_sides": "双侧", + "ui.ui_scale": "界面缩放", + "ui.undo": "撤销", + "ui.units": "单位", + "ui.units_of_new_designs": "新设计的单位", + "ui.untitled": "无标题", + "ui.up_axis_note": "视图中朝上的轴(轴视图、主视图、注视)。设计本身不变。", + "ui.variable": "变量", + "ui.view_centre": "视图中心", + "ui.visual_shaded_hidden_edges": "带隐藏边的着色", + "ui.visual_shaded_with_edges": "带边着色", + "ui.visual_style": "视觉样式", + "ui.visual_wireframe": "线框", + "ui.workspace": "工作区", + "ui.y_distance": "Y 距离", + "ui.x_distance": "X 距离", + "ui.y_up": "Y 朝上", + "ui.z_up": "Z 朝上", + "ui.zoom_to_cursor_tip": "滚轮向光标所在点缩放(关闭:向视图中心缩放)", + "ui.zoom_to_the_cursor": "缩放到光标", + "A new configuration, a copy of the active one": "新建配置,为当前配置的副本", + "About SolidCraft": "关于 SolidCraft", + "Active": "活动", + "Active rule": "活动规则", + "Add": "添加", + "Add Key": "添加按键", + "Add Mid Radius": "添加中间半径", + "Add Row": "添加行", + "All Sketch Points": "所有草图点", + "All open designs": "所有打开的设计", + "Allowed expressions, separated by semicolons": "允许的表达式,以分号分隔", + "Also deletes": "同时删除", + "Angle 2": "角度 2", + "Animate": "动画", + "Appearance name": "外观名称", + "Applied Radius": "应用半径", + "Apply To": "应用于", + "Area": "面积", + "Attachment": "附着", + "BROWSER": "浏览器", + "Bend Line Position": "弯折线位置", + "Bend Position": "折弯位置", + "Bend Radius": "折弯半径", + "Bends": "折弯", + "Body": "实体", + "Bounding box": "边界框", + "CSV or JSON: updates existing parameters, adds new ones": "CSV 或 JSON:更新现有参数、添加新参数", + "Capture All Positions": "捕获所有位置", + "Catch Direction": "卡扣方向", + "Cells": "区域", + "Center X": "中心 X", + "Center Y": "中心 Y", + "Center of mass": "质心", + "Chamfer Type": "倒角类型", + "Check collisions": "检查碰撞", + "Checked": "已检查", + "Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "选择一个新平面。草图几何和尺寸在其二维坐标中保持不变。", + "Close draft": "关闭草稿", + "Collisions": "碰撞", + "Colour": "颜色", + "Component1": "组件1", + "Configuration": "配置", + "Configuration rules": "配置规则", + "Contact": "接触", + "Copy all licences": "复制所有许可证", + "Copy diagnostic information": "复制诊断信息", + "Copy report": "复制报告", + "Corner Relief Shape": "拐角释放形状", + "Counterbore depth": "沉孔深度", + "Countersink angle": "沉头角度", + "Create Issue — review draft": "创建 Issue——审阅草稿", + "Customize Toolbar": "自定义工具栏", + "Delete all": "全部删除", + "Delete rule": "删除规则", + "Delete the row": "删除此行", + "Delete the set": "删除此集合", + "Delete this parameter": "删除此参数", + "Density": "密度", + "Designs you open or save appear here.": "你打开或保存的设计会显示在此处。", + "Determined by To Object. Choose Angle to edit the angular extent.": "由“到对象”确定。选择“角度”可编辑角度范围。", + "Discard": "放弃", + "Double-click to rename": "双击重命名", + "Drive Joint": "驱动接头", + "Edit Profile Sketch": "编辑轮廓草图", + "Enable rule": "启用规则", + "Enabled": "已启用", + "Explode": "爆炸", + "Extents": "范围", + "Face attachment lost": "面附着丢失", + "Faces / edges / vertices": "面 / 边 / 顶点", + "Favourite": "收藏", + "Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "许多 CAD 程序的文件以 Y 为上轴,因此在此处打开时会平躺(Z 朝上)。", + "Filter": "筛选", + "Find in Browser": "在浏览器中查找", + "Flat size": "平板尺寸", + "Follow camera (Look At sketches)": "跟随相机(注视草图)", + "Font": "字体", + "Full Length": "全长", + "Gap": "间隙", + "Grid": "网格", + "Height": "高度", + "Home view": "主视图", + "In This Design": "在此设计中", + "Interferes": "发生干涉", + "Issue title": "Issue 标题", + "Joint Origin1": "接头原点1", + "Keep Tools": "保留工具", + "Later": "稍后", + "Leave it": "保持原样", + "Legacy face attachment. Rebinding to a certified face creates an exact attachment.": "旧版面附着。重新绑定到经验证的面可创建精确附着。", + "Length": "长度", + "Length units": "长度单位", + "Library": "零件库", + "Loop": "循环", + "Margin": "余量", + "Mass": "质量", + "Material": "材质", + "Mid Radius": "中间半径", + "Modeled": "已建模", + "More toolbar panels": "更多工具栏面板", + "Motion": "运动", + "Move Type": "移动类型", + "Move earlier": "向前移动", + "Move later": "向后移动", + "Name": "名称", + "No crash reports.": "没有崩溃报告。", + "No matching appearances": "没有匹配的外观", + "Not available yet": "暂不可用", + "Opacity": "不透明度", + "Open GitHub draft": "打开 GitHub 草稿", + "Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "使用 Rust 编写的开源参数化 3D CAD。基于 MIT OR Apache-2.0 许可。", + "Other side": "另一侧", + "Parameter values": "参数值", + "Per Edge": "每条边", + "Pick the tool bodies": "选择工具实体", + "Place at the origin instead": "改为放置在原点", + "Playback": "播放", + "Position": "位置", + "Position (0 to 1)": "位置(0 到 1)", + "Position along the edge, 0 at the start end, 1 at the other": "沿边的位置,起点端为 0,另一端为 1", + "Preview leaves the current design unchanged.": "预览不会更改当前设计。", + "RECENT": "最近使用", + "Radius Fit": "半径拟合", + "Ratio": "比率", + "Recover": "恢复", + "Recover unsaved design?": "恢复未保存的设计?", + "Remove Tools": "移除工具", + "Remove from list": "从列表移除", + "Remove private design details or paths before sharing.": "共享前请移除私密设计细节或路径。", + "Remove the column": "移除此列", + "Remove the track": "移除轨迹", + "Remove this point": "移除此点", + "Report a bug — review a screenshot and open a GitHub draft": "报告缺陷 — 复核截图并开启 GitHub 草稿", + "Reset Toolbar": "重置工具栏", + "Restore the inherited component or physical material appearance": "还原继承的组件或物理材质外观", + "Result": "结果", + "Retake screenshot": "重新截图", + "Reverse": "反向", + "Reverse Direction": "反向", + "Revert": "还原", + "Revert All Positions": "还原所有位置", + "Review the report and screenshot before opening GitHub.": "打开 GitHub 前请审阅报告和截图。", + "Rim Edge": "轮缘边", + "Roll History Marker Here": "将历史标记滚动到此处", + "Rule": "规则", + "Saved preview. Geometry is being checked before editing.": "已保存预览。编辑前正在检查几何。", + "Scale factor": "比例因子", + "Search": "搜索", + "Search commands": "搜索命令", + "Select bodies (a face picks its body)": "选择实体(单击面即选择其所属实体)", + "Select bodies or faces instead": "改为选择实体或面", + "Selection filter": "选择筛选", + "Semantic faces share their appearance across instances.": "语义面在各实例间共享外观。", + "Shape": "形状", + "Show Agent Cursor": "显示代理光标", + "Show in folder": "在文件夹中显示", + "Size": "大小", + "Sketch plane has no body-face attachment.": "草图平面没有实体面附着。", + "SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft 在这些设计保存前已关闭。可以打开它们的上次自动保存。", + "Speed": "速度", + "Stand it up": "将其立起", + "Stand it up: a quarter turn about X, as one Move feature you can undo.": "将其立起:绕 X 轴旋转四分之一周,作为一个可撤销的移动特征。", + "Stand this part up?": "立起此零件?", + "Step": "步骤", + "Steps": "步数", + "Suppressed": "已抑制", + "Swap Ends": "交换端点", + "Tangent Chain": "相切链", + "The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "虚线草图是已保存的平面引导。选择一个新面以修复附着。", + "The saved author occurrence is missing. Select a valid author placement before rebinding.": "保存的作者实例缺失。重新绑定前请选择有效的作者放置。", + "Then": "操作", + "These features will be deleted:": "以下特征将被删除:", + "These features will fail:": "以下特征将失败:", + "This child is shared. Deleting it removes it from every copy of its parent.": "此子项是共享的。删除它会从其父项的每个副本中移除。", + "This child is shared. Position changes apply to every copy of its parent.": "此子项是共享的。位置更改会应用到其父项的每个副本。", + "This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "此面无法提供持久附着。请选择其他面或显式使用固定平面。", + "To Offset": "偏移", + "Tolerance": "公差", + "Type": "类型", + "Updating the preview": "正在更新预览", + "Use fixed plane": "使用固定平面", + "VIEW": "视图", + "Value": "值", + "Volume": "体积", + "Wall": "壁厚", + "When": "条件", + "Width": "宽度", + "Will fail": "将失败", + "angle, height": "角度、高度", + "command name": "命令名称", + "expression, e.g. 25 mm": "表达式,例如 25 mm", + "fits the face, e.g. M8": "适配该面,例如 M8", + "from the rule": "来自规则", + "needs two joints": "需要两个接头", + "new configuration": "新配置", + "new name": "新名称", + "new parameter": "新参数", + "no interference": "无干涉", + "no joint that moves yet": "尚无运动接头", + "no picture": "无图片", + "pick a hole to seat it, or OK places it at the origin": "选择一个孔放置,或单击确定放置在原点", + "pick one or two vertices, edges or faces": "选择一个或两个顶点、边或面", + "the parts library is not available": "零件库不可用", + "to the next face": "到下一个面", + "Revert Position": "恢复位置", + "Construction": "构造", + "Faces": "面", + "Edges": "边", + "Vertices": "顶点", + "Sketch geometry and profiles": "草图几何和轮廓", + "Sketch curves": "草图曲线", + "Sketch points": "草图点", + "Sketch dimensions": "草图尺寸", + "Sketch constraints": "草图约束", + "0 turns it off": "0 表示关闭", + "Show the pointer an AI agent moves (#35)": "显示 AI 代理移动的指针(#35)", + "Follow the camera (Look At sketches)": "跟随相机(注视草图)", + "Flat": "平面", + "Save Rule": "保存规则", + "Preview Row": "预览行", + "Add condition": "添加条件", + "Apply to selection": "应用到选择", + "Remove override": "移除覆盖", + "Copy screenshot": "复制截图", + "Remove screenshot": "移除截图", + "Use active author placement": "使用活动作者放置", + "Reattach selected face": "重新附着所选面", + "Use saved fixed plane": "使用已保存的固定平面", + "Use selected face as fixed plane": "使用所选面作为固定平面", + "View navigator": "视图导航器", + "Signed axes": "带符号轴", + "Cube": "立方体", + "Middle double-click": "中键双击", + "Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "在视图中双击中键:主视图(主视图,框住所显示的内容)、适合窗口(框住所显示的内容,保持方向)或无动作", + "Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "“选择其他”:在模型上按住左键(或右键单击 › 选择其他…)列出光标下的所有对象;在同一位置再次单击可选择该处的下一个对象。双击中键转到主视图(首选项 › 导航)。", + "cmd.render_backends": "渲染后端", + "cmd.view_navigate": "导航视图", + "ui.animateJoint": "动画接头", + "ui.calibrate": "校准", + "ui.customizeToolbar": "自定义工具栏…", + "ui.editJointLimits": "编辑接头限制", + "ui.exportDxf": "导出 DXF…", + "ui.findInTimeline": "在时间线中查找", + "ui.findInWindow": "在窗口中查找", + "ui.fit": "适合", + "ui.groupSelected": "将所选编组", + "ui.hideAxes": "隐藏轴", + "ui.hideDimension": "隐藏尺寸", + "ui.hideOrigin": "隐藏原点", + "ui.hidePlanes": "隐藏平面", + "ui.hideProfile": "隐藏轮廓", + "ui.homeView": "主视图", + "ui.isolate": "隔离", + "ui.lock": "锁定", + "ui.newSketchGroup": "新建草图组", + "ui.normalConstruction": "普通 / 构造", + "ui.orbit": "环绕", + "ui.heading_position": "位置", + "ui.pan": "平移", + "ui.pinToShortcuts": "固定到快捷方式", + "ui.pinToToolbar": "固定到工具栏", + "ui.properties": "属性", + "ui.removeSection": "移除剖面", + "ui.removeFromToolbar": "从工具栏移除", + "ui.repeat": "重复", + "ui.report_a_bug": "报告缺陷", + "ui.restore": "还原", + "ui.heading_setup": "设置", + "ui.selectOther": "选择其他…", + "ui.selectTangentChain": "选择相切链", + "ui.showAll": "显示全部", + "ui.showAllBodies": "显示所有实体", + "ui.showAxes": "显示轴", + "ui.showDimension": "显示尺寸", + "ui.showOrigin": "显示原点", + "ui.showPlanes": "显示平面", + "ui.suppress": "抑制", + "ui.command_palette_hint": "输入命令,例如 {example} — 或按 S 搜索", + "ui.unground": "取消固定", + "ui.unisolate": "取消隔离", + "ui.unlock": "解锁", + "ui.unsuppress": "取消抑制", + "ui.updateToCurrentView": "更新为当前视图", + "ui.zoom": "缩放", + "ui.command_done": "{command} 完成", + "timeline.group": "新建特征文件夹", + "timeline.ungroup": "移除特征文件夹", + "timeline.rename_group": "重命名特征文件夹", + "timeline.move_to_group": "将特征移至文件夹", + "timeline.move_many": "移动时间线项目", + "timeline.reorder_many": "特征重新排序", + "ui.show_construction_geometries": "构造几何图元", + "ui.total_angle": "总角度", + "sketch_text.builtin_font": "{family}(内置)", + "sketch_text.font_search": "搜索字体", + "sketch_text.loading_fonts": "正在查找已安装字体…", + "sketch_text.use_font_file": "使用字体文件 {path}", + "sketch_text.bold": "粗体", + "sketch_text.italic": "斜体", + "sketch_text.align_left": "左对齐", + "sketch_text.align_center": "居中", + "sketch_text.align_right": "右对齐", + "sketch_text.path_hint": "文字所跟随的草图曲线:先单击此处,再选择曲线", + "sketch_text.path_clear": "将文字移离其曲线", + "Style": "样式", + "Alignment": "对齐", + "Letter Spacing": "字间距", + "Line Spacing": "行间距", + "Path": "路径", + "sketch_text.on_path": "文字沿路径排列:由曲线放置", + "ui.analysis": "分析", + "ui.edit_section_analysis": "编辑截面分析" +} diff --git a/crates/ui-egui/src/palette.rs b/crates/ui-egui/src/palette.rs index 1968eeab..47cea325 100644 --- a/crates/ui-egui/src/palette.rs +++ b/crates/ui-egui/src/palette.rs @@ -67,7 +67,9 @@ pub fn palette_bar(app: &mut SolveApp, ui: &mut egui::Ui) { ui.label(RichText::new("›").strong().color(t.accent)); let r = ui.add( egui::TextEdit::singleline(&mut app.palette.text) - .hint_text("Type a command, e.g. Extrude {\"distance\": 20} — or press S to search") + // The JSON example keeps its braces here rather than in the catalog, so the + // placeholder parser never has to treat JSON punctuation as a name. + .hint_text(crate::i18n::format("ui.command_palette_hint", &[("example", "Extrude {\"distance\": 20}")])) .desired_width(520.0), ); if app.palette.focus { diff --git a/crates/ui-egui/src/prefs.rs b/crates/ui-egui/src/prefs.rs index f22a1505..1bb1f250 100644 --- a/crates/ui-egui/src/prefs.rs +++ b/crates/ui-egui/src/prefs.rs @@ -860,13 +860,15 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) { ); crate::scenario::publish_rect("prefs:scale", scale.rect); ui.end_row(); - ui.label("Feature history"); + ui.label(app.tr("ui.feature_history")); + let timeline_bottom = app.tr("ui.timeline_bottom").to_string(); + let timeline_left = app.tr("ui.timeline_left_sidebar").to_string(); ui.horizontal(|ui| { - ui.radio_value(&mut app.preferences.timeline_left, false, "Bottom timeline"); - ui.radio_value(&mut app.preferences.timeline_left, true, "Left sidebar"); + ui.radio_value(&mut app.preferences.timeline_left, false, timeline_bottom); + ui.radio_value(&mut app.preferences.timeline_left, true, timeline_left); }); ui.end_row(); - note(ui, "The left layout shows the rollback bar above the parts and scene browser."); + note(ui, app.tr("ui.timeline_left_note")); ui.label(app.tr("ui.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 { diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 889a90c0..2a5f1df0 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -296,7 +296,8 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) { tabs_ui.set_clip_rect(tabs_rect.intersect(ui.clip_rect())); crate::documents::tabs(app, &mut tabs_ui, r, tabs_start, tabs_end); let response = ui.interact(report, ui.id().with("sc_report_button"), Sense::click()); - response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Report a bug")); + let report_text = crate::i18n::tr("ui.report_a_bug"); + response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), report_text)); crate::scenario::publish_handle("titlebar:report", report.center()); ui.painter().rect_filled(report, 4.0, if response.hovered() || response.has_focus() { t.hover } else { t.app_bar }); ui.painter().rect_stroke(report, 4.0, Stroke::new(1.0, t.border), egui::StrokeKind::Inside); @@ -306,7 +307,7 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) { ui.painter().text( pos2(report.left() + 29.0, report.center().y), Align2::LEFT_CENTER, - "Report a bug", + report_text, FontId::proportional(13.0), t.app_bar_text, ); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index c6f85b44..e0cf0ec4 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -123,7 +123,7 @@ unit regardless of the design, pass an expression with one: `"value": "50 mm"` o | `ui.home` | `open?`: bool; `sample?`: index | the start page: show or hide it, open a built-in sample (once built); returns `open` and the recent designs | | `ui.shortcut` | `command`, `key?`, `replace?` | a command's key, or rebinds it (`""` clears; a key another command uses is refused unless `replace`) | | `ui.help` | `item?`: `about`, `shortcuts` (read only), `report` (copies diagnostics), `crashes` (the Crash Reports window) | the version, commit, build date and diagnostic text | -| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `tinkercad`, `openscad`), `zoom_reverse`, `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, pinch zooms), `orbit_cursor`, `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system`), `language` (`en`, `ja`; see [localization](localization.md)), `ui_scale`, `up_axis` (`z`, `y`), `offer_stand_up`; `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` | +| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `tinkercad`, `openscad`), `zoom_reverse`, `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, pinch zooms), `orbit_cursor`, `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system`), `language` (`en`, `ja`, `de`, `ko`, `zh`; see [localization](localization.md)), `ui_scale`, `up_axis` (`z`, `y`), `offer_stand_up`; `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` | | `ui.capture` | `action?`: `capture`, `revert`, `cancel` | answers the Capture Position question (asked when a command starts or the design is saved while components were moved without capturing); without `action`, what waits | | `ui.window` | `action`: minimize, maximize, restore, toggle, close | what the title bar's caption buttons do (on Windows and Linux the application bar is the title bar); close asks about unsaved designs first | | `ui.agent_cursor` | `show` (bool), `speed`: a number from 1 to 5 (times the normal speed) or `"instant"` (no animation: the command runs at once and the pointer jumps to where it acted), `follow_camera` (bool); all optional | the settings. While the cursor is shown, `engine.execute` first glides to the command's toolbar button and to each pick, then runs and replies; later requests wait their turn. `"animate": false` on a call skips it | diff --git a/docs/localization.md b/docs/localization.md index c81b2112..407f49e7 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -1,8 +1,9 @@ # UI localization -SolidCraft embeds its translations and works offline. English and Japanese are currently -registered in `crates/ui-egui/src/i18n.rs`. Adding a language requires an embedded catalog and -one registration there; the preferences selector and validation use the same registry. +SolidCraft embeds its translations and works offline. English, Japanese, German, Korean and +Chinese are currently registered in `crates/ui-egui/src/i18n.rs`. Adding a language requires an +embedded catalog and one registration there; the preferences selector and validation use the same +registry. ## Messages @@ -23,6 +24,37 @@ Catalogs are parsed once and looked up through ordered maps. This is offline loc not a claim of zero runtime overhead. The catalog tests check registered locale IDs, translation keys, and placeholder consistency. Run `cargo xtask ci` before submitting changes. +## Coverage + +A message a locale never received falls back to English silently, so a gap shows up only as a row +that looks untranslated. Two guards catch that in the build: + +- `every_complete_catalog_carries_every_english_message` — adding a key to `en.json` + without giving it to a complete catalog fails the tests. English is the source catalog, + Japanese and Chinese are delivered complete; German and Korean are still partial — their + gaps are reported by `cargo xtask i18n` without failing the build. +- `every_toolbar_tab_and_panel_is_in_the_english_catalog` — tabs and panels reach the UI as bare + source strings, resolved only by the English-label adapter, so a heading missing from `en.json` + can never translate. + +Command labels are the same case on a larger scale: every label in the engine's registry now has +an entry under `cmd.`, and `every_command_label_is_in_the_english_catalog` fails if a +command is added without one. + +The context menu and the browser are the same case again, and they carry the most labels of all: +`every_menu_label_is_in_the_english_catalog` opens every menu the app can reach, flips the state +each conditional label branches on (origin shown, bodies locked, bodies hidden) so both branches +are seen, and fails on any label the catalog has never seen. Unit symbols are left out of the +sweep: `mm` is `mm` in every language, so they are not prose to translate. The sweep also asserts +it keeps reaching a broad set of labels, so it cannot pass by finding nothing. + +`cargo xtask i18n` prints the same picture across every locale — which messages each one is +missing and which command labels English cannot translate. `--check` turns it into a non-zero exit +for CI. `--add-missing` writes each uncovered label into `en.json` under `cmd.`, with +the English label as its text, so the label adapter can find it and only the translations are left +to do. One key per distinct label: two commands that share a label (`Trim`, `Extend`) must not both +claim it, or the English-text lookup would silently pick one. + ## Rendering and coverage One immutable `Locale` type handles all translations. Table widgets without an app reference @@ -31,18 +63,23 @@ Standalone translated panels enter the same scope. Measure rendered text to size widgets; UTF-8 byte lengths do not describe their displayed width. The desktop host tries system CJK fonts across Windows, macOS and Linux, as well as an optional -`CRAFT_FONTS_DIR` font pack, and skips files that cannot be parsed. No fonts are redistributed -in native desktop distribution packages; native builds rely entirely on existing operating system fonts -or craft-fonts. +`CRAFT_FONTS_DIR` font pack, and skips files that cannot be parsed. Chinese uses its own fallback +chain after the general CJK font: many Japanese CJK fonts (Hiragino Gothic, Meiryo, Yu Gothic) +match the CJK font search but lack simplified characters, so a second fallback found by the +Chinese font-name list (`PingFang`, `Microsoft YaHei`, `Noto Sans CJK SC`, …) covers them per +glyph. No fonts are redistributed in native desktop distribution packages; native builds rely +entirely on existing operating system fonts or craft-fonts. In the browser, `solidcraft-web` bundles a self-hosted Japanese font (`BIZUDPGothic-Regular.ttf`, licensed under SIL OFL 1.1) within the web distribution artifacts only. The font is loaded asynchronously from the same -origin (`./fonts/BIZUDPGothic-Regular.ttf`) on-demand when the interface language is set to Japanese, or, with +origin (`./fonts/BIZUDPGothic-Regular.ttf`) on-demand when the interface language is a CJK one, or, with any other interface language, the first time Japanese text (kana, kanji or full-width forms in a design name, file name, body name or typed text) is drawn, without contacting any external CDNs or third-party servers. -A session that never shows such text downloads no font. If font loading fails with the Japanese interface, -the web interface shows English for that session only to prevent missing glyphs (tofu); the saved language -preference is kept, so the next visit tries again. This bundled font specifically targets Japanese; Korean (Hangul) and Chinese scripts are not covered. +A session that never shows such text downloads no font. If font loading fails with a CJK interface, the web +interface shows English for that session only to prevent missing glyphs (tofu); the saved language +preference is kept, so the next visit tries again. The bundled font specifically targets Japanese; it also +renders the kanji Chinese shares with Japanese, while Korean (Hangul) and simplified-only Chinese characters +are not covered, so a self-hosted SC/Korean font bundle remains follow-up work for full web coverage. The current messages support named substitution, not locale-specific plural selection, number/date formatting, or right-to-left layout and shaping. Add those capabilities when diff --git a/xtask/src/i18n.rs b/xtask/src/i18n.rs new file mode 100644 index 00000000..888b68b5 --- /dev/null +++ b/xtask/src/i18n.rs @@ -0,0 +1,232 @@ +//! `cargo xtask i18n`: the message catalogs' coverage report, and a way to repair it. +//! +//! The catalogs under `crates/ui-egui/src/locales` are plain JSON maps from a stable message id +//! to one translation. The English catalog is also the lookup table for `Locale::label()`, which +//! translates source strings coming from the engine (command labels, toolbar tabs and panels) by +//! matching their English text. A label the English catalog does not carry therefore stays English +//! in *every* locale, silently — nothing fails, the row just looks untranslated. +//! +//! That is the failure this command exists to make visible. `--check` turns the report into a +//! non-zero exit for CI; `--add-missing` writes the uncovered command labels into the English +//! catalog under `cmd.` so a translator only has to fill in the other catalogs. +//! +//! German and Korean predate the coverage guards and are still partial, so their gaps are +//! reported but do not fail `--check`; the catalogs delivered complete (`COMPLETE`) may not +//! regress. + +use std::collections::BTreeMap; +use std::path::Path; + +use serde_json::{Map, Value}; + +/// Where the catalogs live, and the file the English one must lead. +const DIR: &str = "crates/ui-egui/src/locales"; + +/// Catalogs delivered complete: a missing message here fails `--check`. English is the source +/// catalog and is checked implicitly; keep this in step with `COMPLETE` in `ui-egui::i18n`. +const COMPLETE: &[&str] = &["ja", "zh"]; + +/// What `run` found. +#[derive(Default)] +struct Report { + /// Locale id → the English message ids it has not translated. + missing: BTreeMap>, + /// Command label → the command ids that use it, for labels English does not carry. + untranslated: BTreeMap>, +} + +impl Report { + /// Gaps that must be repaired (complete catalogs only; partial catalogs are reported). + fn problems(&self) -> usize { + self.missing.iter().filter(|(id, _)| COMPLETE.contains(&id.as_str())).map(|(_, m)| m.len()).sum::() + self.untranslated.len() + } + + /// Gaps in catalogs that are still allowed to be partial. + fn reported_only(&self) -> usize { + self.missing.iter().filter(|(id, _)| !COMPLETE.contains(&id.as_str())).map(|(_, m)| m.len()).sum() + } +} + +pub fn run(root: &Path, args: &[&str]) -> Result<(), String> { + let english = read(root, "en")?; + let mut catalogs: Vec<(String, Map)> = Vec::new(); + for id in locales(root)? { + let messages = read(root, &id)?; + catalogs.push((id, messages)); + } + + let mut report = Report::default(); + for (id, messages) in &catalogs { + if id == "en" { + continue; + } + let missing: Vec = english.keys().filter(|k| !messages.contains_key(*k)).cloned().collect(); + if !missing.is_empty() { + report.missing.insert(id.clone(), missing); + } + } + + let known: Vec<&str> = english.values().filter_map(Value::as_str).collect(); + for (label, ids) in labels_needing_translation(&known) { + report.untranslated.insert(label, ids); + } + + if args.contains(&"--add-missing") { + let added = add_missing(root, &mut report)?; + println!("i18n: wrote {added} new English message(s) into {DIR}/en.json"); + return Ok(()); + } + + print_report(&report); + + if args.contains(&"--check") { + if report.problems() > 0 { + return Err(format!( + "{} message(s) missing from a complete catalog and {} command label(s) with no English entry \ + (see above); `cargo xtask i18n --add-missing` writes the labels into en.json", + report.missing.iter().filter(|(id, _)| COMPLETE.contains(&id.as_str())).map(|(_, m)| m.len()).sum::(), + report.untranslated.len() + )); + } + if report.reported_only() > 0 { + println!( + "i18n: every complete catalog ({}) carries every English message and every command label is translatable; \ + {} message(s) are still missing from the partial catalogs (de, ko)", + COMPLETE.join(", "), + report.reported_only() + ); + } else { + println!("i18n: every catalog carries every English message; every command label is translatable"); + } + } + Ok(()) +} + +/// Locale ids other than English: every catalog file that sits beside `en.json`, so a newly +/// added locale is picked up without editing this command. +fn locales(root: &Path) -> Result, String> { + let dir = root.join(DIR); + let entries = std::fs::read_dir(&dir).map_err(|e| format!("{}: {e}", dir.display()))?; + let mut ids: Vec = entries + .filter_map(Result::ok) + .map(|e| e.path()) + .filter(|p| p.extension().and_then(|x| x.to_str()) == Some("json")) + .filter_map(|p| p.file_stem().map(|s| s.to_string_lossy().into_owned())) + .filter(|id| id != "en") + .collect(); + ids.sort(); + Ok(ids) +} + +fn read(root: &Path, locale: &str) -> Result, String> { + let path = root.join(DIR).join(format!("{locale}.json")); + let text = std::fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?; + match serde_json::from_str(&text) { + Ok(Value::Object(map)) => Ok(map), + Ok(_) => Err(format!("{}: a catalog must be a JSON object", path.display())), + Err(e) => Err(format!("{}: {e}", path.display())), + } +} + +/// Command labels the English catalog cannot translate, mapped to the commands that use them. +/// +/// The registry is read through `solidcraft-cli commands` so the report comes from the real +/// command table, never from a copy of it. +fn labels_needing_translation(known: &[&str]) -> BTreeMap> { + let mut out: BTreeMap> = BTreeMap::new(); + let Ok(commands) = registry() else { + return out; + }; + for command in commands { + let (Some(id), Some(label)) = (command["id"].as_str(), command["label"].as_str()) else { + continue; + }; + if known.contains(&label) { + continue; + } + let ids = out.entry(label.to_string()).or_default(); + if !ids.iter().any(|x| x == id) { + ids.push(id.to_string()); + } + } + out +} + +fn registry() -> Result, String> { + let out = crate::cargo() + .args(["run", "-q", "--release", "-p", "solidcraft-cli", "--", "commands"]) + .output() + .map_err(|e| format!("solidcraft-cli: {e}"))?; + if !out.status.success() { + return Err(format!("solidcraft-cli commands failed:\n{}", String::from_utf8_lossy(&out.stderr))); + } + serde_json::from_slice(&out.stdout).map_err(|e| format!("commands JSON: {e}")) +} + +/// Write one `cmd.` entry per still-untranslated label into the English catalog and +/// drop the newly covered labels from the report. +fn add_missing(root: &Path, report: &mut Report) -> Result { + if report.untranslated.is_empty() { + return Ok(0); + } + let path = root.join(DIR).join("en.json"); + let mut english = read(root, "en")?; + // One key per label, so two commands sharing a label cannot collide in `label()`'s + // English-text → id table. The lowest command id wins, keeping the entry stable. + let mut chosen: BTreeMap = BTreeMap::new(); + for (label, ids) in &report.untranslated { + if let Some(id) = ids.iter().min() { + chosen.insert(label.clone(), format!("cmd.{id}")); + } + } + for (label, key) in &chosen { + if english.contains_key(key) { + continue; + } + english.insert(key.clone(), Value::String(label.clone())); + } + english.sort_keys(); + std::fs::write(&path, serde_json::to_string_pretty(&Value::Object(english)).map_err(|e| e.to_string())? + "\n") + .map_err(|e| format!("{}: {e}", path.display()))?; + let added = chosen.len(); + report.untranslated.clear(); + Ok(added) +} + +fn print_report(report: &Report) { + for (locale, missing) in &report.missing { + println!("i18n: {locale} is missing {} English message(s):", missing.len()); + for id in missing { + println!(" {id}"); + } + } + if !report.untranslated.is_empty() { + println!("i18n: {} command label(s) have no English catalog entry, so they stay English in every locale:", report.untranslated.len()); + for (label, ids) in &report.untranslated { + println!(" {label:<42} {}", ids.join(", ")); + } + println!("i18n: run `cargo xtask i18n --add-missing` to add them to en.json, then translate them."); + } + if report.problems() == 0 { + println!("i18n: catalogs complete; every command label is translatable"); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn report_counts_both_kinds_of_gap() { + let report = Report { + missing: BTreeMap::from([("zh".into(), vec!["a".into(), "b".into()])]), + untranslated: BTreeMap::from([("Flange".into(), vec!["sheet.flange".into()])]), + }; + assert_eq!(report.problems(), 3); + } + + #[test] + fn an_empty_report_has_no_problems() { + assert_eq!(Report::default().problems(), 0); + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 14b6b209..5e87a5be 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -6,6 +6,7 @@ mod assets; mod book; mod features; +mod i18n; mod layers; mod licences; mod test; @@ -20,6 +21,12 @@ commands: layers enforce the crate dependency layering (plan/architecture.md §3) assets check that every non-code asset file is attributed in ATTRIBUTION.md features list SolidCraft's commands by area in docs/features.md (from the registry) + i18n [--check | --add-missing] + report how complete the message catalogs in crates/ui-egui/src/locales are: + which messages a locale has not translated and which command labels the English + catalog cannot translate (so they stay English everywhere). --check fails when + there is a gap, for CI; --add-missing writes the uncovered command labels into + en.json under cmd. so only the translations are left to do oracle [--cli BIN] replay the reference tests (tests/oracle/*/recipe.json), compare with each case's expected.json and write docs/oracle.md (--cli: a solidcraft-cli @@ -49,6 +56,7 @@ fn main() -> ExitCode { Some("layers") => cmd_layers(), Some("assets") => assets::run(&root()), Some("features") => features::run(&root()), + Some("i18n") => i18n::run(&root(), &rest), Some("oracle") => cmd_oracle(rest.iter().position(|a| *a == "--cli").and_then(|i| rest.get(i + 1)).copied()), Some("action-matrix") => { cmd_action_matrix(rest.contains(&"--bless"), rest.iter().position(|a| *a == "--results").and_then(|i| rest.get(i + 1)).copied())