use std::borrow::Borrow;
use bevy::asset::AssetLoadFailedEvent;
use bevy::prelude::*;
use bevy_cobweb::prelude::*;
use fluent::bundle::FluentBundle;
use fluent::memoizer::MemoizerKind;
use fluent::{FluentArgs, FluentResource};
use fluent_content::Request;
use fluent_langneg::LanguageIdentifier;
use crate::prelude::*;
fn set_content<'a, T, U, V, W>(
bundle: &FluentBundle<V, W>,
template: T,
template_str: &str,
target: &mut String,
) -> bool
where
T: Into<Request<'a, U>>,
U: Borrow<FluentArgs<'a>>,
V: Borrow<FluentResource>,
W: MemoizerKind,
{
let request = template.into();
let request = request.borrow();
let Some(message) = bundle.get_message(request.id) else { return false };
let pattern = match request.attr {
Some(key) => {
let Some(attribute) = message.get_attribute(key) else { return false };
attribute.value()
}
None => {
let Some(value) = message.value() else { return false };
value
}
};
let mut errors = Vec::new();
target.clear();
bundle
.write_pattern(target, pattern, request.args.as_ref().map(Borrow::borrow), &mut errors)
.expect("writing to string failed");
for error in &errors {
tracing::warn!("error while localizing template \"{template_str}\", {:?}", error);
}
true
}
fn handle_new_lang_list(
mut c: Commands,
asset_server: Res<AssetServer>,
manifest: Res<LocalizationManifest>,
mut localizer: ResMut<TextLocalizer>,
)
{
localizer.update_localizations(&manifest, &asset_server);
localizer.try_emit_load_event(&mut c);
}
fn get_localization_data(
mut c: Commands,
mut errors: MessageReader<AssetLoadFailedEvent<FtlBundle>>,
mut events: MessageReader<AssetEvent<FtlBundle>>,
mut assets: ResMut<Assets<FtlBundle>>,
mut localizer: ResMut<TextLocalizer>,
)
{
let started_loading = localizer.is_loading();
let mut fail_count = 0;
let mut refresh_count = 0;
for error in errors.read() {
let AssetLoadFailedEvent { id, .. } = error;
if localizer.remove_failed_load(*id) {
fail_count += 1;
}
}
for event in events.read() {
let id = match event {
AssetEvent::Added { id } | AssetEvent::Modified { id } => id,
_ => {
tracing::debug!("ignoring localization asset event {:?}", event);
continue;
}
};
let Some(ftl_bundle) = assets.remove(*id) else {
tracing::error!("failed to remove localization asset {:?}", id);
continue;
};
if localizer.try_set(*id, ftl_bundle) {
refresh_count += 1;
}
}
if fail_count > 0 || refresh_count > 0 {
localizer.update_is_loading();
}
if started_loading || refresh_count > 0 {
localizer.try_emit_load_event(&mut c);
}
}
enum TextLocalization
{
Loading
{
id: LanguageIdentifier, handle: Handle<FtlBundle>
},
Loaded
{
id: LanguageIdentifier,
asset: FtlBundle,
handle: Handle<FtlBundle>,
},
}
impl TextLocalization
{
fn try_set(&mut self, id: &AssetId<FtlBundle>, asset: FtlBundle) -> Result<(), FtlBundle>
{
if self.handle().id() != *id {
return Err(asset);
}
*self = Self::Loaded {
id: self.lang_id().clone(),
asset,
handle: self.handle().clone(),
};
Ok(())
}
fn lang_id(&self) -> &LanguageIdentifier
{
match self {
Self::Loading { id, .. } => id,
Self::Loaded { id, .. } => id,
}
}
fn handle(&self) -> &Handle<FtlBundle>
{
match self {
Self::Loading { handle, .. } => handle,
Self::Loaded { handle, .. } => handle,
}
}
fn is_loading(&self) -> bool
{
matches!(*self, Self::Loading{..})
}
fn asset(&self) -> Option<(&LanguageIdentifier, &FtlBundle)>
{
let Self::Loaded { id, asset, .. } = self else { return None };
Some((id, asset))
}
}
pub struct TextLocalizerLoaded;
#[derive(Resource)]
pub struct TextLocalizer
{
is_awaiting_renegotiation: bool,
is_loading: bool,
localizations: Vec<TextLocalization>,
}
impl TextLocalizer
{
pub fn is_loading(&self) -> bool
{
self.is_loading
}
fn try_emit_load_event(&mut self, c: &mut Commands)
{
if self.is_loading() {
return;
}
if self.is_awaiting_renegotiation {
self.is_awaiting_renegotiation = false;
return;
}
c.react().broadcast(TextLocalizerLoaded);
}
pub fn localize(&self, template: &str, target: &mut String) -> Option<&LanguageIdentifier>
{
if self.is_loading() {
return None;
}
self.localizations
.iter()
.filter_map(TextLocalization::asset)
.find_map(|(lang, bundle)| {
if set_content(&*bundle, template, template, target) {
Some(lang)
} else {
None
}
})
}
fn update_localizations(&mut self, manifest: &LocalizationManifest, asset_server: &AssetServer)
{
let mut new_localizations = Vec::with_capacity(manifest.negotiated().len());
for negotiated in manifest.iter_negotiated_metas() {
let next = match self
.localizations
.iter()
.position(|l| *l.lang_id() == negotiated.id)
{
Some(idx) => {
let removed = self.localizations.swap_remove(idx);
#[cfg(feature = "hot_reload")]
{
let path: std::path::PathBuf = removed.handle().path().unwrap().clone().into();
if negotiated.manifest != path {
TextLocalization::Loading {
id: negotiated.id.clone(),
handle: asset_server.load(negotiated.manifest.clone()),
}
} else {
removed
}
}
#[cfg(not(feature = "hot_reload"))]
{
removed
}
}
None => TextLocalization::Loading {
id: negotiated.id.clone(),
handle: asset_server.load(negotiated.manifest.clone()),
},
};
new_localizations.push(next);
}
self.localizations = new_localizations;
self.is_awaiting_renegotiation = true;
self.update_is_loading();
}
fn update_is_loading(&mut self)
{
self.is_loading = self.localizations.iter().any(|l| l.is_loading());
}
fn remove_failed_load(&mut self, id: AssetId<FtlBundle>) -> bool
{
let mut removed_id = false;
self.localizations.retain(|l| {
if l.handle().id() == id {
tracing::warn!("failed loading localization data for {:?}", l.lang_id());
removed_id = true;
return false;
}
true
});
removed_id
}
fn try_set(&mut self, id: AssetId<FtlBundle>, mut asset: FtlBundle) -> bool
{
for localization in self.localizations.iter_mut() {
match localization.try_set(&id, asset) {
Ok(()) => return true,
Err(returned_asset) => {
asset = returned_asset;
}
}
}
tracing::warn!("ignoring stale localization bundle {:?}", asset.locale().to_string());
false
}
}
impl Default for TextLocalizer
{
fn default() -> Self
{
Self {
is_awaiting_renegotiation: false,
is_loading: false,
localizations: Vec::default(),
}
}
}
impl AssetLoadProgress for TextLocalizer
{
fn pending_assets(&self) -> usize
{
self.localizations.iter().filter(|l| l.is_loading()).count()
}
fn total_assets(&self) -> usize
{
self.localizations.len()
}
}
pub(crate) struct TextLocalizerPlugin;
impl Plugin for TextLocalizerPlugin
{
fn build(&self, app: &mut App)
{
app.init_resource::<TextLocalizer>()
.register_asset_tracker::<TextLocalizer>()
.react(|rc| rc.on_persistent(broadcast::<LanguagesNegotiated>(), handle_new_lang_list))
.add_systems(First, get_localization_data.after(FileProcessingSet));
}
}