Skip to main content

Crate es_fluent

Crate es_fluent 

Source
Expand description

§es-fluent

Build Status Codecov mdBook llms.txt llms-full.txt Docs Crates.io

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

This framework gives you:

§Examples

§Used in

§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 = "*"

es_fluent_manager_embedded::init_with_language(...) is the simplest startup path. If you want initialization errors back instead of log-only behavior, use es-fluent-manager-embedded::try_init_with_language(...):

es_fluent_manager_embedded::init_with_language(langid!("en-US"));

For custom runtime integrations, use es-fluent-manager-core::FluentManager::try_new_with_discovered_modules(). The Bevy plugin uses the same strict discovery model and exposes both RequestedLanguageId and ActiveLanguageId so systems can distinguish the requested locale from the currently published one. Failed locale switches keep the last ready locale active.

§Project configuration

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

# Default fallback language (required)
fallback_language = "en-US"

# Path to FTL assets relative to the config file (required)
assets_dir = "assets/locales"

# Features to enable if the crate’s es-fluent derives are gated behind a feature (optional)
fluent_feature = ["my-feature"]

# Optional allowlist of namespace values for FTL file splitting
namespaces = ["ui", "errors", "messages"]

Locale directory names use canonical BCP-47 tags such as en-US, fr, or de-DE-1901.

§Incremental builds for locale assets

If your crate uses the embedded or Bevy manager macros, they discover locales at compile time by scanning assets_dir. To ensure locale folder/file renames (for example fr to fr-FR) trigger rebuilds, enable the build feature of es-fluent in build dependencies and call the tracking helper from build.rs. Crates that only use the derive macros do not need this setup.

[build-dependencies]
es-fluent = { version = "*", features = ["build"] }
// build.rs
fn main() {
    es_fluent::build::track_i18n_assets();
}

§Namespaces (optional)

You can route specific types into separate .ftl files by adding a namespace. All derive macros support the same namespace options:

§EsFluent

use es_fluent::EsFluent;

#[derive(EsFluent)]
#[fluent(namespace = "ui")]
pub struct Button<'a>(pub &'a str);

#[derive(EsFluent)]
#[fluent(namespace = file)]
pub struct Dialog {
    pub title: String,
}

#[derive(EsFluent)]
#[fluent(namespace(file(relative)))]
pub enum Gender {
    Male,
    Female,
    Other(String),
    Helicopter { type_: String },
}

§EsFluentThis

use es_fluent::EsFluentThis;

#[derive(EsFluentThis)]
#[fluent_this(origin)]
#[fluent(namespace = "forms")]
pub enum GenderThis { Male, Female, Other }

#[derive(EsFluentThis)]
#[fluent_this(origin)]
#[fluent(namespace = file)]
pub enum Status { Active, Inactive }

#[derive(EsFluentThis)]
#[fluent_this(origin)]
#[fluent(namespace(file(relative)))]
pub struct UserProfile;

#[derive(EsFluentThis)]
#[fluent_this(origin)]
#[fluent(namespace = folder)]
pub enum FolderStatus { Active, Inactive }

#[derive(EsFluentThis)]
#[fluent_this(origin)]
#[fluent(namespace(folder(relative)))]
pub struct FolderUserProfile;

§EsFluentVariants

use es_fluent::EsFluentVariants;

#[derive(EsFluentVariants)]
#[fluent_variants(keys = ["label", "description"])]
#[fluent(namespace = "forms")]
pub struct LoginForm {
    pub username: String,
    pub password: String,
}

#[derive(EsFluentVariants)]
#[fluent(namespace = file)]
pub enum StatusVariants { Active, Inactive }

§Output Layout

  • Default: assets_dir/{locale}/{crate}.ftl
  • Namespaced: assets_dir/{locale}/{crate}/{namespace}.ftl

When namespaces are used, namespace files are treated as the canonical split for that locale, and {crate}.ftl can still participate as an optional base resource for non-namespaced messages.

§Namespace Values

  • namespace = "name" - explicit namespace string
  • namespace = file - uses the source file stem (e.g., src/ui/button.rs -> button)
  • namespace(file(relative)) - uses the file path relative to the crate root, strips src/, and removes the extension (e.g., src/ui/button.rs -> ui/button)
  • namespace = folder - uses the source file parent folder (e.g., src/ui/button.rs -> ui)
  • namespace(folder(relative)) - uses the parent folder path relative to the crate root, strips src/ when nested, and keeps src for root module files (e.g., src/ui/button.rs -> ui)

If namespaces = [...] is set in i18n.toml, both the compiler (at compile-time) and the CLI will validate that string-based namespaces used by your code are in that allowlist.

§Derives

§#[derive(EsFluent)]

Turns an enum or struct into a localizable message.

  • Enums: Each variant becomes a message ID (e.g., MyEnum::Variant -> my_enum-Variant).
  • Structs: The struct itself becomes the message ID (e.g., MyStruct -> my_struct).
  • Fields: Fields are automatically exposed as arguments to the Fluent message.
use es_fluent::{EsFluent};

#[derive(EsFluent)]
pub enum LoginError {
    InvalidPassword, // no params
    UserNotFound { username: String }, // exposed as $username in the ftl file
    Something(String, String, String), // exposed as $f0, $f1, $f2 in the ftl file
    SomethingArgNamed(
        #[fluent(arg_name = "input")] String,
        #[fluent(arg_name = "expected")] String,
        #[fluent(arg_name = "details")] String,
    ), // exposed as $input, $expected, $details
}

use es_fluent::ToFluentString;
let _ = LoginError::InvalidPassword.to_fluent_string();
let _ = LoginError::UserNotFound { username: "john".to_string() }.to_fluent_string();
let _ = LoginError::Something("a".to_string(), "b".to_string(), "c".to_string()).to_fluent_string();
let _ = LoginError::SomethingArgNamed("a".to_string(), "b".to_string(), "c".to_string()).to_fluent_string();

#[derive(EsFluent)]
pub struct WelcomeMessage<'a> {
    pub name: &'a str, // exposed as $name in the ftl file
    pub count: i32,    // exposed as $count in the ftl file
}

use es_fluent::ToFluentString;
let welcome = WelcomeMessage { name: "John", count: 5 };
let _ = welcome.to_fluent_string();

Argument naming attributes:

  • arg_name = "..." on a field renames that exposed Fluent argument (works on struct fields, enum named fields, and enum tuple fields).

Skipped single-field enum variants:

#[fluent(skip)] on a single-field enum variant suppresses that variant’s own key and delegates to_fluent_string() to the wrapped value. This is useful for transparent wrapper enums.

use es_fluent::{EsFluent, ToFluentString};

#[derive(EsFluent)]
pub enum NetworkError {
    ApiUnavailable,
}

#[derive(EsFluent)]
pub enum TransactionError {
    #[fluent(skip)]
    Network(NetworkError),
}

let _ = TransactionError::Network(NetworkError::ApiUnavailable).to_fluent_string();
## NetworkError

network_error-ApiUnavailable = API is unavailable

§#[derive(EsFluentChoice)]

Allows an enum to be used inside another message as a selector (e.g., for gender or status).

use es_fluent::{EsFluent, EsFluentChoice};

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

#[derive(EsFluent)]
pub struct Greeting<'a> {
    pub name: &'a str,
    #[fluent(choice)] // Matches $gender -> [male]...
    pub gender: &'a GenderChoice,
}

use es_fluent::ToFluentString;
let greeting = Greeting { name: "John", gender: &GenderChoice::Male };
let _ = greeting.to_fluent_string();

§#[derive(EsFluentVariants)]

Generates key-value pair enums for struct fields or enum variants. This is useful for generating UI labels, placeholders, or descriptions for a form object, and it can also expose enum variants as localizable keys.

use es_fluent::EsFluentVariants;

#[derive(EsFluentVariants)]
#[fluent_variants(keys = ["label", "description"])]
pub struct LoginFormVariants {
    pub username: String,
    pub password: String,
}

// Generates enums -> keys:
// LoginFormVariantsLabelVariants::{Variants} -> (login_form_variants_label_variants-{variant})
// LoginFormVariantsDescriptionVariants::{Variants} -> (login_form_variants_description_variants-{variant})

use es_fluent::ToFluentString;
let _ = LoginFormVariantsLabelVariants::Username.to_fluent_string();

#[derive(EsFluentVariants)]
pub enum SettingsTab {
    General,
    Notifications,
    Privacy,
}

// Generates enum -> keys:
// SettingsTabVariants::{General, Notifications, Privacy}
//     -> (settings_tab_variants-{variant})

let _ = SettingsTabVariants::Notifications.to_fluent_string();

§#[derive(EsFluentThis)]

Generates a helper implementation of the ThisFtl trait and registers the type’s name as a key. This is similar to EsFluentVariants (which registers field- or variant-derived keys), but for the parent type itself.

  • #[fluent_this(origin)]: Generates an implementation where this_ftl() returns the base key for the type.
use es_fluent::EsFluentThis;

#[derive(EsFluentThis)]
#[fluent_this(origin)]
pub enum GenderThisOnly {
    Male,
    Female,
    Other,
}

// Generates key:
// (gender_this_only_this)

use es_fluent::ThisFtl;
let _ = GenderThisOnly::this_ftl();
  • #[fluent_this(variants)]: Can be combined with EsFluentVariants derives to generate keys for variants.
#[derive(EsFluentVariants, EsFluentThis)]
#[fluent_this(origin, variants)]
#[fluent_variants(keys = ["label", "description"])]
pub struct LoginFormCombined {
    pub username: String,
    pub password: String,
}

// Generates keys:
// (login_form_combined_label_variants_this)
// (login_form_combined_description_variants_this)

use es_fluent::ThisFtl;
let _ = LoginFormCombinedDescriptionVariants::this_ftl();

Enums§

GlobalLocalizationError

Traits§

EsFluentChoice
This trait is used to convert an enum into a string that can be used as a Fluent choice.
FluentDisplay
This trait is similar to std::fmt::Display, but it is used for formatting types that can be displayed in a Fluent message.
ThisFtl
A trait for types that have a “this” fluent key representing the type itself.
ToFluentString
This trait is automatically implemented for any type that implements FluentDisplay.