diff --git a/assets/locales/pending/reference-reselect.json b/assets/locales/pending/reference-reselect.json new file mode 100644 index 0000000000..2cea7dc5eb --- /dev/null +++ b/assets/locales/pending/reference-reselect.json @@ -0,0 +1,23 @@ +{ + "ar": { + "diagnostic.reselect_reference": "{feature}: المرجع `{reference}` لم يعد موجودًا كما اختير؛ أعد اختياره" + }, + "de": { + "diagnostic.reselect_reference": "{feature}: Die Referenz `{reference}` existiert nicht mehr so, wie sie ausgewählt wurde; wählen Sie sie erneut aus" + }, + "en": { + "diagnostic.reselect_reference": "{feature}: the reference `{reference}` no longer exists as it was picked; reselect it" + }, + "he": { + "diagnostic.reselect_reference": "{feature}: ההפניה `{reference}` כבר אינה קיימת כפי שנבחרה; יש לבחור אותה מחדש" + }, + "ja": { + "diagnostic.reselect_reference": "{feature}: 参照 `{reference}` は選択したときのままでは存在しません。選択し直してください" + }, + "ko": { + "diagnostic.reselect_reference": "{feature}: 참조 `{reference}`이(가) 선택했을 때의 모습으로 더 이상 존재하지 않습니다. 다시 선택하세요" + }, + "zh": { + "diagnostic.reselect_reference": "{feature}:引用 `{reference}` 已不再是选择时的样子;请重新选择" + } +} diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index 93d2b3897b..c958389412 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -2063,7 +2063,7 @@ impl Model { } Ok(None) => f, Err(e) => { - frame_error = Some(e.to_string()); + frame_error = Some(e.in_feature(&f.name).to_string()); f } }; @@ -2074,7 +2074,7 @@ impl Model { None => kernel::guard(&f.name, || { let (r, deferred) = kernel::validating(validation, || eval_feature(doc, &vals, f, &mut next, &mut warning, &mut import_report, &mut repair)); - r.map_err(|e| kernel::KernelError::Failed(e.to_string()))?; + r.map_err(|e| kernel::KernelError::Failed(e.in_feature(&f.name).to_string()))?; // The checks the feature's steps left to its end: the bodies it keeps. if deferred { check_kept(&state, &next) } else { Ok(()) } }), diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index f51058bf1e..7e43f91ba4 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -103,6 +103,41 @@ pub enum DocError { Sketch(#[from] solidcraft_sketch::SketchError), #[error(transparent)] Kernel(#[from] solidcraft_kernel::KernelError), + /// A stored reference (a face or edge name, a piece of a split face) no longer finds exactly + /// what it named: the feature using it fails, and the user picks it again. `feature` is + /// empty until the evaluation of the feature fills it in ([`DocError::in_feature`]). + #[error("{}", reselect_message(feature, reference))] + Reselect { feature: String, reference: String }, +} + +impl DocError { + /// A feature's `reference` that no longer finds what it named ([`DocError::Reselect`]), for + /// the feature being evaluated. + pub fn reselect(reference: impl Into) -> Self { + DocError::Reselect { feature: String::new(), reference: reference.into() } + } + + /// This error, raised while feature `feature` was evaluated: a [`DocError::Reselect`] names it. + pub fn in_feature(self, feature: &str) -> Self { + match self { + DocError::Reselect { feature: f, reference } if f.is_empty() => DocError::Reselect { feature: feature.into(), reference }, + other => other, + } + } +} + +/// What a feature that lost a reference says ([`DocError::Reselect`]). +fn reselect_message(feature: &str, reference: &str) -> String { + let feature = if feature.is_empty() { "this feature" } else { feature }; + format!("{feature}: the reference `{reference}` no longer exists as it was picked; reselect it") +} + +/// The feature and the reference of a [`DocError::Reselect`] message, read back from its text +/// (a feature's error is kept as text): `None` for any other message. +pub fn reselect_parts(text: &str) -> Option<(&str, &str)> { + let (feature, rest) = text.split_once(": the reference `")?; + let reference = rest.strip_suffix("` no longer exists as it was picked; reselect it")?; + Some((feature, reference)) } pub type Result = std::result::Result; diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 166c9ca62e..a3ef0e6379 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -202,8 +202,12 @@ impl Locale { } /// Evaluation errors stay English for scripts; the empty Cut/Intersect hint that names - /// another component is translated where it is shown. Other text is returned unchanged. + /// another component, and a feature's request to reselect a reference it lost, are translated + /// where they are shown. Other text is returned unchanged. pub fn diagnostic(self, text: &str) -> String { + if let Some((feature, reference)) = solidcraft_engine::doc::reselect_parts(text) { + return self.format("diagnostic.reselect_reference", &[("feature", &self.given_name(feature)), ("reference", reference)]); + } const PREFIX: &str = "there is no body to cut or intersect in component '"; if let Some((before, detail)) = text.split_once(PREFIX) && let Some((component, detail)) = detail.split_once("'; the tool may overlap '") @@ -379,6 +383,18 @@ mod tests { assert_eq!(Locale::for_language("ja").diagnostic(plain), plain); } + #[test] + fn reselect_reference_diagnostic_translates_with_the_feature_and_reference_as_given() { + let message = solidcraft_engine::doc::DocError::reselect("F4:side:c1#1|F4:top").in_feature("Fillet {2}").to_string(); + assert_eq!(Locale::for_language("en").diagnostic(&message), message); + for language in ["ar", "de", "he", "ja", "ko", "zh"] { + let translated = Locale::for_language(language).diagnostic(&message); + assert_ne!(translated, message, "{language}"); + // Right-to-left text comes back in display order, so only what it holds is checked. + assert!(translated.contains("Fillet {2}") && translated.contains("`F4:side:c1#1|F4:top`"), "{language}: {translated}"); + } + } + /// English first, then by id, so adding a locale edits only its own line. #[test] fn locales_are_listed_english_first_then_by_id() {