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.
- Integration via a embedded singleton manager (
es-fluent-manager-embedded) or for Bevy (es-fluent-manager-bevy).
Examples
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`
= "*"
To bootstrap .ftl files from your Rust types, add the build helper:
[]
= "*"
And create a build.rs:
// build.rs
use FluentParseMode;
Project configuration
Create an i18n.toml next to your Cargo.toml:
# i18n.toml
= "i18n" # where your localized files live
= "en"
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 EsFluent;
// default; use "std" to implement std::fmt::Display
;
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 ;
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:
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 EsFluent;
// generates `Address::this_ftl()`
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 ;
use Decimal;
use EnumIter;
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 EsFluent;
use FluentValue;
Language Enum Generation
#[es_fluent_language]
This macro reads your crate's i18n.toml, finds all available languages in your assets_dir, and generates an enum with a variant for each one. It also implements Default (using your fallback_language) and conversions to/from unic_langid::LanguageIdentifier.
Usage
Add the dependencies:
[]
= "*"
= "*"
Then, apply the macro to an empty enum:
use EsFluent;
use es_fluent_language;
use EnumIter;
// The macro generates variants from your i18n asset folders.
// If you have 'en' and 'fr-CA', it generates:
// enum Language { En, FrCA }