Fluent-rs 0.1.0 released - Modern localization library from Mozilla

Hi all!

We're excited to announce the first alpha release of Fluent library in Rust!

Fluent is a modern localization system designed by Mozilla. We're fairly inexperienced in Rust so any feedback on our use of Rust and ergonomics welcomed!

https://github.com/projectfluent/fluent-rs

15 Likes

It's really nice to see we're getting localization libraries in Rust. This is something we could really do with at work! I had a quick flick through your docs and while overall it looks really good, there are a couple places where you can make it more idiomatic.


I'm curious why you use a stringly-typed API for adding new messages (fn add_messages(&mut self, source: &str)) instead of something more strongly typed? Stringly-typed APIs tend to be a bit of an anti-pattern in Rust because you're deferring a lot of error checking until runtime when it could be easily caught early on or even at compile time.

I'm not overly familiar with the fluent's FTL syntax, but a more idiomatic version could look somewhat like this:

pub enum FtlMessage<'a> {
  /// A bare string, e.g. "Hello World!".
  Raw(&'a str),
  /// A compiled template and the keys which should be substituted in. 
  Templated(CompiledTemplate<'a>),
  ...
}

// so we can use `str.parse()` to construct a `FtlMessage`.
impl<'a> FromStr for FtlMessage<'a> {
  ...
}

impl MessageContext {
  ...

  fn add_message(&mut self, key: &str, value: FtlMessage) {
    ...
  }
}

That way you are only ever accepting valid messages and errors won't be silently ignored.

You can make add_message() more ergonomic by adding some IntoFtlMessage trait and accepting anything that can be turned into a FtlMessage. Although that means you'll probably want to return a Result in case users pass in garbage.

You may also want to add a macro to let people do arbitrary string formatting, similar to the builtin format!() trait. You can also use the stringify!() macro under the hood to make it look like keyword parameters are a thing. Your format() function should also return some kind of Result so we know why it failed (message doesn't exist, incorrect template parameter provided, etc) instead of just an Option.

let mut ctx = MessageContext::new(&["en-US"]);
ctx.add_messages("intro = Welcome, { $name }.");

let got = fluent!(ctx, "intro", name="John").unwrap();
assert_eq!(value, "Welcome, John.");
3 Likes

I've noticed that too, but then realized it's probably not worth having messages "baked in" into the executable:

  • You'd compile in all messages from all locales, rather than just the current locale.
  • It's valuable to allow translators update the text without recompilation.

So in practice messages will be loaded from an external file anyway.

1 Like

If that is the case, wouldn't just you have a MessageContext::from_reader() constructor then?

1 Like

Thanks for the feedback!

Our reasoning is similar to what @kornel described, but the exact source of the string is purposefully left open. Depending on your particular app and environment, you may store the FTL resource as a file (most common I guess), or load it via some stream/http channel, or load it from a database.

I guess since our first implementation was in JavaScript it was just easy to assume that we'll load an FTL string.
But I agree that add_messages should return a Result and formatting should return a Result as well.

Please, let us know if you have more feedback! I'll be filing issues in our github on all things you listed so far, and we'll triage them for the next release.

Our hope is to move to rust implementation as our reference one over the next months :slight_smile:

6 Likes

One of our products at work needs to deal with the fact that not all customers speak English, so I'll need to translations for various error and status messages. When you create a MessageContext you need to pass in specifiers like en-US... Does that mean I'd be able to use the fluent crate to help with translations?