Merged all of the Fluent errors into the fluent::Error
module, and added error handling for:
en-US
)2SITVDYW6KANM24QXRHVSBL6S77UHKJLOSOHSUZQBJFL5NAAGQYAC
QFPQZR4K4UZ7R2GQZJG4NYBGVQJVL2ANIKGGTOHAMIRIBQHPSQGAC
F5LG7WENUUDRSCTDMA4M6BAC5RWTGQO45C4ZEBZDX6FHCTTHBVGQC
O77KA6C4UJGZXVGPEA7WCRQH6XYQJPWETSPDXI3VOKOSRQND7JEQC
NO3PDO7PY7J3WPADNCS5VD6HKFY63E23I3SDR4DHXNVQJTG27RAAC
5TEX4MNUC4LDDRMNEOVCFNUUEZAGUXMKO3OIEQFXWRQKXSHY2NRQC
ROSR4HD5ENPQU3HH5IVYSOA5YM72W77CHVQARSD3T67BUNYG7KZQC
2XQ6ZB4WZNNR4KNC3VWNTV7IRMGGAEP33JPQUVB3CVWAKHECZVRQC
SHNZZSZGIBTTD4IV5SMW5BIN5DORUWQVTVTNB5RMRD5CTFNOMJ6AC
InvalidReference(#[from] ast::InvalidReference),
#[error("the reference `${fluent_name}` doesn't match a Rust field `{rust_name}`")]
InvalidReference {
fluent_name: String,
rust_name: String,
#[source_code]
source_code: NamedSource<String>,
#[label("This references `{rust_name}` which doesn't exist")]
span: SourceSpan,
#[help]
valid_references: String,
},
#[error("missing canonical locale `{canonical_locale}`")]
MissingCanonicalLocale {
canonical_locale: String,
#[help]
matched_locales: String,
},
#[error(r#"message "{unexpected_key}" from `{locale}` is not in the canonical `{canonical_locale}` locale"#)]
#[help("the canonical locale must include all keys!")]
UnexpectedKey {
unexpected_key: String,
locale: String,
canonical_locale: String,
#[source_code]
source_code: NamedSource<String>,
#[label("This key isn't in the `{canonical_locale}` locale")]
span: SourceSpan,
},
#[error("unable to parse Fluent source code")]
ParserErrors {
#[source_code]
source_code: NamedSource<String>,
#[related]
related: Vec<ParserError>,
},
// TODO: return an error instead of panic
let syntax_tree = fluent_syntax::parser::parse(file_contents).unwrap();
let syntax_tree = match fluent_syntax::parser::parse(file_contents.clone()) {
Ok(syntax_tree) => syntax_tree,
Err((_partial_syntax_tree, parser_errors)) => {
// Map the `fluent_syntax` errors to `miette::Diagnostic` errors
let related = parser_errors
.into_iter()
.map(|error| ParserError {
span: SourceSpan::from(error.pos),
kind: error.kind,
})
.collect();
return Err(Error::ParserErrors {
source_code: NamedSource::new(path.to_string_lossy(), file_contents),
related,
});
}
};
}
/// Calculate the byte offset of the serialized
fn source_with_message_offset(&self, id: &str) -> (NamedSource<String>, usize) {
// Find the message position in the AST
let ast_index = self
.syntax_tree
.body
.iter()
.position(|entry| {
if let Entry::Message(message) = entry {
message.id.name == id
} else {
false
}
})
.unwrap();
let options = fluent_syntax::serializer::Options {
// Make sure to include all source code in error snippet, even if marked as "junk"
with_junk: true,
};
// Serialize everything before this message to get the byte offset
let source_before_message = fluent_syntax::serializer::serialize_with_options(
&Resource {
body: self.syntax_tree.body[0..ast_index].to_vec(),
},
options,
);
let byte_offset = source_before_message.len();
let source_after_offset = fluent_syntax::serializer::serialize_with_options(
&Resource {
body: self.syntax_tree.body[ast_index..].to_vec(),
},
options,
);
let source = format!("{source_before_message}{source_after_offset}");
let named_source = NamedSource::new(self.path.to_string_lossy(), source);
(named_source, byte_offset)
// Collect all keys used in the Fluent source code
let keys: HashSet<&str> =
HashSet::from_iter(locales.values().flat_map(|resource| resource.message_ids()));
// Collect all keys from the canonical resource
// TODO: return an error instead of panic
let canonical_resource = locales.get(&canonical_locale).unwrap();
// TODO: return an error instead of panic
// Make sure the canonical locale exists
let canonical_resource = if let Some(canonical_resource) = locales.get(&canonical_locale) {
canonical_resource
} else {
return Err(Error::MissingCanonicalLocale {
canonical_locale: canonical_locale.to_string(),
matched_locales: format!(
"the following locales were found:\n{}",
locales
.keys()
.map(|locale| format!("- {locale}"))
.collect::<Vec<_>>()
.join("\n")
),
});
};
assert_eq!(keys, HashSet::from_iter(canonical_resource.message_ids()));
let canonical_keys: Vec<&str> = canonical_resource.message_ids().collect();
for (locale, source_file) in &locales {
// Find any keys in `locale_keys` missing from `canonical_keys`
let mut unexpected_keys = source_file
.message_ids()
.filter(|id: &&str| !canonical_keys.contains(id));
if let Some(unexpected_key) = unexpected_keys.next() {
let (src, message_offset) = source_file.source_with_message_offset(unexpected_key);
return Err(Error::UnexpectedKey {
unexpected_key: unexpected_key.to_string(),
locale: locale.to_string(),
canonical_locale: canonical_locale.to_string(),
source_code: src,
span: SourceSpan::new(message_offset.into(), unexpected_key.len()),
});
}
}
// Serialize the Fluent file AST back into a String
let source_string = fluent_syntax::serializer::serialize_with_options(
&message_context.syntax_tree,
fluent_syntax::serializer::Options {
// Make sure to include all source code in error snippet, even if marked as "junk"
with_junk: true,
},
);
let (source_code, offset) = message_context
.source
.source_with_message_offset(message_context.root_id);
return Err(Error::InvalidReference(InvalidReference {
src: NamedSource::new(message_context.path, source_string.clone()),
span: (location..location + id.name.len()).into(),
help: format!(
return Err(Error::InvalidReference {
fluent_name: id.name.clone(),
rust_name: id.name.to_snake_case(),
source_code,
span: SourceSpan::new(location.into(), id.name.len()),
valid_references: format!(