about summary refs log tree commit diff
path: root/compiler/rustc_errors/src
diff options
context:
space:
mode:
authornils <48135649+Nilstrieb@users.noreply.github.com>2023-01-11 17:30:54 +0100
committerGitHub <noreply@github.com>2023-01-11 17:30:54 +0100
commit73476554e9f89245be50195d5ff8e66cc5e64c82 (patch)
tree291180ff80c335e8552b10fc87c733f099e0a0c1 /compiler/rustc_errors/src
parent16938915a46c64dd83f02dee85e5b4eba6891076 (diff)
parent4c0c32c895df28b762a61958b21cbe4d68f60238 (diff)
downloadrust-73476554e9f89245be50195d5ff8e66cc5e64c82.tar.gz
rust-73476554e9f89245be50195d5ff8e66cc5e64c82.zip
Rollup merge of #106427 - mejrs:translation_errors, r=davidtwco
Improve fluent error messages

These have been really frustrating me while migrating diagnostics.
Diffstat (limited to 'compiler/rustc_errors/src')
-rw-r--r--compiler/rustc_errors/src/emitter.rs11
-rw-r--r--compiler/rustc_errors/src/error.rs137
-rw-r--r--compiler/rustc_errors/src/json.rs9
-rw-r--r--compiler/rustc_errors/src/lib.rs17
-rw-r--r--compiler/rustc_errors/src/tests.rs188
-rw-r--r--compiler/rustc_errors/src/translation.rs117
6 files changed, 401 insertions, 78 deletions
diff --git a/compiler/rustc_errors/src/emitter.rs b/compiler/rustc_errors/src/emitter.rs
index 0ca200abe19..7f01df32101 100644
--- a/compiler/rustc_errors/src/emitter.rs
+++ b/compiler/rustc_errors/src/emitter.rs
@@ -28,6 +28,7 @@ use rustc_error_messages::{FluentArgs, SpanLabel};
 use rustc_span::hygiene::{ExpnKind, MacroKind};
 use std::borrow::Cow;
 use std::cmp::{max, min, Reverse};
+use std::error::Report;
 use std::io::prelude::*;
 use std::io::{self, IsTerminal};
 use std::iter;
@@ -250,7 +251,7 @@ pub trait Emitter: Translate {
         let mut primary_span = diag.span.clone();
         let suggestions = diag.suggestions.as_deref().unwrap_or(&[]);
         if let Some((sugg, rest)) = suggestions.split_first() {
-            let msg = self.translate_message(&sugg.msg, fluent_args);
+            let msg = self.translate_message(&sugg.msg, fluent_args).map_err(Report::new).unwrap();
             if rest.is_empty() &&
                // ^ if there is only one suggestion
                // don't display multi-suggestions as labels
@@ -1325,7 +1326,7 @@ impl EmitterWriter {
         //                very *weird* formats
         //                see?
         for (text, style) in msg.iter() {
-            let text = self.translate_message(text, args);
+            let text = self.translate_message(text, args).map_err(Report::new).unwrap();
             let lines = text.split('\n').collect::<Vec<_>>();
             if lines.len() > 1 {
                 for (i, line) in lines.iter().enumerate() {
@@ -1387,7 +1388,7 @@ impl EmitterWriter {
                 label_width += 2;
             }
             for (text, _) in msg.iter() {
-                let text = self.translate_message(text, args);
+                let text = self.translate_message(text, args).map_err(Report::new).unwrap();
                 // Account for newlines to align output to its label.
                 for (line, text) in normalize_whitespace(&text).lines().enumerate() {
                     buffer.append(
@@ -2301,7 +2302,9 @@ impl FileWithAnnotatedLines {
                     hi.col_display += 1;
                 }
 
-                let label = label.as_ref().map(|m| emitter.translate_message(m, args).to_string());
+                let label = label.as_ref().map(|m| {
+                    emitter.translate_message(m, args).map_err(Report::new).unwrap().to_string()
+                });
 
                 if lo.line != hi.line {
                     let ml = MultilineAnnotation {
diff --git a/compiler/rustc_errors/src/error.rs b/compiler/rustc_errors/src/error.rs
new file mode 100644
index 00000000000..ec0a2fe8cd8
--- /dev/null
+++ b/compiler/rustc_errors/src/error.rs
@@ -0,0 +1,137 @@
+use rustc_error_messages::{
+    fluent_bundle::resolver::errors::{ReferenceKind, ResolverError},
+    FluentArgs, FluentError,
+};
+use std::borrow::Cow;
+use std::error::Error;
+use std::fmt;
+
+#[derive(Debug)]
+pub enum TranslateError<'args> {
+    One {
+        id: &'args Cow<'args, str>,
+        args: &'args FluentArgs<'args>,
+        kind: TranslateErrorKind<'args>,
+    },
+    Two {
+        primary: Box<TranslateError<'args>>,
+        fallback: Box<TranslateError<'args>>,
+    },
+}
+
+impl<'args> TranslateError<'args> {
+    pub fn message(id: &'args Cow<'args, str>, args: &'args FluentArgs<'args>) -> Self {
+        Self::One { id, args, kind: TranslateErrorKind::MessageMissing }
+    }
+    pub fn primary(id: &'args Cow<'args, str>, args: &'args FluentArgs<'args>) -> Self {
+        Self::One { id, args, kind: TranslateErrorKind::PrimaryBundleMissing }
+    }
+    pub fn attribute(
+        id: &'args Cow<'args, str>,
+        args: &'args FluentArgs<'args>,
+        attr: &'args str,
+    ) -> Self {
+        Self::One { id, args, kind: TranslateErrorKind::AttributeMissing { attr } }
+    }
+    pub fn value(id: &'args Cow<'args, str>, args: &'args FluentArgs<'args>) -> Self {
+        Self::One { id, args, kind: TranslateErrorKind::ValueMissing }
+    }
+
+    pub fn fluent(
+        id: &'args Cow<'args, str>,
+        args: &'args FluentArgs<'args>,
+        errs: Vec<FluentError>,
+    ) -> Self {
+        Self::One { id, args, kind: TranslateErrorKind::Fluent { errs } }
+    }
+
+    pub fn and(self, fallback: TranslateError<'args>) -> TranslateError<'args> {
+        Self::Two { primary: Box::new(self), fallback: Box::new(fallback) }
+    }
+}
+
+#[derive(Debug)]
+pub enum TranslateErrorKind<'args> {
+    MessageMissing,
+    PrimaryBundleMissing,
+    AttributeMissing { attr: &'args str },
+    ValueMissing,
+    Fluent { errs: Vec<FluentError> },
+}
+
+impl fmt::Display for TranslateError<'_> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        use TranslateErrorKind::*;
+
+        match self {
+            Self::One { id, args, kind } => {
+                writeln!(f, "failed while formatting fluent string `{id}`: ")?;
+                match kind {
+                    MessageMissing => writeln!(f, "message was missing")?,
+                    PrimaryBundleMissing => writeln!(f, "the primary bundle was missing")?,
+                    AttributeMissing { attr } => {
+                        writeln!(f, "the attribute `{attr}` was missing")?;
+                        writeln!(f, "help: add `.{attr} = <message>`")?;
+                    }
+                    ValueMissing => writeln!(f, "the value was missing")?,
+                    Fluent { errs } => {
+                        for err in errs {
+                            match err {
+                                FluentError::ResolverError(ResolverError::Reference(
+                                    ReferenceKind::Message { id, .. }
+                                    | ReferenceKind::Variable { id, .. },
+                                )) => {
+                                    if args.iter().any(|(arg_id, _)| arg_id == id) {
+                                        writeln!(
+                                            f,
+                                            "argument `{id}` exists but was not referenced correctly"
+                                        )?;
+                                        writeln!(f, "help: try using `{{${id}}}` instead")?;
+                                    } else {
+                                        writeln!(
+                                            f,
+                                            "the fluent string has an argument `{id}` that was not found."
+                                        )?;
+                                        let vars: Vec<&str> =
+                                            args.iter().map(|(a, _v)| a).collect();
+                                        match &*vars {
+                                            [] => writeln!(f, "help: no arguments are available")?,
+                                            [one] => writeln!(
+                                                f,
+                                                "help: the argument `{one}` is available"
+                                            )?,
+                                            [first, middle @ .., last] => {
+                                                write!(f, "help: the arguments `{first}`")?;
+                                                for a in middle {
+                                                    write!(f, ", `{a}`")?;
+                                                }
+                                                writeln!(f, " and `{last}` are available")?;
+                                            }
+                                        }
+                                    }
+                                }
+                                _ => writeln!(f, "{err}")?,
+                            }
+                        }
+                    }
+                }
+            }
+            // If someone cares about primary bundles, they'll probably notice it's missing
+            // regardless or will be using `debug_assertions`
+            // so we skip the arm below this one to avoid confusing the regular user.
+            Self::Two { primary: box Self::One { kind: PrimaryBundleMissing, .. }, fallback } => {
+                fmt::Display::fmt(fallback, f)?;
+            }
+            Self::Two { primary, fallback } => {
+                writeln!(
+                    f,
+                    "first, fluent formatting using the primary bundle failed:\n {primary}\n \
+                    while attempting to recover by using the fallback bundle instead, another error occurred:\n{fallback}"
+                )?;
+            }
+        }
+        Ok(())
+    }
+}
+
+impl Error for TranslateError<'_> {}
diff --git a/compiler/rustc_errors/src/json.rs b/compiler/rustc_errors/src/json.rs
index a37073d8fa3..dc38b8725ad 100644
--- a/compiler/rustc_errors/src/json.rs
+++ b/compiler/rustc_errors/src/json.rs
@@ -24,6 +24,7 @@ use rustc_data_structures::sync::Lrc;
 use rustc_error_messages::FluentArgs;
 use rustc_span::hygiene::ExpnData;
 use rustc_span::Span;
+use std::error::Report;
 use std::io::{self, Write};
 use std::path::Path;
 use std::sync::{Arc, Mutex};
@@ -321,7 +322,8 @@ impl Diagnostic {
     fn from_errors_diagnostic(diag: &crate::Diagnostic, je: &JsonEmitter) -> Diagnostic {
         let args = to_fluent_args(diag.args());
         let sugg = diag.suggestions.iter().flatten().map(|sugg| {
-            let translated_message = je.translate_message(&sugg.msg, &args);
+            let translated_message =
+                je.translate_message(&sugg.msg, &args).map_err(Report::new).unwrap();
             Diagnostic {
                 message: translated_message.to_string(),
                 code: None,
@@ -411,7 +413,10 @@ impl DiagnosticSpan {
         Self::from_span_etc(
             span.span,
             span.is_primary,
-            span.label.as_ref().map(|m| je.translate_message(m, args)).map(|m| m.to_string()),
+            span.label
+                .as_ref()
+                .map(|m| je.translate_message(m, args).unwrap())
+                .map(|m| m.to_string()),
             suggestion,
             je,
         )
diff --git a/compiler/rustc_errors/src/lib.rs b/compiler/rustc_errors/src/lib.rs
index 66e5c41f56c..535812fb0e2 100644
--- a/compiler/rustc_errors/src/lib.rs
+++ b/compiler/rustc_errors/src/lib.rs
@@ -11,6 +11,10 @@
 #![feature(never_type)]
 #![feature(result_option_inspect)]
 #![feature(rustc_attrs)]
+#![feature(yeet_expr)]
+#![feature(try_blocks)]
+#![feature(box_patterns)]
+#![feature(error_reporter)]
 #![allow(incomplete_features)]
 
 #[macro_use]
@@ -41,6 +45,7 @@ use rustc_span::HashStableContext;
 use rustc_span::{Loc, Span};
 
 use std::borrow::Cow;
+use std::error::Report;
 use std::fmt;
 use std::hash::Hash;
 use std::num::NonZeroUsize;
@@ -54,11 +59,14 @@ mod diagnostic;
 mod diagnostic_builder;
 mod diagnostic_impls;
 pub mod emitter;
+pub mod error;
 pub mod json;
 mod lock;
 pub mod registry;
 mod snippet;
 mod styled_buffer;
+#[cfg(test)]
+mod tests;
 pub mod translation;
 
 pub use diagnostic_builder::IntoDiagnostic;
@@ -616,7 +624,14 @@ impl Handler {
     ) -> SubdiagnosticMessage {
         let inner = self.inner.borrow();
         let args = crate::translation::to_fluent_args(args);
-        SubdiagnosticMessage::Eager(inner.emitter.translate_message(&message, &args).to_string())
+        SubdiagnosticMessage::Eager(
+            inner
+                .emitter
+                .translate_message(&message, &args)
+                .map_err(Report::new)
+                .unwrap()
+                .to_string(),
+        )
     }
 
     // This is here to not allow mutation of flags;
diff --git a/compiler/rustc_errors/src/tests.rs b/compiler/rustc_errors/src/tests.rs
new file mode 100644
index 00000000000..52103e46097
--- /dev/null
+++ b/compiler/rustc_errors/src/tests.rs
@@ -0,0 +1,188 @@
+use crate::error::{TranslateError, TranslateErrorKind};
+use crate::fluent_bundle::*;
+use crate::translation::Translate;
+use crate::FluentBundle;
+use rustc_data_structures::sync::Lrc;
+use rustc_error_messages::fluent_bundle::resolver::errors::{ReferenceKind, ResolverError};
+use rustc_error_messages::langid;
+use rustc_error_messages::DiagnosticMessage;
+
+struct Dummy {
+    bundle: FluentBundle,
+}
+
+impl Translate for Dummy {
+    fn fluent_bundle(&self) -> Option<&Lrc<FluentBundle>> {
+        None
+    }
+
+    fn fallback_fluent_bundle(&self) -> &FluentBundle {
+        &self.bundle
+    }
+}
+
+fn make_dummy(ftl: &'static str) -> Dummy {
+    let resource = FluentResource::try_new(ftl.into()).expect("Failed to parse an FTL string.");
+
+    let langid_en = langid!("en-US");
+
+    #[cfg(parallel_compiler)]
+    let mut bundle = FluentBundle::new_concurrent(vec![langid_en]);
+
+    #[cfg(not(parallel_compiler))]
+    let mut bundle = FluentBundle::new(vec![langid_en]);
+
+    bundle.add_resource(resource).expect("Failed to add FTL resources to the bundle.");
+
+    Dummy { bundle }
+}
+
+#[test]
+fn wellformed_fluent() {
+    let dummy = make_dummy("mir_build_borrow_of_moved_value = borrow of moved value
+    .label = value moved into `{$name}` here
+    .occurs_because_label = move occurs because `{$name}` has type `{$ty}` which does not implement the `Copy` trait
+    .value_borrowed_label = value borrowed here after move
+    .suggestion = borrow this binding in the pattern to avoid moving the value");
+
+    let mut args = FluentArgs::new();
+    args.set("name", "Foo");
+    args.set("ty", "std::string::String");
+    {
+        let message = DiagnosticMessage::FluentIdentifier(
+            "mir_build_borrow_of_moved_value".into(),
+            Some("suggestion".into()),
+        );
+
+        assert_eq!(
+            dummy.translate_message(&message, &args).unwrap(),
+            "borrow this binding in the pattern to avoid moving the value"
+        );
+    }
+
+    {
+        let message = DiagnosticMessage::FluentIdentifier(
+            "mir_build_borrow_of_moved_value".into(),
+            Some("value_borrowed_label".into()),
+        );
+
+        assert_eq!(
+            dummy.translate_message(&message, &args).unwrap(),
+            "value borrowed here after move"
+        );
+    }
+
+    {
+        let message = DiagnosticMessage::FluentIdentifier(
+            "mir_build_borrow_of_moved_value".into(),
+            Some("occurs_because_label".into()),
+        );
+
+        assert_eq!(
+            dummy.translate_message(&message, &args).unwrap(),
+            "move occurs because `\u{2068}Foo\u{2069}` has type `\u{2068}std::string::String\u{2069}` which does not implement the `Copy` trait"
+        );
+
+        {
+            let message = DiagnosticMessage::FluentIdentifier(
+                "mir_build_borrow_of_moved_value".into(),
+                Some("label".into()),
+            );
+
+            assert_eq!(
+                dummy.translate_message(&message, &args).unwrap(),
+                "value moved into `\u{2068}Foo\u{2069}` here"
+            );
+        }
+    }
+}
+
+#[test]
+fn misformed_fluent() {
+    let dummy = make_dummy("mir_build_borrow_of_moved_value = borrow of moved value
+    .label = value moved into `{name}` here
+    .occurs_because_label = move occurs because `{$oops}` has type `{$ty}` which does not implement the `Copy` trait
+    .suggestion = borrow this binding in the pattern to avoid moving the value");
+
+    let mut args = FluentArgs::new();
+    args.set("name", "Foo");
+    args.set("ty", "std::string::String");
+    {
+        let message = DiagnosticMessage::FluentIdentifier(
+            "mir_build_borrow_of_moved_value".into(),
+            Some("value_borrowed_label".into()),
+        );
+
+        let err = dummy.translate_message(&message, &args).unwrap_err();
+        assert!(
+            matches!(
+                &err,
+                TranslateError::Two {
+                    primary: box TranslateError::One {
+                        kind: TranslateErrorKind::PrimaryBundleMissing,
+                        ..
+                    },
+                    fallback: box TranslateError::One {
+                        kind: TranslateErrorKind::AttributeMissing { attr: "value_borrowed_label" },
+                        ..
+                    }
+                }
+            ),
+            "{err:#?}"
+        );
+        assert_eq!(
+            format!("{err}"),
+            "failed while formatting fluent string `mir_build_borrow_of_moved_value`: \nthe attribute `value_borrowed_label` was missing\nhelp: add `.value_borrowed_label = <message>`\n"
+        );
+    }
+
+    {
+        let message = DiagnosticMessage::FluentIdentifier(
+            "mir_build_borrow_of_moved_value".into(),
+            Some("label".into()),
+        );
+
+        let err = dummy.translate_message(&message, &args).unwrap_err();
+        if let TranslateError::Two {
+            primary: box TranslateError::One { kind: TranslateErrorKind::PrimaryBundleMissing, .. },
+            fallback: box TranslateError::One { kind: TranslateErrorKind::Fluent { errs }, .. },
+        } = &err
+            && let [FluentError::ResolverError(ResolverError::Reference(
+                ReferenceKind::Message { id, .. }
+                    | ReferenceKind::Variable { id, .. },
+            ))] = &**errs
+            && id == "name"
+        {} else {
+            panic!("{err:#?}")
+        };
+        assert_eq!(
+            format!("{err}"),
+            "failed while formatting fluent string `mir_build_borrow_of_moved_value`: \nargument `name` exists but was not referenced correctly\nhelp: try using `{$name}` instead\n"
+        );
+    }
+
+    {
+        let message = DiagnosticMessage::FluentIdentifier(
+            "mir_build_borrow_of_moved_value".into(),
+            Some("occurs_because_label".into()),
+        );
+
+        let err = dummy.translate_message(&message, &args).unwrap_err();
+        if let TranslateError::Two {
+            primary: box TranslateError::One { kind: TranslateErrorKind::PrimaryBundleMissing, .. },
+            fallback: box TranslateError::One { kind: TranslateErrorKind::Fluent { errs }, .. },
+        } = &err
+            && let [FluentError::ResolverError(ResolverError::Reference(
+                ReferenceKind::Message { id, .. }
+                    | ReferenceKind::Variable { id, .. },
+            ))] = &**errs
+            && id == "oops"
+        {} else {
+            panic!("{err:#?}")
+        };
+        assert_eq!(
+            format!("{err}"),
+            "failed while formatting fluent string `mir_build_borrow_of_moved_value`: \nthe fluent string has an argument `oops` that was not found.\nhelp: the arguments `name` and `ty` are available\n"
+        );
+    }
+}
diff --git a/compiler/rustc_errors/src/translation.rs b/compiler/rustc_errors/src/translation.rs
index afd660ff1bf..addfc9726ca 100644
--- a/compiler/rustc_errors/src/translation.rs
+++ b/compiler/rustc_errors/src/translation.rs
@@ -1,11 +1,10 @@
+use crate::error::TranslateError;
 use crate::snippet::Style;
 use crate::{DiagnosticArg, DiagnosticMessage, FluentBundle};
 use rustc_data_structures::sync::Lrc;
-use rustc_error_messages::{
-    fluent_bundle::resolver::errors::{ReferenceKind, ResolverError},
-    FluentArgs, FluentError,
-};
+use rustc_error_messages::FluentArgs;
 use std::borrow::Cow;
+use std::error::Report;
 
 /// Convert diagnostic arguments (a rustc internal type that exists to implement
 /// `Encodable`/`Decodable`) into `FluentArgs` which is necessary to perform translation.
@@ -46,7 +45,10 @@ pub trait Translate {
         args: &FluentArgs<'_>,
     ) -> Cow<'_, str> {
         Cow::Owned(
-            messages.iter().map(|(m, _)| self.translate_message(m, args)).collect::<String>(),
+            messages
+                .iter()
+                .map(|(m, _)| self.translate_message(m, args).map_err(Report::new).unwrap())
+                .collect::<String>(),
         )
     }
 
@@ -55,83 +57,56 @@ pub trait Translate {
         &'a self,
         message: &'a DiagnosticMessage,
         args: &'a FluentArgs<'_>,
-    ) -> Cow<'_, str> {
+    ) -> Result<Cow<'_, str>, TranslateError<'_>> {
         trace!(?message, ?args);
         let (identifier, attr) = match message {
             DiagnosticMessage::Str(msg) | DiagnosticMessage::Eager(msg) => {
-                return Cow::Borrowed(msg);
+                return Ok(Cow::Borrowed(msg));
             }
             DiagnosticMessage::FluentIdentifier(identifier, attr) => (identifier, attr),
         };
+        let translate_with_bundle =
+            |bundle: &'a FluentBundle| -> Result<Cow<'_, str>, TranslateError<'_>> {
+                let message = bundle
+                    .get_message(identifier)
+                    .ok_or(TranslateError::message(identifier, args))?;
+                let value = match attr {
+                    Some(attr) => message
+                        .get_attribute(attr)
+                        .ok_or(TranslateError::attribute(identifier, args, attr))?
+                        .value(),
+                    None => message.value().ok_or(TranslateError::value(identifier, args))?,
+                };
+                debug!(?message, ?value);
 
-        let translate_with_bundle = |bundle: &'a FluentBundle| -> Option<(Cow<'_, str>, Vec<_>)> {
-            let message = bundle.get_message(identifier)?;
-            let value = match attr {
-                Some(attr) => message.get_attribute(attr)?.value(),
-                None => message.value()?,
+                let mut errs = vec![];
+                let translated = bundle.format_pattern(value, Some(args), &mut errs);
+                debug!(?translated, ?errs);
+                if errs.is_empty() {
+                    Ok(translated)
+                } else {
+                    Err(TranslateError::fluent(identifier, args, errs))
+                }
             };
-            debug!(?message, ?value);
-
-            let mut errs = vec![];
-            let translated = bundle.format_pattern(value, Some(args), &mut errs);
-            debug!(?translated, ?errs);
-            Some((translated, errs))
-        };
 
-        self.fluent_bundle()
-            .and_then(|bundle| translate_with_bundle(bundle))
-            // If `translate_with_bundle` returns `None` with the primary bundle, this is likely
-            // just that the primary bundle doesn't contain the message being translated, so
-            // proceed to the fallback bundle.
-            //
-            // However, when errors are produced from translation, then that means the translation
-            // is broken (e.g. `{$foo}` exists in a translation but `foo` isn't provided).
-            //
-            // In debug builds, assert so that compiler devs can spot the broken translation and
-            // fix it..
-            .inspect(|(_, errs)| {
-                debug_assert!(
-                    errs.is_empty(),
-                    "identifier: {:?}, attr: {:?}, args: {:?}, errors: {:?}",
-                    identifier,
-                    attr,
-                    args,
-                    errs
-                );
-            })
-            // ..otherwise, for end users, an error about this wouldn't be useful or actionable, so
-            // just hide it and try with the fallback bundle.
-            .filter(|(_, errs)| errs.is_empty())
-            .or_else(|| translate_with_bundle(self.fallback_fluent_bundle()))
-            .map(|(translated, errs)| {
-                // Always bail out for errors with the fallback bundle.
+        try {
+            match self.fluent_bundle().map(|b| translate_with_bundle(b)) {
+                // The primary bundle was present and translation succeeded
+                Some(Ok(t)) => t,
 
-                let mut help_messages = vec![];
+                // Always yeet out for errors on debug
+                Some(Err(primary)) if cfg!(debug_assertions) => do yeet primary,
 
-                if !errs.is_empty() {
-                    for error in &errs {
-                        match error {
-                            FluentError::ResolverError(ResolverError::Reference(
-                                ReferenceKind::Message { id, .. },
-                            )) if args.iter().any(|(arg_id, _)| arg_id == id) => {
-                                help_messages.push(format!("Argument `{id}` exists but was not referenced correctly. Try using `{{${id}}}` instead"));
-                            }
-                            _ => {}
-                        }
-                    }
+                // If `translate_with_bundle` returns `Err` with the primary bundle, this is likely
+                // just that the primary bundle doesn't contain the message being translated or
+                // something else went wrong) so proceed to the fallback bundle.
+                Some(Err(primary)) => translate_with_bundle(self.fallback_fluent_bundle())
+                    .map_err(|fallback| primary.and(fallback))?,
 
-                    panic!(
-                        "Encountered errors while formatting message for `{identifier}`\n\
-                        help: {}\n\
-                        attr: `{attr:?}`\n\
-                        args: `{args:?}`\n\
-                        errors: `{errs:?}`",
-                        help_messages.join("\nhelp: ")
-                    );
-                }
-
-                translated
-            })
-            .expect("failed to find message in primary or fallback fluent bundles")
+                // The primary bundle is missing, proceed to the fallback bundle
+                None => translate_with_bundle(self.fallback_fluent_bundle())
+                    .map_err(|fallback| TranslateError::primary(identifier, args).and(fallback))?,
+            }
+        }
     }
 }