doc, ui: a feature that lost a reference asks for it to be picked again

DocError::Reselect names the feature and the stored reference (a face or edge
name) that no longer finds what was picked: "<feature>: the reference `<name>`
no longer exists as it was picked; reselect it". Evaluation fills in the
feature's name; reselect_parts reads both back from a feature's error text, and
the UI shows the message translated (ar, de, en, he, ja, ko, zh in
assets/locales/pending/reference-reselect.json).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-30 12:12:15 -04:00
committed by jyviko
parent e43fe1ab04
commit b24d902692
4 changed files with 77 additions and 3 deletions
@@ -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}` 已不再是选择时的样子;请重新选择"
}
}
+2 -2
View File
@@ -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(()) }
}),
+35
View File
@@ -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<String>) -> 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<T> = std::result::Result<T, DocError>;
+17 -1
View File
@@ -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() {