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)]
178#[reflect(Debug)]
179pub struct LocaleChangeEvent(pub String);
180
181#[derive(Component, Reflect, Clone, Deref, DerefMut, Debug)]
186#[component(on_remove = remove_localize)]
187#[require(LocaleResult)]
188#[reflect(Component, Debug)]
189pub struct LocaleKey {
190 #[deref]
193 pub key: Cow<'static, str>,
194 pub collection: Handle<LocaleCollection>,
196}
197
198fn remove_localize(mut world: DeferredWorld, e: Entity, _: ComponentId) {
199 let args = std::mem::take(&mut world.get_mut::<LocaleArgs>(e).unwrap().0);
200 world.commands().entity(e).queue(move |e: Entity, world: &mut World| {
201 world.entity_mut(e).remove::<LocaleArgs>();
202 for arg in args {
203 world.despawn(arg);
204 }
205 });
206}
207
208#[derive(Component, Reflect, Clone, Default, Deref, DerefMut, Debug)]
212#[reflect(Component, Default, Debug)]
213pub struct LocaleResult {
214 #[deref]
217 pub result: String,
218 #[reflect(ignore)]
219 changed: bool,
220 #[reflect(ignore)]
221 locale: AssetId<Locale>,
222}
223
224#[derive(Component, Reflect, Clone, VisitEntitiesMut)]
225#[reflect(Component, MapEntities, VisitEntities, VisitEntitiesMut)]
226pub(crate) struct LocaleArgs(pub SmallVec<[Entity; 4]>);
227impl<'a> IntoIterator for &'a LocaleArgs {
228 type Item = <Self::IntoIter as Iterator>::Item;
229 type IntoIter = Iter<'a, Entity>;
230
231 #[inline]
232 fn into_iter(self) -> Self::IntoIter {
233 self.0.iter()
234 }
235}
236
237#[derive(Component, Reflect, Deref)]
238#[require(LocaleCache)]
239#[reflect(Component)]
240pub(crate) struct LocaleSrc<T: LocaleArg>(pub T);
241
242#[derive(Component, Default)]
243pub(crate) struct LocaleCache {
244 pub result: Option<String>,
245 pub locale: AssetId<Locale>,
246 pub changed: bool,
247}
248
249pub(crate) fn update_locale_asset(
250 mut collection_events: EventReader<AssetEvent<LocaleCollection>>,
251 mut locale_events: EventReader<AssetEvent<Locale>>,
252 mut change_events: EventReader<LocaleChangeEvent>,
253 locales: Res<Assets<LocaleCollection>>,
254 mut localize_query: Query<(Ref<LocaleKey>, &mut LocaleResult, &LocaleArgs)>,
255 mut cache_query: Query<&mut LocaleCache>,
256 mut last: Local<Option<String>>,
257) {
258 let new_id = (!change_events.is_empty()).then(|| {
259 let mut iter = change_events.read();
260 let mut last = iter.next().expect("`events.is_empty()` returned false");
261
262 for next in iter {
263 last = next;
264 }
265
266 &last.0
267 });
268
269 let mut all_change = if let Some(new_id) = new_id {
270 last.get_or_insert_default().clone_from(new_id);
271 true
272 } else {
273 false
274 };
275
276 if !collection_events.is_empty() || !locale_events.is_empty() {
279 collection_events.clear();
280 locale_events.clear();
281 all_change = true;
282 }
283
284 for (loc, mut result, args) in &mut localize_query {
285 if all_change || loc.is_changed() {
286 let locale_id = locales
287 .get(&loc.collection)
288 .and_then(|collection| {
289 collection
290 .languages
291 .get(new_id.unwrap_or(last.as_ref().unwrap_or(&collection.default)))
292 })
293 .map(Handle::id)
294 .unwrap_or_default();
295
296 for &e in args {
297 let Ok(mut cache) = cache_query.get_mut(e) else {
300 continue;
301 };
302
303 if all_change {
304 let cache = cache.bypass_change_detection();
306 cache.changed = true;
307 cache.locale = locale_id;
308 }
309 }
310
311 let result = result.bypass_change_detection();
313 result.changed = true;
314 result.locale = locale_id;
315 }
316 }
317}
318
319pub(crate) fn update_locale_cache<T: LocaleArg>(
320 locales: Res<Assets<Locale>>,
321 mut sources: Query<(Entity, &LocaleSrc<T>, &mut LocaleCache)>,
322) {
323 for (e, src, mut cache) in &mut sources {
324 let cache = cache.bypass_change_detection();
325 if !cache.changed {
326 continue;
327 }
328
329 cache.changed = false;
330
331 let Some(locale) = locales.get(cache.locale) else {
332 cache.result = None;
333 continue;
334 };
335
336 let result = cache.result.get_or_insert_default();
337 result.clear();
338
339 if src.localize_into(locale, result).is_err() {
340 result.clear();
341 warn_once!("An error occurred while trying to format {} in {e}", type_name::<T>());
342 }
343 }
344}
345
346pub(crate) fn update_locale_result(
347 locales: Res<Assets<Locale>>,
348 mut result: Query<(Entity, &LocaleKey, &mut LocaleResult, &LocaleArgs)>,
349 cache_query: Query<&LocaleCache>,
350 mut arguments: Local<Vec<&'static str>>,
351) {
352 #[inline]
362 #[allow(unsafe_op_in_unsafe_fn)]
363 unsafe fn guard<'a, 'this: 'a>(
364 spans: &'this mut Vec<&'static str>,
365 ) -> ScopeGuard<&'this mut Vec<&'a str>, fn(&mut Vec<&'a str>), Always> {
366 ScopeGuard::with_strategy(
368 std::mem::transmute::<&'this mut Vec<&'static str>, &'this mut Vec<&'a str>>(spans),
369 Vec::clear,
370 )
371 }
372
373 let arguments = &mut **unsafe { guard(&mut arguments) };
375 'outer: for (e, loc, result, args) in &mut result {
376 if !result.changed {
377 continue 'outer;
378 };
379
380 let result = result.into_inner();
382 result.changed = false;
383
384 let Some(locale) = locales.get(result.locale) else {
386 result.clear();
387 continue 'outer;
388 };
389
390 arguments.clear();
391 for &arg in args {
392 let Ok(cache) = cache_query.get(arg) else {
395 warn_once!("Locale argument {arg} missing for entity {e}");
396
397 result.clear();
398 continue 'outer;
399 };
400
401 let Some(ref result) = cache.result else {
402 warn_once!("Locale argument {arg} failed to localize for entity {e}");
403
404 result.clear();
405 continue 'outer;
406 };
407
408 arguments.push(result);
409 }
410
411 if let Err(error) = locale.localize_into(&loc.key, arguments, result) {
412 warn_once!("Couldn't localize {e}: {error}");
413 result.clear();
414 }
415 }
416}