1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
use super::resource::load_fluent_resource;
use anyhow::Result;
use bevy::{
    asset::{AssetLoader, AssetPath, LoadContext, LoadedAsset},
    reflect::TypeUuid,
    utils::BoxedFuture,
};
use fluent::{bundle::FluentBundle, FluentArgs, FluentResource};
use intl_memoizer::concurrent::IntlLangMemoizer;
use log::{error, log_enabled, warn, Level};
use serde::Deserialize;
use std::{
    ops::{Deref, DerefMut},
    path::PathBuf,
    str,
};
use typed_builder::TypedBuilder;
use unic_langid::LanguageIdentifier;

/// `FluentBundle` wrapper.
///
/// A collection of resources for a single locale.  
/// If locale fallback chain is empty then it is interlocale bundle.
#[derive(TypeUuid)]
#[uuid = "929113bb-9187-44c3-87be-6027fc3b7ac5"]
pub struct Bundle(FluentBundle<FluentResource, IntlLangMemoizer>);

impl Bundle {
    /// Get message content by query.
    pub fn content(&self, query: &Query) -> Option<String> {
        let message = self.get_message(&query.id)?;
        let pattern = match &query.attribute {
            None => message.value()?,
            Some(key) => message.get_attribute(key)?.value(),
        };
        let mut errors = Vec::new();
        let content = self
            .format_pattern(pattern, query.args.as_ref(), &mut errors)
            .to_string();
        if log_enabled!(Level::Error) {
            for error in errors {
                error!("{}", error);
            }
        }
        Some(content)
    }
}

impl Deref for Bundle {
    type Target = FluentBundle<FluentResource, IntlLangMemoizer>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for Bundle {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

/// Bundle loader.
#[derive(Default)]
pub struct Loader;

impl AssetLoader for Loader {
    fn load<'a>(
        &'a self,
        bytes: &'a [u8],
        load_context: &'a mut LoadContext,
    ) -> BoxedFuture<'a, Result<()>> {
        Box::pin(async move { load_bundle(bytes, load_context).await })
    }

    fn extensions(&self) -> &[&str] {
        &["bundle", "ron"]
    }
}

async fn load_bundle<'a, 'b>(bytes: &'a [u8], load_context: &'a mut LoadContext<'b>) -> Result<()> {
    let Intermediate { locales, resources } = ron::de::from_bytes(bytes)?;
    let mut fluent_bundle = FluentBundle::new_concurrent(locales);
    let mut asset_paths = Vec::new();
    let parent = load_context.path().with_file_name("");
    for mut path in resources {
        if path.is_relative() {
            path = parent.join(path);
        }
        let bytes = load_context.read_asset_bytes(&path).await?;
        let fluent_resource = load_fluent_resource(&bytes).await?;
        if let Err(error) = fluent_bundle.add_resource(fluent_resource) {
            warn!("overriding fluent message: {:?}", error);
        }
        let asset_path = AssetPath::new(path, None);
        asset_paths.push(asset_path.clone());
    }
    load_context
        .set_default_asset(LoadedAsset::new(Bundle(fluent_bundle)).with_dependencies(asset_paths));
    // load_context.set_labeled_asset(
    //     "en-US",
    //     LoadedAsset::new(Bundle {
    //         locales,
    //         resources: handles,
    //     })
    //     .with_dependencies(asset_paths),
    // );
    Ok(())
}

/// Message content query.
#[derive(TypedBuilder)]
pub struct Query<'a> {
    #[builder(setter(into))]
    id: String,
    #[builder(default, setter(into, strip_option))]
    attribute: Option<String>,
    #[builder(default, setter(into, strip_option))]
    args: Option<FluentArgs<'a>>,
}

#[derive(Deserialize)]
struct Intermediate {
    locales: Vec<LanguageIdentifier>,
    resources: Vec<PathBuf>,
}