use std::io;
use std::path::PathBuf;
use bevy::asset::io::Reader;
use bevy::asset::{AssetLoader, AsyncReadExt, LoadContext, LoadDirectError};
use bevy::prelude::*;
use bevy::reflect::TypePath;
use fluent::bundle::FluentBundle;
use fluent::FluentResource;
use intl_memoizer::concurrent::IntlLangMemoizer;
use ron::error::SpannedError;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use unic_langid::LanguageIdentifier;
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct FtlBundleData
{
locale: LanguageIdentifier,
resources: Vec<PathBuf>,
}
async fn load_ftl_bundle_contents(
data: FtlBundleData,
load_context: &mut LoadContext<'_>,
) -> Result<FtlBundle, FtlLoadError>
{
let mut bundle = FluentBundle::new_concurrent(vec![data.locale]);
for mut path in data.resources {
if path.is_relative() {
if let Some(parent) = load_context.path().parent() {
path = parent.join(path);
}
}
let loaded = load_context
.loader()
.immediate()
.with_unknown_type()
.load(path)
.await?;
let resource = loaded.take::<FtlResource>().unwrap();
if let Err(errors) = bundle.add_resource(resource.0) {
warn_span!("add_ftl_resource").in_scope(|| {
for error in errors {
warn!(%error);
}
});
}
}
bundle.set_use_isolating(true);
Ok(FtlBundle(bundle))
}
#[derive(Asset, TypePath)]
struct FtlResource(FluentResource);
#[derive(Default)]
struct FtlResourceLoader;
impl AssetLoader for FtlResourceLoader
{
type Asset = FtlResource;
type Settings = ();
type Error = FtlLoadError;
async fn load(
&self,
reader: &mut dyn Reader,
_: &Self::Settings,
_: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error>
{
let mut content = String::new();
reader.read_to_string(&mut content).await?;
let resource = match FluentResource::try_new(content) {
Ok(fluent_resource) => fluent_resource,
Err((fluent_resource, errors)) => {
warn_span!("load_ftl_resource").in_scope(|| {
for error in errors {
warn!(%error);
}
});
fluent_resource
}
};
Ok(FtlResource(resource))
}
fn extensions(&self) -> &[&str]
{
&["ftl"]
}
}
#[derive(Default)]
struct FtlBundleLoader;
impl AssetLoader for FtlBundleLoader
{
type Asset = FtlBundle;
type Settings = ();
type Error = FtlLoadError;
async fn load(
&self,
reader: &mut dyn Reader,
_: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error>
{
let path = load_context.path();
let mut content = String::new();
reader.read_to_string(&mut content).await?;
match path.extension() {
Some(extension) if extension == "ron" => {
load_ftl_bundle_contents(ron::de::from_str(&content)?, load_context).await
}
Some(extension) if extension == "yaml" || extension == "yml" => {
load_ftl_bundle_contents(serde_yaml::from_str(&content)?, load_context).await
}
_ => unreachable!("We already checked all the supported extensions."),
}
}
fn extensions(&self) -> &[&str]
{
&["ftl.ron", "ftl.yaml", "ftl.yml"]
}
}
#[derive(Debug, Error)]
enum FtlLoadError
{
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
LoadDirect(#[from] LoadDirectError),
#[error(transparent)]
Ron(#[from] SpannedError),
#[error(transparent)]
Yaml(#[from] serde_yaml::Error),
}
#[derive(Asset, TypePath, Deref)]
pub(crate) struct FtlBundle(FluentBundle<FluentResource, IntlLangMemoizer>);
impl FtlBundle
{
pub(crate) fn locale(&self) -> &LanguageIdentifier
{
&self.locales[0]
}
fn _add_formatting_fallback(&mut self, locale: LanguageIdentifier)
{
self.0.locales.push(locale);
}
}
pub(crate) struct FtlBundleAssetLoaderPlugin;
impl Plugin for FtlBundleAssetLoaderPlugin
{
fn build(&self, app: &mut App)
{
app.init_asset::<FtlResource>()
.init_asset::<FtlBundle>()
.register_asset_loader(FtlResourceLoader)
.register_asset_loader(FtlBundleLoader);
}
}