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}