1#![doc = include_str!("../README.md")]
2
3pub use bevy;
4pub use inventory;
5
6use bevy::asset::{Asset, AssetLoader, AsyncReadExt as _, LoadContext};
7use bevy::prelude::*;
8use fluent_bundle::{FluentResource, FluentValue, bundle::FluentBundle};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::sync::Arc;
12use unic_langid::LanguageIdentifier;
13
14#[cfg(feature = "macros")]
15pub use es_fluent_manager_macros::BevyFluentText;
16#[cfg(feature = "macros")]
17pub use es_fluent_manager_macros::define_bevy_i18n_module as define_i18n_module;
18
19pub use unic_langid;
20
21pub mod components;
22pub mod plugin;
23pub mod systems;
24
25pub use components::*;
26pub use es_fluent::{FluentDisplay, ToFluentString};
27pub use plugin::*;
28pub use systems::*;
29
30#[derive(Clone, Resource)]
32pub struct CurrentLanguageId(pub LanguageIdentifier);
33
34pub fn primary_language(lang: &LanguageIdentifier) -> &str {
38 lang.language.as_str()
39}
40
41pub trait FromLocale {
46 fn from_locale(lang: &LanguageIdentifier) -> Self;
48}
49
50pub trait RefreshForLocale {
55 fn refresh_for_locale(&mut self, lang: &LanguageIdentifier);
57}
58
59impl<T> RefreshForLocale for T
64where
65 T: FromLocale,
66{
67 #[inline]
68 fn refresh_for_locale(&mut self, lang: &LanguageIdentifier) {
69 *self = T::from_locale(lang);
70 }
71}
72
73#[derive(Asset, Clone, Debug, Deserialize, Serialize, TypePath)]
75pub struct FtlAsset {
76 pub content: String,
78}
79
80#[derive(Default, TypePath)]
82pub struct FtlAssetLoader;
83
84impl AssetLoader for FtlAssetLoader {
85 type Asset = FtlAsset;
86 type Settings = ();
87 type Error = std::io::Error;
88
89 async fn load(
90 &self,
91 reader: &mut dyn bevy::asset::io::Reader,
92 _settings: &Self::Settings,
93 _load_context: &mut LoadContext<'_>,
94 ) -> Result<Self::Asset, Self::Error> {
95 let mut content = String::new();
96 reader.read_to_string(&mut content).await?;
97 Ok(FtlAsset { content })
98 }
99
100 fn extensions(&self) -> &[&str] {
101 &["ftl"]
102 }
103}
104
105#[derive(Clone, Message)]
107pub struct LocaleChangeEvent(pub LanguageIdentifier);
108
109#[derive(Clone, Message)]
111pub struct LocaleChangedEvent(pub LanguageIdentifier);
112
113#[derive(Clone, Default, Resource)]
115pub struct I18nAssets {
116 pub assets: HashMap<(LanguageIdentifier, String), Handle<FtlAsset>>,
118 pub loaded_resources: HashMap<(LanguageIdentifier, String), Arc<FluentResource>>,
120}
121
122type SyncFluentBundle =
123 FluentBundle<Arc<FluentResource>, intl_memoizer::concurrent::IntlLangMemoizer>;
124
125#[derive(Clone, Default, Resource)]
127pub struct I18nBundle(pub HashMap<LanguageIdentifier, Arc<SyncFluentBundle>>);
128
129impl I18nAssets {
130 pub fn new() -> Self {
132 Self::default()
133 }
134
135 pub fn add_asset(
137 &mut self,
138 lang: LanguageIdentifier,
139 domain: String,
140 handle: Handle<FtlAsset>,
141 ) {
142 self.assets.insert((lang, domain), handle);
143 }
144
145 pub fn is_language_loaded(&self, lang: &LanguageIdentifier) -> bool {
147 self.assets
148 .keys()
149 .filter(|(l, _)| l == lang)
150 .all(|key| self.loaded_resources.contains_key(key))
151 }
152
153 pub fn get_language_resources(&self, lang: &LanguageIdentifier) -> Vec<&Arc<FluentResource>> {
155 self.loaded_resources
156 .iter()
157 .filter_map(
158 |((l, _), resource)| {
159 if l == lang { Some(resource) } else { None }
160 },
161 )
162 .collect()
163 }
164
165 pub fn available_languages(&self) -> Vec<LanguageIdentifier> {
167 let mut seen = std::collections::HashSet::new();
168 let mut languages = Vec::new();
169
170 for (lang, _) in self.assets.keys() {
171 if seen.insert(lang.clone()) {
172 languages.push(lang.clone());
173 }
174 }
175
176 languages.sort_by_key(|lang| lang.to_string());
177 languages
178 }
179}
180
181#[derive(Resource)]
183pub struct I18nResource {
184 current_language: LanguageIdentifier,
185}
186
187impl I18nResource {
188 pub fn new(initial_language: LanguageIdentifier) -> Self {
190 Self {
191 current_language: initial_language,
192 }
193 }
194
195 pub fn current_language(&self) -> &LanguageIdentifier {
197 &self.current_language
198 }
199
200 pub fn set_language(&mut self, lang: LanguageIdentifier) {
202 self.current_language = lang;
203 }
204
205 pub fn localize<'a>(
209 &self,
210 id: &str,
211 args: Option<&HashMap<&str, FluentValue<'a>>>,
212 i18n_bundle: &I18nBundle,
213 ) -> Option<String> {
214 let bundle = i18n_bundle.0.get(&self.current_language)?;
215
216 let message = bundle.get_message(id)?;
217 let pattern = message.value()?;
218
219 let mut errors = Vec::new();
220 let fluent_args = args.map(|args| {
221 let mut fa = fluent_bundle::FluentArgs::new();
222 for (key, value) in args {
223 fa.set(*key, value.clone());
224 }
225 fa
226 });
227
228 let value = bundle.format_pattern(pattern, fluent_args.as_ref(), &mut errors);
229
230 if !errors.is_empty() {
231 error!("Fluent formatting errors for '{}': {:?}", id, errors);
232 }
233
234 Some(value.into_owned())
235 }
236}
237
238pub fn localize<'a>(
244 i18n_resource: &I18nResource,
245 i18n_bundle: &I18nBundle,
246 id: &str,
247 args: Option<&HashMap<&str, FluentValue<'a>>>,
248) -> String {
249 i18n_resource
250 .localize(id, args, i18n_bundle)
251 .unwrap_or_else(|| {
252 warn!("Translation for '{}' not found", id);
253 id.to_string()
254 })
255}
256
257pub fn update_values_on_locale_change<T>(
260 mut locale_changed_events: MessageReader<LocaleChangedEvent>,
261 mut query: Query<&mut FluentText<T>>,
262) where
263 T: RefreshForLocale + ToFluentString + Clone + Component,
264{
265 for event in locale_changed_events.read() {
266 for mut fluent_text in query.iter_mut() {
267 fluent_text.value.refresh_for_locale(&event.0);
268 }
269 }
270}
271
272pub struct EsFluentBevyPlugin;
274
275impl Plugin for EsFluentBevyPlugin {
276 fn build(&self, _app: &mut App) {
277 debug!("EsFluentBevyPlugin initialized");
278 }
279}
280
281pub trait BevyFluentTextRegistration: Send + Sync {
286 fn register(&self, app: &mut App);
288}
289
290inventory::collect!(&'static dyn BevyFluentTextRegistration);
291
292pub trait FluentTextRegistration {
294 fn register_fluent_text<
296 T: es_fluent::ToFluentString + Clone + Component + Send + Sync + 'static,
297 >(
298 &mut self,
299 ) -> &mut Self;
300
301 fn register_fluent_text_from_locale<
306 T: es_fluent::ToFluentString + Clone + Component + RefreshForLocale + Send + Sync + 'static,
307 >(
308 &mut self,
309 ) -> &mut Self;
310}
311
312impl FluentTextRegistration for App {
313 fn register_fluent_text<
314 T: es_fluent::ToFluentString + Clone + Component + Send + Sync + 'static,
315 >(
316 &mut self,
317 ) -> &mut Self {
318 self.add_systems(
319 PostUpdate,
320 (
321 crate::systems::update_all_fluent_text_on_locale_change::<T>,
322 crate::systems::update_fluent_text_system::<T>,
323 )
324 .chain(),
325 );
326 self
327 }
328
329 fn register_fluent_text_from_locale<
330 T: es_fluent::ToFluentString + Clone + Component + RefreshForLocale + Send + Sync + 'static,
331 >(
332 &mut self,
333 ) -> &mut Self {
334 self.add_systems(
335 PostUpdate,
336 (
337 crate::update_values_on_locale_change::<T>,
338 crate::systems::update_fluent_text_system::<T>,
339 )
340 .chain(),
341 );
342 self
343 }
344}
345
346pub use unic_langid::langid;