Expand description
§bevy-intl
A pragmatic internationalization (i18n) plugin for Bevy that loads translations from JSON files, supports fallback languages, named placeholders, CLDR-correct plurals, gendered text, and a reactive I18nText component for Bevy UI. WASM-friendly out of the box.
§Features
- WASM compatible — translations are bundled at build time for web targets.
- Flexible loading — filesystem on desktop, bundled on WASM, or
bundle-onlyeverywhere via a feature flag. - JSON layout — one folder per language, one file per “namespace” (e.g.
ui.json,menu.json). - Named placeholders —
{{name}}substituted by name, with thei18n_args!macro for ergonomics. - CLDR-correct plurals — backed by
intl_pluralrules; Polish, Russian, Arabic etc. work as expected. - Gendered translations — single-axis or combined gender × plural via nested JSON.
- Reactive UI — drop an
I18nTextcomponent on an entity and it stays in sync as the language changes. - Fallback language — automatic fallback when a key is missing.
§Quick start
[dependencies]
bevy = "0.18"
bevy-intl = "0.3"
# Optional: force bundled translations on every target (e.g. for shipping a single binary)
# bevy-intl = { version = "0.3", features = ["bundle-only"] }use bevy::prelude::*;
use bevy_intl::{I18nPlugin, I18nConfig};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// Default setup auto-detects WASM vs desktop
.add_plugins(I18nPlugin::default())
// Or with custom config:
// .add_plugins(I18nPlugin::with_config(I18nConfig {
// messages_folder: "locales".to_string(),
// default_lang: "fr".to_string(),
// fallback_lang: "en".to_string(),
// ..Default::default()
// }))
.run();
}Version compatibility
| Bevy | bevy-intl |
|---|---|
| 0.18.x | 0.3.x |
| 0.17.x | 0.2.2 |
| 0.16.x | 0.2.1 |
MSRV — Rust 1.85 (uses std::sync::LazyLock and edition 2024).
§Folder layout
messages/
├── en/
│ ├── ui.json
│ └── menu.json
├── fr/
│ ├── ui.json
│ └── menu.json
└── pl/
├── ui.json
└── menu.json
assets/
src/A folder name that is not a recognized ISO/CLDR locale logs a warning at startup. Disable with I18nConfig.warn_unknown_locales = false if you intentionally use custom codes.
§JSON format
Three shapes are supported per key:
{
"greeting": "Hello", // plain text
"farewell": { // single-axis: gender OR plural
"male": "Goodbye, sir",
"female": "Goodbye, ma'am"
},
"apples": {
"0": "No apples", // exact-count beats CLDR category
"one": "One apple",
"other": "{{count}} apples"
},
"guests": { // two-axis: gender × plural (nested)
"male": { "one": "{{count}} guest (M)", "other": "{{count}} guests (M)" },
"female": { "one": "{{count}} guest (F)", "other": "{{count}} guests (F)" }
}
}§Plural-key resolution priority
- Exact count —
"0","1","5", … - CLDR category for the active locale — resolved by
intl_pluralrules(so Polish getsone/few/many/other, Russian getsone/few/many/otherwith the right buckets, Arabic getszero/one/two/few/many/other, etc.). - Anglo-centric fallback —
"one"forcount == 1,"other"otherwise. - Last resort —
"many".
§API
use bevy::prelude::*;
use bevy_intl::{I18n, i18n_args};
fn translation_system(i18n: Res<I18n>) {
let t = i18n.translation("ui");
// Plain
let _ = t.t("greeting");
// Named placeholders
let _ = t.t_with_args("welcome", i18n_args!{ name = "John", count = 5 });
// Equivalent without the macro:
let _ = t.t_with_args("welcome", &[("name", &"John"), ("count", &5)]);
// Plural
let _ = t.t_with_plural("apples", 5);
// Gender (single-axis)
let _ = t.t_with_gender("farewell", "female");
// Gender + named placeholders
let _ = t.t_with_gender_and_args("greeting", "male", i18n_args!{ name = "Smith" });
// Gender + plural (nested JSON)
let _ = t.t_with_gender_and_plural("guests", "female", 3);
}Deprecated —
t_with_argandt_with_gender_and_arg(positional placeholders) still work but ignore placeholder names in your JSON. Migrate tot_with_args/t_with_gender_and_argsfor proper named substitution.
§Switching language
fn change_language_system(mut i18n: ResMut<I18n>) {
// Logging variant:
i18n.set_lang("fr");
// Result-returning variant:
if let Err(e) = i18n.try_set_lang("xx") {
eprintln!("locale not loaded: {e}");
}
let _ = i18n.get_lang(); // current
let _ = i18n.get_fallback_lang(); // fallback
let _ = i18n.available_languages(); // sorted list
}set_lang_i18n / set_fallback_lang are also available on App (via LanguageAppExt) for setting the language at startup before app.run():
use bevy_intl::{I18nPlugin, LanguageAppExt};
App::new()
.add_plugins(I18nPlugin::default())
.set_lang_i18n("fr")
.set_fallback_lang("en")
.run();§Reactive UI: I18nText
Spawn an I18nText next to any text node and it stays in sync — no manual rebuild loop, no boilerplate. When the language changes, every I18nText is re-rendered and a LanguageChanged message is broadcast.
use bevy::prelude::*;
use bevy_intl::{I18nPlugin, I18n, I18nText, I18nMode, LanguageChanged};
fn setup_ui(mut commands: Commands) {
// I18nText auto-adds a `Text` component thanks to `#[require(Text)]`.
commands.spawn(I18nText::new("ui", "welcome"));
commands.spawn(I18nText {
file: "ui".to_string(),
key: "guests".to_string(),
mode: I18nMode::GenderPlural("female".to_string(), 3),
});
}
fn switcher(input: Res<ButtonInput<KeyCode>>, mut i18n: ResMut<I18n>) {
if input.just_pressed(KeyCode::F1) { i18n.set_lang("en"); }
if input.just_pressed(KeyCode::F2) { i18n.set_lang("fr"); }
}
fn react(mut reader: MessageReader<LanguageChanged>) {
for ev in reader.read() {
info!("language switched: {} → {}", ev.from, ev.to);
}
}
fn main() {
App::new()
.add_plugins((DefaultPlugins, I18nPlugin::default()))
.add_systems(Startup, setup_ui)
.add_systems(Update, (switcher, react))
.run();
}Bevy 0.18 renamed buffered events to messages, so LanguageChanged derives Message and is read with MessageReader<LanguageChanged> (not EventReader).
§WASM / platform behaviour
| Target | Default loading |
|---|---|
| Desktop | reads messages/ folder at runtime |
| WASM | uses bundled translations (compiled in by build.rs) |
To force bundled mode on every target — for example, to ship a single binary:
bevy-intl = { version = "0.3", features = ["bundle-only"] }§Migration 0.2 → 0.3
- Placeholders — replace
t_with_arg(key, &[&"John"])witht_with_args(key, i18n_args!{ name = "John" })(positional API kept but deprecated). - Plurals — exact-count keys still win. CLDR categories now resolve correctly for the active locale; if you authored translations against the old anglo-centric
3..=10 → "few"rule, double-check Polish/Russian/Arabic JSON. - Events —
LanguageChangedis aMessage(Bevy 0.18). UseMessageReader, notEventReader. - Type lifetime —
I18nPartialnow borrows fromI18n(I18nPartial<'_>). If you stored it in a struct, that struct now needs a lifetime parameter; usually you can just inlinei18n.translation("ui")at the call site. - Naming —
file_traductions/fallback_traductionare renamed (private fields, no API impact). - MSRV — bumped to Rust 1.85 (edition 2024).
§License
Dual-licensed under either of:
- MIT License (LICENSE-MIT)
- Apache License 2.0 (LICENSE-APACHE)
at your option.
§bevy-intl
A comprehensive internationalization (i18n) plugin for Bevy that provides:
- WASM Compatible: Automatic translation bundling for web deployment
- Flexible Loading: Filesystem (desktop) or bundled files (WASM)
- Feature Flag:
bundle-onlyto force bundled translations on any platform - Advanced Plurals: Support for complex plural rules (ICU-compliant)
- Gender Support: Gendered translations
- Placeholders: Dynamic text replacement
- Fallback System: Automatic fallback to default language
§Quick Start
use bevy::prelude::*;
use bevy_intl::I18nPlugin;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(I18nPlugin::default())
.add_systems(Startup, setup_ui)
.run();
}
fn setup_ui(mut commands: Commands, i18n: Res<bevy_intl::I18n>) {
let text = i18n.translation("ui");
commands.spawn((
Text::new(text.t("welcome")),
Node::default(),
));
}§Features
§Translation Loading
- Desktop: Loads from
messages/folder at runtime - WASM: Uses bundled translations (compiled at build time)
- Bundle-only: Force bundled mode with
features = ["bundle-only"]
§Advanced Plural Support
Supports multiple plural forms with fallback priority:
- Exact counts:
"0","1","2", etc. - ICU categories:
"zero","one","two","few","many" - Basic fallback:
"one"vs"other"
Perfect for complex languages like Polish, Russian, and Arabic.
Macros§
- i18n_
args - Build an argument slice for the named-placeholder translation methods.
Structs§
- I18n
- Main resource for accessing translations in Bevy systems.
- I18n
Config - Configuration for the I18n plugin.
- I18n
Partial - Represents translations for a single file.
- I18n
Plugin - Main plugin for Bevy internationalization.
- I18n
Text - Component describing a translation key to render into a sibling
Text. - Language
Changed - Message broadcast by
update_i18n_textwhen the active language changes. - Translations
- Contains all translations loaded from filesystem or bundled data.
Enums§
- I18n
Error - Errors returned by fallible operations on
I18n. - I18n
Mode - Selects which translation method to call when rendering an
I18nText. - Section
Value - Represents a value in a translation file.
Traits§
- Language
AppExt - Extension trait for
Appto set languages at startup, beforerun().
Functions§
- update_
i18n_ text - Bevy system that keeps
Textin sync withI18nText.