34 releases

Uses new Rust 2024

new 0.18.11 Apr 6, 2026
0.18.10 Mar 27, 2026
0.18.9 Feb 21, 2026
0.18.2 Jan 31, 2026
0.17.5 Oct 20, 2025

#417 in Internationalization (i18n)

MIT/Apache

160KB
3K SLoC

Docs Crates.io

es-fluent-manager-bevy

online demo

Seamless Bevy integration for es-fluent.

This plugin connects es-fluent's type-safe localization with Bevy's ECS and Asset system. It allows you to use standard #[derive(EsFluent)] types as components that automatically update when the app/game's language changes.

es-fluent-manager-bevy bevy
crates.io
0.18.x 0.18.x
0.17.x 0.17.x

Features

  • Asset Loading: Loads .ftl files via Bevy's AssetServer.
  • Hot Reloading: Supports hot-reloading of translations during development.
  • Reactive UI: The FluentText component automatically refreshes text when the locale changes.
  • Global Hook: Integrates with es-fluent's global state.

Quick Start

1. Define the Module

In your crate root (lib.rs or main.rs), tell the manager to scan your assets:

// a i18n.toml file must exist in the root of the crate
es_fluent_manager_embedded::define_i18n_module!();

2. Initialize & Use

Add the plugin to your App and define your I18n module:

use bevy::prelude::*;
use es_fluent_manager_bevy::I18nPlugin;
use unic_langid::langid;

// a i18n.toml file must exist in the root of the crate
es_fluent_manager_bevy::define_i18n_module!();

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // Initialize with default language
        .add_plugins(I18nPlugin::with_language(langid!("en-US")))
        .run();
}

Prefer the BevyFluentText derive macro. It auto-registers your type with I18nPlugin via inventory, so you don't have to call any registration functions manually.

If a field depends on the active locale (like the Languages enum from es_fluent_lang), mark it with #[locale]. The macro will generate RefreshForLocale and register the locale-aware systems for you.

use bevy::prelude::Component;
use es_fluent::EsFluent;
use es_fluent_manager_bevy::BevyFluentText;

#[derive(BevyFluentText, Clone, Component, EsFluent)]
pub enum UiMessage {
    StartGame,
    Settings,
    LanguageHint {
        #[locale]
        current_language: Languages,
    },
}

4. Using in UI

Use the FluentText component wrapper for any type that implements ToFluentString (which #[derive(EsFluent)] provides).

use es_fluent_manager_bevy::FluentText;

fn spawn_menu(mut commands: Commands) {
    commands.spawn((
        // This text will automatically update if language changes
        FluentText::new(UiMessage::StartGame),
        Text::new(""),
    ));
}

Manual Registration (Fallback)

If you cannot derive BevyFluentText (e.g., external types), you can still register manually:

app.register_fluent_text::<UiMessage>();

If the type needs locale refresh, implement RefreshForLocale and use the locale-aware registration function:

use es_fluent_manager_bevy::RefreshForLocale;

#[derive(EsFluent, Clone, Component)]
pub enum UiMessage {
    LanguageHint { current_language: Languages },
}

impl RefreshForLocale for UiMessage {
    fn refresh_for_locale(&mut self, lang: &unic_langid::LanguageIdentifier) {
        match self {
            UiMessage::LanguageHint { current_language } => {
                if let Ok(value) = Languages::try_from(lang) {
                    *current_language = value;
                }
            }
        }
    }
}

app.register_fluent_text_from_locale::<UiMessage>();

Do Nested Types Need BevyFluentText?

Only the component type wrapped by FluentText<T> needs registration. If a nested field (like KbKeys) is only used inside a registered component, it does not need BevyFluentText. When the parent component re-renders, its EsFluent implementation formats all fields using the current locale.

You only need BevyFluentText for a nested type if you plan to use it directly as FluentText<ThatType> or otherwise register it as its own component.

Dependencies

~30–51MB
~776K SLoC