Skip to main content

bevy_intl/
components.rs

1//! Reactive translation component and supporting types.
2//!
3//! Spawn an [`I18nText`] alongside any entity that has a `Text` component;
4//! the [`update_i18n_text`] system (registered automatically by
5//! [`crate::I18nPlugin`]) keeps the rendered text in sync with the active
6//! language. When the language changes, every `I18nText` in the world is
7//! re-rendered and a [`LanguageChanged`] event is fired so other systems can
8//! react (e.g. reloading localized assets).
9
10use bevy::prelude::*;
11
12use crate::I18n;
13
14/// Component describing a translation key to render into a sibling `Text`.
15///
16/// The component owns its `file` / `key` strings to keep things `Send + Sync`
17/// without lifetimes; for hot UI text consider caching the
18/// [`I18nText`] entity rather than rebuilding it every frame.
19#[derive(Component, Clone, Debug)]
20#[require(Text)]
21pub struct I18nText {
22    /// Translation file (without the `.json` extension), e.g. `"ui"`.
23    pub file: String,
24    /// Translation key inside that file, e.g. `"welcome"`.
25    pub key: String,
26    /// How to render the translation (plain, plural, gender, …).
27    pub mode: I18nMode,
28}
29
30impl I18nText {
31    /// Convenience constructor for a plain translation.
32    pub fn new(file: impl Into<String>, key: impl Into<String>) -> Self {
33        Self {
34            file: file.into(),
35            key: key.into(),
36            mode: I18nMode::Plain,
37        }
38    }
39}
40
41/// Selects which translation method to call when rendering an [`I18nText`].
42#[derive(Clone, Debug)]
43pub enum I18nMode {
44    /// `t(key)`
45    Plain,
46    /// `t_with_args(key, args)` — owned name/value pairs (any `Display` value).
47    Args(Vec<(String, String)>),
48    /// `t_with_plural(key, count)`
49    Plural(usize),
50    /// `t_with_gender(key, gender)`
51    Gender(String),
52    /// `t_with_gender_and_args(key, gender, args)`
53    GenderArgs(String, Vec<(String, String)>),
54    /// `t_with_gender_and_plural(key, gender, count)`
55    GenderPlural(String, usize),
56}
57
58/// Message broadcast by [`update_i18n_text`] when the active language changes.
59///
60/// Useful for reacting to language changes outside of `I18nText` (e.g. swapping
61/// images, reloading audio, refreshing a custom widget). Read it with a
62/// `MessageReader<LanguageChanged>` system param.
63///
64/// Bevy 0.18 renamed buffered events to *messages*, so this type derives
65/// `Message` rather than `Event` — the practical usage is the same.
66#[derive(Message, Debug, Clone)]
67pub struct LanguageChanged {
68    pub from: String,
69    pub to: String,
70}
71
72/// Bevy system that keeps `Text` in sync with `I18nText`.
73///
74/// - When the active language changes, every `I18nText` is re-rendered and a
75///   `LanguageChanged` event is written.
76/// - Otherwise, only entities with `Added<I18nText>` or `Changed<I18nText>` are
77///   re-rendered (cheap incremental updates on spawn / edit).
78pub fn update_i18n_text(
79    i18n: Res<I18n>,
80    mut sets: ParamSet<(
81        Query<(&I18nText, &mut Text), Or<(Changed<I18nText>, Added<I18nText>)>>,
82        Query<(&I18nText, &mut Text)>,
83    )>,
84    mut last_lang: Local<Option<String>>,
85    mut events: MessageWriter<LanguageChanged>,
86) {
87    let current = i18n.get_lang().to_string();
88    let lang_changed = last_lang.as_deref() != Some(current.as_str());
89
90    if lang_changed {
91        let prev = last_lang.replace(current.clone());
92        if let Some(prev) = prev {
93            events.write(LanguageChanged { from: prev, to: current.clone() });
94        }
95        let mut q = sets.p1();
96        for (it, mut text) in &mut q {
97            text.0 = render(&i18n, it);
98        }
99    } else {
100        let mut q = sets.p0();
101        for (it, mut text) in &mut q {
102            text.0 = render(&i18n, it);
103        }
104    }
105}
106
107fn render(i18n: &I18n, it: &I18nText) -> String {
108    let t = i18n.translation(&it.file);
109    match &it.mode {
110        I18nMode::Plain => t.t(&it.key),
111        I18nMode::Plural(c) => t.t_with_plural(&it.key, *c),
112        I18nMode::Gender(g) => t.t_with_gender(&it.key, g),
113        I18nMode::Args(args) => {
114            let view: Vec<(&str, &dyn ToString)> = args
115                .iter()
116                .map(|(k, v)| (k.as_str(), v as &dyn ToString))
117                .collect();
118            t.t_with_args(&it.key, &view)
119        }
120        I18nMode::GenderArgs(g, args) => {
121            let view: Vec<(&str, &dyn ToString)> = args
122                .iter()
123                .map(|(k, v)| (k.as_str(), v as &dyn ToString))
124                .collect();
125            t.t_with_gender_and_args(&it.key, g, &view)
126        }
127        I18nMode::GenderPlural(g, c) => t.t_with_gender_and_plural(&it.key, g, *c),
128    }
129}