1
Fork 0

Improve fluent error messages

This commit is contained in:
mejrs 2023-01-05 03:39:07 +01:00
parent 3b1c8a94a4
commit 0b5d6ae5db
3 changed files with 183 additions and 70 deletions

View file

@ -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.
@ -63,75 +62,50 @@ pub trait Translate {
}
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()?,
};
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.
let mut help_messages = vec![];
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"));
}
_ => {}
}
}
panic!(
"Encountered errors while formatting message for `{identifier}`\n\
help: {}\n\
attr: `{attr:?}`\n\
args: `{args:?}`\n\
errors: `{errs:?}`",
help_messages.join("\nhelp: ")
);
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))
}
};
translated
})
let ret: Result<Cow<'_, str>, TranslateError<'_>> = try {
match self.fluent_bundle().map(|b| translate_with_bundle(b)) {
// The primary bundle was present and translation succeeded
Some(Ok(t)) => t,
// Always yeet out for errors on debug
Some(Err(primary)) if cfg!(debug_assertions) => do yeet primary,
// 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))?,
// 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))?,
}
};
ret.map_err(Report::new)
.expect("failed to find message in primary or fallback fluent bundles")
}
}