es-fluent
Derive macros and utilities for authoring strongly-typed messages with Project Fluent.
This framework gives you:
- Derives to turn enums/structs into Fluent message IDs and arguments.
- A cli to generate ftl files skeleton and other utilities.
- Language Enum Generation
- Integration via a embedded singleton manager or es-fluent-manager-bevy for bevy
Examples
Used in
Installation
Add the crate with the derive feature to access the procedural macros:
[]
= { = "*", = ["derive"] }
= "*"
# If you want to register modules with the embedded singleton and localize at runtime:
= "*"
# For Bevy integration: replace `es-fluent-manager-embedded` with `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)
= "en-US"
# Path to FTL assets relative to the config file (required)
= "assets/locales"
# Features to enable if the crate’s es-fluent derives are gated behind a feature (optional)
= ["my-feature"]
# Optional allowlist of namespace values for FTL file splitting
= ["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"] }
// build.rs
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 EsFluent;
;
EsFluentThis
use EsFluentThis;
;
;
EsFluentVariants
use EsFluentVariants;
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 stringnamespace = 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, stripssrc/, 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, stripssrc/when nested, and keepssrcfor 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 ;
use ToFluentString;
let _ = InvalidPassword.to_fluent_string;
let _ = UserNotFound .to_fluent_string;
let _ = Something.to_fluent_string;
let _ = SomethingArgNamed.to_fluent_string;
use ToFluentString;
let welcome = WelcomeMessage ;
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 ;
let _ = Network.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 ;
use ToFluentString;
let greeting = Greeting ;
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 EsFluentVariants;
// Generates enums -> keys:
// LoginFormVariantsLabelVariants::{Variants} -> (login_form_variants_label_variants-{variant})
// LoginFormVariantsDescriptionVariants::{Variants} -> (login_form_variants_description_variants-{variant})
use ToFluentString;
let _ = Username.to_fluent_string;
// Generates enum -> keys:
// SettingsTabVariants::{General, Notifications, Privacy}
// -> (settings_tab_variants-{variant})
let _ = 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 wherethis_ftl()returns the base key for the type.
use EsFluentThis;
// Generates key:
// (gender_this_only_this)
use ThisFtl;
let _ = this_ftl;
#[fluent_this(variants)]: Can be combined withEsFluentVariantsderives to generate keys for variants.
// Generates keys:
// (login_form_combined_label_variants_this)
// (login_form_combined_description_variants_this)
use ThisFtl;
let _ = this_ftl;