1use std::{any::type_name, borrow::Cow, ops::Range, slice::Iter};
10
11use bevy_asset::{ReflectAsset, UntypedAssetId, VisitAssetDependencies, prelude::*};
12use bevy_derive::{Deref, DerefMut};
13use bevy_ecs::{
14 component::ComponentId,
15 entity::VisitEntitiesMut,
16 prelude::*,
17 reflect::{ReflectMapEntities, ReflectVisitEntities, ReflectVisitEntitiesMut},
18 world::DeferredWorld,
19};
20use bevy_reflect::prelude::*;
21use bevy_utils::{HashMap, warn_once};
22use scopeguard::{Always, ScopeGuard};
23use smallvec::SmallVec;
24use thiserror::Error;
25
26use crate::arg::LocaleArg;
27
28#[derive(Asset, Reflect, Deref, DerefMut, Debug)]
31#[reflect(Asset, Debug)]
32pub struct Locale(pub HashMap<String, LocaleFmt>);
33impl Locale {
34 pub fn localize_into(&self, key: impl AsRef<str>, args_src: &[&str], out: &mut String) -> Result<(), LocalizeError> {
36 match self.get(key.as_ref()).ok_or(LocalizeError::MissingKey)? {
37 LocaleFmt::Unformatted(res) => {
38 out.clone_from(res);
39 Ok(())
40 }
41 LocaleFmt::Formatted { format, args } => {
42 let len = args.iter().try_fold(0, |mut len, &(ref range, i)| {
43 len += range.end - range.start;
44 len += args_src.get(i).ok_or(LocalizeError::MissingArgument(i))?.len();
45 Ok(len)
46 })?;
47
48 out.clear();
49 out.reserve_exact(len);
50
51 let mut last = 0;
52 for &(ref range, i) in args {
53 let start = range.start.min(format.len());
55 let end = range.end.min(format.len());
56 last = last.max(end);
57
58 out.push_str(&format[start..end]);
60 out.push_str(args_src[i]);
61 }
62 out.push_str(&format[last..]);
63
64 Ok(())
65 }
66 }
67 }
68
69 #[inline]
72 pub fn localize(&self, key: impl AsRef<str>, args_src: &[&str]) -> Result<String, LocalizeError> {
73 let mut out = String::new();
74 self.localize_into(key, args_src, &mut out)?;
75 Ok(out)
76 }
77}
78
79#[derive(Error, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
81pub enum LocalizeError {
82 #[error("The locale doesn't contain the localization for the supplied key.")]
84 MissingKey,
85 #[error("Missing argument at index {0}.")]
87 MissingArgument(usize),
88}
89
90#[derive(Reflect, Clone, Debug)]
144#[reflect(Debug)]
145pub enum LocaleFmt {
146 Unformatted(String),
148 Formatted {
151 format: String,
153 args: Vec<(Range<usize>, usize)>,
155 },
156}
157
158#[derive(Reflect, Debug)]
160#[reflect(Asset, Debug)]
161pub struct LocaleCollection {
162 pub default: String,
164 pub languages: HashMap<String, Handle<Locale>>,
166}
167
168impl Asset for LocaleCollection {}
169impl VisitAssetDependencies for LocaleCollection {
170 #[inline]
171 fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
172 self.languages.values().for_each(|handle| visit(handle.id().untyped()))
173 }
174}
175
176#[derive(Event, Reflect, Clone, Debug)]
178pub struct LocaleChangeEvent(pub String);
179
180#[derive(Component, Reflect, Clone, Deref, DerefMut, Debug)]
185#[component(on_remove = remove_localize)]
186#[require(LocaleResult)]
187#[reflect(Component, Debug)]
188pub struct LocaleKey {
189 #[deref]
192 pub key: Cow<'static, str>,
193 pub collection: Handle<LocaleCollection>,
195}
196
197fn remove_localize(mut world: DeferredWorld, e: Entity, _: ComponentId) {
198 let args = std::mem::take(&mut world.get_mut::<LocaleArgs>(e).unwrap().0);
199 world.commands().entity(e).queue(move |e: Entity, world: &mut World| {
200 world.entity_mut(e).remove::<LocaleArgs>();
201 for arg in args {
202 world.despawn(arg);
203 }
204 });
205}
206
207#[derive(Component, Reflect, Clone, Default, Deref, DerefMut, Debug)]
211#[reflect(Component, Default, Debug)]
212pub struct LocaleResult {
213 #[deref]
216 pub result: String,
217 #[reflect(ignore)]
218 changed: bool,
219 #[reflect(ignore)]
220 locale: AssetId<Locale>,
221}
222
223#[derive(Component, Reflect, Clone, VisitEntitiesMut)]
224#[reflect(Component, MapEntities, VisitEntities, VisitEntitiesMut)]
225pub(crate) struct LocaleArgs(pub SmallVec<[Entity; 4]>);
226impl<'a> IntoIterator for &'a LocaleArgs {
227 type Item = <Self::IntoIter as Iterator>::Item;
228 type IntoIter = Iter<'a, Entity>;
229
230 #[inline]
231 fn into_iter(self) -> Self::IntoIter {
232 self.0.iter()
233 }
234}
235
236#[derive(Component, Reflect, Deref)]
237#[require(LocaleCache)]
238#[reflect(Component)]
239pub(crate) struct LocaleSrc<T: LocaleArg>(pub T);
240
241#[derive(Component, Default)]
242pub(crate) struct LocaleCache {
243 pub result: Option<String>,
244 pub locale: AssetId<Locale>,
245 pub changed: bool,
246}
247
248pub(crate) fn update_locale_asset(
249 mut collection_events: EventReader<AssetEvent<LocaleCollection>>,
250 mut locale_events: EventReader<AssetEvent<Locale>>,
251 mut change_events: EventReader<LocaleChangeEvent>,
252 locales: Res<Assets<LocaleCollection>>,
253 mut localize_query: Query<(Ref<LocaleKey>, &mut LocaleResult, &LocaleArgs)>,
254 mut cache_query: Query<&mut LocaleCache>,
255 mut last: Local<Option<String>>,
256) {
257 let new_id = (!change_events.is_empty()).then(|| {
258 let mut iter = change_events.read();
259 let mut last = iter.next().expect("`events.is_empty()` returned false");
260
261 for next in iter {
262 last = next;
263 }
264
265 &last.0
266 });
267
268 let mut all_change = if let Some(new_id) = new_id {
269 last.get_or_insert_default().clone_from(new_id);
270 true
271 } else {
272 false
273 };
274
275 if !collection_events.is_empty() || !locale_events.is_empty() {
278 collection_events.clear();
279 locale_events.clear();
280 all_change = true;
281 }
282
283 for (loc, mut result, args) in &mut localize_query {
284 if all_change || loc.is_changed() {
285 let locale_id = locales
286 .get(&loc.collection)
287 .and_then(|collection| {
288 collection
289 .languages
290 .get(new_id.unwrap_or(last.as_ref().unwrap_or(&collection.default)))
291 })
292 .map(Handle::id)
293 .unwrap_or_default();
294
295 for &e in args {
296 let Ok(mut cache) = cache_query.get_mut(e) else {
299 continue;
300 };
301
302 if all_change {
303 let cache = cache.bypass_change_detection();
305 cache.changed = true;
306 cache.locale = locale_id;
307 }
308 }
309
310 let result = result.bypass_change_detection();
312 result.changed = true;
313 result.locale = locale_id;
314 }
315 }
316}
317
318pub(crate) fn update_locale_cache<T: LocaleArg>(
319 locales: Res<Assets<Locale>>,
320 mut sources: Query<(Entity, &LocaleSrc<T>, &mut LocaleCache)>,
321) {
322 for (e, src, mut cache) in &mut sources {
323 let cache = cache.bypass_change_detection();
324 if !cache.changed {
325 continue;
326 }
327
328 cache.changed = false;
329
330 let Some(locale) = locales.get(cache.locale) else {
331 cache.result = None;
332 continue;
333 };
334
335 let result = cache.result.get_or_insert_default();
336 result.clear();
337
338 if src.localize_into(locale, result).is_err() {
339 result.clear();
340 warn_once!("An error occurred while trying to format {} in {e}", type_name::<T>());
341 }
342 }
343}
344
345pub(crate) fn update_locale_result(
346 locales: Res<Assets<Locale>>,
347 mut result: Query<(Entity, &LocaleKey, &mut LocaleResult, &LocaleArgs)>,
348 cache_query: Query<&LocaleCache>,
349 mut arguments: Local<Vec<&'static str>>,
350) {
351 #[inline]
361 #[allow(unsafe_op_in_unsafe_fn)]
362 unsafe fn guard<'a, 'this: 'a>(
363 spans: &'this mut Vec<&'static str>,
364 ) -> ScopeGuard<&'this mut Vec<&'a str>, fn(&mut Vec<&'a str>), Always> {
365 ScopeGuard::with_strategy(
367 std::mem::transmute::<&'this mut Vec<&'static str>, &'this mut Vec<&'a str>>(spans),
368 Vec::clear,
369 )
370 }
371
372 let arguments = &mut **unsafe { guard(&mut arguments) };
374 'outer: for (e, loc, result, args) in &mut result {
375 if !result.changed {
376 continue 'outer;
377 };
378
379 let result = result.into_inner();
381 result.changed = false;
382
383 let Some(locale) = locales.get(result.locale) else {
385 result.clear();
386 continue 'outer;
387 };
388
389 arguments.clear();
390 for &arg in args {
391 let Ok(cache) = cache_query.get(arg) else {
394 warn_once!("Locale argument {arg} missing for entity {e}");
395
396 result.clear();
397 continue 'outer;
398 };
399
400 let Some(ref result) = cache.result else {
401 warn_once!("Locale argument {arg} failed to localize for entity {e}");
402
403 result.clear();
404 continue 'outer;
405 };
406
407 arguments.push(result);
408 }
409
410 if let Err(error) = locale.localize_into(&loc.key, arguments, result) {
411 warn_once!("Couldn't localize {e}: {error}");
412 result.clear();
413 }
414 }
415}