-
Notifications
You must be signed in to change notification settings - Fork 97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fluent × SNAFU collaboration #107
Comments
Yeah, that sounds like a great use of Fluent! For lower-level command-line with no events and no runtime-locale-switches, all you should need is a macro on top of |
@shepmaster You might be interested in my |
Somewhat related, see discussion of Fluent for localization of Clap: clap-rs/clap#380 (comment) |
@shepmaster Are you still interested in this? We're in a position to actually facilitate contributions moving forward in this project now. I also know there has been some progress in building localization into Clap and an |
I am indeed still interested! Since my original post, the syntax of SNAFU hasn't drastically changed, so an error still is defined something like: use snafu::prelude::*;
#[derive(Debug, Snafu)]
#[snafu(display("The example failed for user {name}"))]
struct ExampleError {
source: std::io::Error,
name: String,
}
fn demo() -> Result<(), ExampleError> {
std::fs::read_to_string("/etc/hosts").context(ExampleSnafu { name: "viv" })?;
Ok(())
} Among other things, this expands into a impl fmt::Display for ExampleError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self { name, source } => {
write!(f, "The example failed for user {name}")
}
}
}
} I guess my biggest mental block is how to connect a user's language preference to the
This seems to indicate that Unfortunately, we can only downcast an error to a concrete type, not to a trait, so it's not possible to create a sibling trait (e.g. It may be possible to use generic member access somehow here, but I'm not quite sure how. Beyond that, I don't have experience with the mechanics of defining Fluent translations and how they get into the binary, so it's possible that some amount of coordination would need to be done by the SNAFU macro. |
Initial thoughts re...
|
I played around with this a bit, and I think we can have something reasonably nice if Fluent would consider adding a few traits and types and a nightly unstable feature were to stabilize. Here's a code dump with some explanatory comments: #![feature(error_generic_member_access)]
use fluent::{FluentArgs, FluentBundle, FluentError, FluentResource};
use snafu::prelude::*;
use std::{borrow::Borrow, error, fmt};
use unic_langid::langid;
// Ideally, we could reduce the user's API surface area down to one
// attribute that defines what the message ID would be. Attributes may
// also need to be added to individual fields to indicate which fields
// should be available to Fluent.
// #[snafu(fluent("hello-world"))]
// struct ExampleError {
// source: std::io::Error,
// #[snafu(fluent)]
// name: String,
// }
#[derive(Debug, Snafu)]
#[snafu(display("The example failed for user {name}"))]
// This uses the generic member access feature to return a trait
// object. It's explicit here, but would by implied by the theoretical
// `snafu(fluent)` attribute above.
#[snafu(provide(ref, dyn FluentDisplay => self))]
struct ExampleError {
source: std::io::Error,
name: String,
}
// This code would be generated by the SNAFU macros
impl FluentDisplay for ExampleError {
fn fmt(&self, f: &mut dyn FluentFormatter) -> Result<(), FormatterError> {
f.write_message("hello-world", {
let mut args = FluentArgs::new();
args.set("name", &self.name);
args
})
}
}
// These traits are the trickiest part...
//
// - In order to be able to create a `dyn FluentDisplay`, the trait
// must have no generics. However, `FluentBundle` is a generic type.
//
// - The decision of which bundle to use should come from outside of
// the thing being formatted.
//
// Those constraints lead to this dual trait solution.
//
// For maximum usefulness, these traits (or something similar) should
// be a part of Fluent. If they were a part of SNAFU, then the
// ecosystem at large couldn't benefit from them and there would be
// greatly decreased interoperability.
trait FluentDisplay {
fn fmt(&self, f: &mut dyn FluentFormatter) -> Result<(), FormatterError>;
}
trait FluentFormatter {
fn write_message(&mut self, id: &str, args: FluentArgs) -> Result<(), FormatterError>;
}
// I used SNAFU to define this, but that couldn't happen for real as
// it would result in circular dependencies :-)
#[derive(Debug, Snafu)]
#[snafu(module)]
enum FormatterError {
#[snafu(display("Fluent message `{id}` does not exist"))]
MessageDoesNotExist { id: String },
#[snafu(display("Fluent message `{id}` has no value"))]
MessageHasNoValue { id: String },
#[snafu(display("Could not format the Fluent message"))]
Formatting { source: fmt::Error },
#[snafu(display("Internal Fluent errors occurred"))]
Fluent { errors: Vec<FluentError> },
}
// This is a demonstration implementation of `FluentFormatter` and
// this implementation (or something similar) should probably live
// alongside the trait definition.
struct Formatter<'a, R, W> {
bundle: &'a FluentBundle<R>,
output: W,
}
impl<'a, R, W> Formatter<'a, R, W>
where
R: Borrow<FluentResource>,
W: fmt::Write,
{
fn new(bundle: &'a FluentBundle<R>, output: W) -> Self {
Self { bundle, output }
}
fn into_inner(self) -> W {
self.output
}
}
impl<R, W> FluentFormatter for Formatter<'_, R, W>
where
R: Borrow<FluentResource>,
W: fmt::Write,
{
fn write_message(&mut self, id: &str, args: FluentArgs) -> Result<(), FormatterError> {
use formatter_error::*;
let msg = self
.bundle
.get_message(id)
.context(MessageDoesNotExistSnafu { id })?;
let pattern = msg
.value() // TODO: PR about this as the docs are wrong.
.context(MessageHasNoValueSnafu { id })?;
let args = Some(&args);
let mut errors = vec![];
self.bundle
.write_pattern(&mut self.output, pattern, args, &mut errors)
.context(FormattingSnafu)?;
ensure!(errors.is_empty(), FluentSnafu { errors });
Ok(())
}
}
// An example that fails
fn demo() -> Result<(), ExampleError> {
std::fs::read_to_string("/no/no/no").context(ExampleSnafu { name: "viv" })?;
Ok(())
}
fn main() {
// Set up our dummy bundle
let bundle = {
let ftl_string = "hello-world = The example failed for user { $name }".to_owned();
let res = FluentResource::try_new(ftl_string).expect("Failed to parse an FTL string.");
let langid_en = langid!("en-US");
let mut bundle = FluentBundle::new(vec![langid_en]);
bundle
.add_resource(res)
.expect("Failed to add FTL resources to the bundle.");
bundle
};
let error = demo().unwrap_err();
let as_display = error::request_ref::<dyn FluentDisplay>(&error).unwrap();
let mut m = Formatter::new(&bundle, String::new());
as_display.fmt(&mut m).unwrap();
let value = m.into_inner();
assert!(value.contains("The example failed"), "was: {value}");
println!(
"{}",
FluentReport {
bundle: &bundle,
error: &error
}
);
}
// SNAFU would probably add some functionality akin to this to format
// error messages nicely for end users.
struct FluentReport<'a, R> {
bundle: &'a FluentBundle<R>,
error: &'a (dyn error::Error + 'static),
}
impl<R> fmt::Display for FluentReport<'_, R>
where
R: Borrow<FluentResource>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut head = Some(self.error);
let mut idx = 0;
while let Some(error) = head {
head = error.source();
idx += 1;
write!(f, "{idx}: ")?;
match error::request_ref::<dyn FluentDisplay>(error) {
Some(fluent_error) => {
let mut fluent_f = Formatter::new(self.bundle, &mut *f);
if let Err(e) = fluent_error.fmt(&mut fluent_f) {
fmt::Display::fmt(error, f)?;
fmt::Display::fmt(&e, f)?;
}
}
None => {
fmt::Display::fmt(error, f)?;
}
}
writeln!(f)?;
}
Ok(())
}
} TL;DR, what's your gut reaction on adding the |
I haven't had a chance to run or fiddle with your code dumps yet, but my gut reaction is quite positive. I don't think this runs afoul of anything we need to watch out for: not breaking the existing API surface area if there isn't a very compelling reason to do so, not regressing any performance, not forcefully exposing folks to dependencies they may not like, etc. If we need to we can start with additional traits behind a feature flag, especially for anything that needs nightly. We definitely need to go easy or our MSRV. On the flip side of watching out for anything that regresses existing use cases, making things more ergonomic and enabling new use cases like this is definitely a plus. |
The good news is that the nightly code would be constrained to SNAFU — Fluent should just need to gain some new traits and types. I haven't tested, but I don't see anything obvious about the traits that would cause them to require an extremely modern version of Rust. Would you like me to assemble a PR adding these traits and types to have a place for detailed discussion? |
Yes please! |
Which crate would you expect
However, these traits feels like they could be one of those higher level APIs. I'll start by adding it to fluent directly, but I'm happy to put it wherever seems more appropriate. |
I've opened #361 |
Sorry I'm a bit behind here. My kids and I got sick and nothing is getting done right :-( I don't have a good feel for where these traits should go. I've been rather baffled by the |
Given my own lack of clarity on the scope of each crate, I took a stab in #359 and redoing the summaries of each one. The docs have been terrible on this point sometimes having specific descriptions, sometimes just parroting the main project talking points. This is particularly unhelpful on crates.io when searching for crates because several different crates that have identical summaries is useless, and the readmes just having boiler plate doesn't help understand the specific crate you are looking at. I don't know if my understanding is right, but any feedback on whether the wording is helpful to clarify what might be found where would be appreciated. |
Howdy! I'm the author of SNAFU an error type library. I think it would be very powerful to be able to use Fluent to enhance error types with localized error messages.
An error type enhanced with SNAFU looks something like this:
This implements all the appropriate error traits, but
Display
is hard-coded to whatever the programmer typed.In my head, I'm wondering if the two crates could be combined to create something used like this:
Highlights:
fluent
procedural macro identifies the key of the error and implements a newIntoFluent
trait.HashMap
of properties and the key.IntoFluent
, using the translation available from fluent or falling back to the built-inDisplay
implementation.For Fluent's side, I think that everything I just described should be agnostic of the error library. I believe that SNAFU would just be a great fit for making use of it!
The text was updated successfully, but these errors were encountered: