es-fluent 0.2.4

The es-fluent crate
Documentation

es-fluent

Derive macros and utilities for authoring strongly-typed messages with Project Fluent.

This crate gives you:

  • Derives to turn enums/structs into Fluent message IDs and arguments.
  • A simple API to format values for Fluent and convert them into strings.
  • Optional integration via a embedded singleton manager (es-fluent-manager-embedded) or for Bevy (es-fluent-manager-bevy).

Installation

Add the crate with the derive feature to access the procedural macros:

[dependencies]
es-fluent = { version = "*", features = ["derive"] }
unic-langid = "*"

# If you want to register modules with the embedded singleton and localize at runtime:
es-fluent-manager-embedded = "*"

# For Bevy integration: replace `es-fluent-manager-embedded` with  `es-fluent-manager-bevy`
es-fluent-manager-bevy = "*"

To bootstrap .ftl files from your Rust types, add the build helper:

[build-dependencies]
es-fluent-build = "*"

And create a build.rs:

// build.rs
use es_fluent_build::FluentParseMode;

fn main() {
    if let Err(e) = es_fluent_build::FluentBuilder::new()
        .mode(FluentParseMode::Conservative)
        .build()
    {
        eprintln!("Error building FTL files: {e}");
    }
}

Project configuration

Create an i18n.toml next to your Cargo.toml:

# i18n.toml
assets_dir = "i18n"      # where your localized files live
fallback_language = "en" # default language subdirectory under assets_dir

When you run a build, the builder will:

  • Discover your crate name,
  • Parse Rust sources under src/,
  • Generate or update a base FTL file at {assets_dir}/{fallback_language}/{crate_name}.ftl.

For example, with assets_dir = "../i18n" and fallback_language = "en", the file would be ../i18n/en/{crate_name}.ftl.

Core derives

#[derive(EsFluent)] on enums

Annotate an enum or a struct to generate message IDs and, optionally, implement es_fluent::FluentDisplay or std::fmt::Display.

use es_fluent::EsFluent;

#[derive(EsFluent)]
#[fluent(display = "fluent")] // default; use "std" to implement std::fmt::Display
pub struct HelloUser<'a>(&'a str);

impl<'a> HelloUser<'a> {
    pub fn new(user_name: &'a str) -> Self {
        Self(user_name)
    }
}

Fields become Fluent arguments. The derive generates stable keys and formatting logic for you.

Choices with EsFluentChoice

When a message needs to match on an enum (a Fluent select expression), implement EsFluentChoice. You can then mark a field with #[fluent(choice)] to pass its choice value instead of formatting it as a nested message.

use es_fluent::{EsFluent, EsFluentChoice};

#[derive(EsFluent, EsFluentChoice)]
#[fluent_choice(serialize_all = "snake_case")]
pub enum Gender {
    Male,
    Female,
    Other,
}

#[derive(EsFluent)]
pub enum Shared<'a> {
    Photos {
        user_name: &'a str,
        photo_count: &'a u32,
        #[fluent(choice)]
        user_gender: &'a Gender,
    },
}

A prototyping build will write skeleton FTL like:

## Gender
gender-Male = Male
gender-Female = Female
gender-Other = Other

## Hello
hello-User = User { $user_name }

## Shared
shared-Photos = Photos { $user_name } { $photo_count } { $user_gender }

You can then edit it into a real copy, e.g.:

## Gender
gender-Female = Female
gender-Helicopter = Helicopter
gender-Male = Male
gender-Other = Other

## Hello
hello-User = Hello, {$user_name}!

## Shared
shared-Photos =
    {$user_name} {$photo_count ->
        [one] added a new photo
       *[other] added {$photo_count} new photos
    } to {$user_gender ->
        [male] his stream
        [female] her stream
       *[other] their stream
    }.

Display strategy

By default, EsFluent implements es_fluent::FluentDisplay, which formats through Fluent. If you prefer plain Rust Display for a type, use:

#[derive(EsFluent)]
#[fluent(display = "std")]
pub enum AbcStdDisplay {
    A, B, C,
}

This also works with strum::EnumDiscriminants when you want to display the discriminants.

#[derive(EsFluent)] on structs (keys and “this”)

You can derive on structs to produce key enums (labels, descriptions, etc.). For example:

use es_fluent::EsFluent;

#[derive(EsFluent)]
#[fluent(display = "std")]
#[fluent(this)] // generates `Address::this_ftl()`
#[fluent(keys = ["Description", "Label"])]
pub struct Address {
    pub street: String,
    pub postal_code: String,
}

This expands to enums like AddressLabelFtl and AddressDescriptionFtl with variants for each field (Street, PostalCode). They implement the selected display strategy. this adds a helper Address::this_ftl() that returns the ID of the parent.

#[derive(EsFluentKv)] on structs

For key-value generation from structs, you can use EsFluentKv. This derive is specialized for generating keys for struct fields, often used for UI elements like labels and descriptions.

Here is an example of a User struct with various fields:

use es_fluent::{EsFluent, EsFluentKv};
use rust_decimal::Decimal;
use strum::EnumIter;

#[derive(Clone, Debug, Default, EnumIter, EsFluent, PartialEq)]
#[fluent(display = "std")]
pub enum PreferedLanguage {
    #[default]
    English,
    French,
    Chinese,
}

#[derive(Clone, Debug, Default, EnumIter, EsFluent, PartialEq)]
#[fluent(display = "std")]
pub enum EnumCountry {
    #[default]
    UnitedStates,
    France,
    China,
}

#[derive(Clone, Debug, Default, EsFluentKv)]
#[fluent_kv(display = "std")]
#[fluent_kv(this, keys = ["Description", "Label"])]
pub struct User {
    pub username: Option<String>,
    pub email: String,
    pub age: Option<u32>,
    pub balance: Decimal,
    pub subscribe_newsletter: bool,
    pub enable_notifications: bool,
    pub preferred: PreferedLanguage,
    pub country: Option<EnumCountry>,
    pub birth_date: Option<chrono::NaiveDate>,
    pub skip_me: bool,
}

The #[fluent_kv(this, keys = ["Description", "Label"])] attribute instructs the derive to generate enums UserDescriptionFtl and UserLabelFtl. The this argument also generates a message ID for the struct itself.

This will generate the following FTL entries:

## EnumCountry

enum_country-China = China
enum_country-France = France
enum_country-UnitedStates = United States

## PreferedLanguage

prefered_language-Chinese = Chinese
prefered_language-English = English
prefered_language-French = French

## User

user = User

## UserDescriptionFtl

user_description_ftl = User Description Ftl
user_description_ftl-age = Age
user_description_ftl-balance = Balance
user_description_ftl-birth_date = Birth Date
user_description_ftl-country = Country
user_description_ftl-email = Email
user_description_ftl-enable_notifications = Enable Notifications
user_description_ftl-preferred = Preferred
user_description_ftl-skip_me = Skip Me
user_description_ftl-subscribe_newsletter = Subscribe Newsletter
user_description_ftl-username = Username

## UserLabelFtl

user_label_ftl = User Label Ftl
user_label_ftl-age = Age
user_label_ftl-balance = Balance
user_label_ftl-birth_date = Birth Date
user_label_ftl-country = Country
user_label_ftl-email = Email
user_label_ftl-enable_notifications = Enable Notifications
user_label_ftl-preferred = Preferred
user_label_ftl-skip_me = Skip Me
user_label_ftl-subscribe_newsletter = Subscribe Newsletter
user_label_ftl-username = Username

Derive Macro Supported kinds

Enums

  • enum_unit
  • enum_named
  • enum_tuple

Structs

  • struct_named
  • struct_tuple

Generics

Generic parameters must convert into Fluent values when used as arguments:

use es_fluent::EsFluent;
use fluent_bundle::FluentValue;

#[derive(EsFluent)]
pub enum GenericFluentDisplay<T>
where
    for<'a> &'a T: Into<FluentValue<'a>>,
{
    A(T),
    B { c: T },
    D,
}

Examples