es-fluent 0.4.0

The es-fluent crate
Documentation

es-fluent

Build Status Docs Crates.io

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

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

To bootstrap .ftl files from your Rust types, install the CLI tool:

cargo install es-fluent-cli

Project configuration

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

# i18n.toml
assets_dir = "i18n"         # where your localized files live
fallback_language = "en"

CLI

Docs Crates.io

Generate

Run the generator to discover your crate name, parse Rust sources under src/, and generate or update a base FTL file at {assets_dir}/{fallback_language}/{crate_name}.ftl.

es-fluent generate

Watch

Automatically compile and run the generator whenever you modify your source code.

es-fluent watch

Clean

Remove orphan keys and groups that are no longer present in your source code.

es-fluent clean

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

Core derives

#[derive(EsFluent)]

Annotate an enum or a struct to generate message IDs and implement es_fluent::FluentDisplay.

use es_fluent::EsFluent;

#[derive(EsFluent)]
pub enum TeaBlend {
    EarlGrey,
    EnglishBreakfast,
    Darjeeling,
}

#[derive(EsFluent)]
pub enum Drink {
    Tea { blend: TeaBlend },
    Water,
}

#[derive(EsFluent)]
pub struct Hello<'a>(pub &'a str);

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
    }.

#[derive(EsFluentKv)]

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

use es_fluent::EsFluentKv;

#[derive(EsFluentKv)]
#[fluent_kv(keys = ["description", "label"])]
pub struct Address {
    pub street: String,
    pub postal_code: String,
}

#[derive(EsFluentThis)]

Generates a helper method this_ftl() that returns the fluent representation of the parent.

use es_fluent::{EsFluent, EsFluentKv, EsFluentThis};

#[derive(EsFluent, EsFluentThis)]
#[fluent_this(origin)]
pub enum TeaBlend {
    EarlGrey,
    EnglishBreakfast,
    Darjeeling,
}

#[derive(EsFluent, EsFluentThis)]
#[fluent_this(origin)]
pub enum Drink {
    Tea { blend: TeaBlend },
    Water,
}

#[derive(EsFluent, EsFluentThis)]
#[fluent_this(origin)]
pub struct Hello<'a>(pub &'a str);

#[derive(EsFluentKv, EsFluentThis)]
#[fluent_this(origin, members)]
#[fluent_kv(keys = ["description", "label"])]
pub struct Address {
    pub street: String,
    pub postal_code: String,
}

#[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)]
pub enum PreferedLanguage {
    #[default]
    English,
    French,
    Chinese,
}

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

#[derive(Clone, Debug, Default, EsFluentKv)]
#[fluent_kv(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(keys = ["description", "label"])] attribute instructs the derive to generate enums UserDescriptionFtl and UserLabelFtl.

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_kv_ftl = User Description Ftl
user_description_kv_ftl-age = Age
user_description_kv_ftl-balance = Balance
user_description_kv_ftl-birth_date = Birth Date
user_description_kv_ftl-country = Country
user_description_kv_ftl-email = Email
user_description_kv_ftl-enable_notifications = Enable Notifications
user_description_kv_ftl-preferred = Preferred
user_description_kv_ftl-skip_me = Skip Me
user_description_kv_ftl-subscribe_newsletter = Subscribe Newsletter
user_description_kv_ftl-username = Username

## UserLabelFtl

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

Generics

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

use es_fluent::{EsFluent, FluentValue};

#[derive(Clone, EsFluent)]
pub struct GenericStruct<T>
where
    T: Into<FluentValue<'static>> + Clone,
{
    pub field: T,
}

#[derive(Clone, EsFluent)]
pub struct GenericTupleStruct<T>(pub T)
where
    T: Into<FluentValue<'static>> + Clone;

#[derive(Clone, EsFluent)]
pub enum GenericEnum<T>
where
    T: Into<FluentValue<'static>> + Clone,
{
    Variant(T),
    StructVariant { field: T },
    Unit,
}

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:

[dependencies]
es-fluent-lang = "*"
unic-langid = "*"

Then, apply the macro to an empty enum:

use es_fluent::EsFluent;
use es_fluent_lang::es_fluent_language;
use strum::EnumIter;

#[es_fluent_language]
#[derive(Clone, Copy, Debug, EnumIter, EsFluent, PartialEq)]
pub enum Languages {}

// The macro generates variants from your i18n asset folders.
// If you have 'en' and 'fr-CA', it generates:
// enum Language { En, FrCA }