use elsa::FrozenMap;
use fluent_bundle::{FluentBundle, FluentResource};
use fluent_fallback::{
generator::{BundleGenerator, FluentBundleResult},
types::ResourceId,
};
use futures::stream::Stream;
use rustc_hash::FxHashSet;
use std::io;
use std::{fs, iter};
use thiserror::Error;
use unic_langid::LanguageIdentifier;
fn read_file(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path)
}
pub struct ResourceManager {
resources: FrozenMap<String, Box<FluentResource>>,
path_scheme: String,
}
impl ResourceManager {
pub fn new(path_scheme: String) -> Self {
ResourceManager {
resources: FrozenMap::new(),
path_scheme,
}
}
fn get_resource(
&self,
resource_id: &str,
locale: &str,
) -> Result<&FluentResource, ResourceManagerError> {
let path = self
.path_scheme
.replace("{locale}", locale)
.replace("{res_id}", resource_id);
Ok(if let Some(resource) = self.resources.get(&path) {
resource
} else {
let resource = match FluentResource::try_new(read_file(&path)?) {
Ok(resource) => resource,
Err((resource, _err)) => resource,
};
self.resources.insert(path.to_string(), Box::new(resource))
})
}
pub fn get_bundle(
&self,
locales: Vec<LanguageIdentifier>,
resource_ids: Vec<String>,
) -> Result<FluentBundle<&FluentResource>, Vec<ResourceManagerError>> {
let mut errors: Vec<ResourceManagerError> = vec![];
let mut bundle = FluentBundle::new(locales.clone());
let locale = &locales[0];
for resource_id in &resource_ids {
match self.get_resource(resource_id, &locale.to_string()) {
Ok(resource) => {
if let Err(errs) = bundle.add_resource(resource) {
for error in errs {
errors.push(ResourceManagerError::Fluent(error));
}
}
}
Err(error) => errors.push(error),
};
}
if errors.is_empty() {
Ok(bundle)
} else {
Err(errors)
}
}
pub fn get_bundles(
&self,
locales: Vec<LanguageIdentifier>,
resource_ids: Vec<String>,
) -> impl Iterator<Item = Result<FluentBundle<&FluentResource>, Vec<ResourceManagerError>>>
{
let mut idx = 0;
iter::from_fn(move || {
locales.get(idx).map(|locale| {
idx += 1;
let mut errors: Vec<ResourceManagerError> = vec![];
let mut bundle = FluentBundle::new(vec![locale.clone()]);
for resource_id in &resource_ids {
match self.get_resource(resource_id, &locale.to_string()) {
Ok(resource) => {
if let Err(errs) = bundle.add_resource(resource) {
for error in errs {
errors.push(ResourceManagerError::Fluent(error));
}
}
}
Err(error) => errors.push(error),
}
}
if !errors.is_empty() {
Err(errors)
} else {
Ok(bundle)
}
})
})
}
}
#[derive(Debug, Error)]
pub enum ResourceManagerError {
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Fluent(#[from] fluent_bundle::FluentError),
}
pub struct BundleIter {
locales: <Vec<LanguageIdentifier> as IntoIterator>::IntoIter,
res_ids: FxHashSet<ResourceId>,
}
impl Iterator for BundleIter {
type Item = FluentBundleResult<FluentResource>;
fn next(&mut self) -> Option<Self::Item> {
let locale = self.locales.next()?;
let mut bundle = FluentBundle::new(vec![locale.clone()]);
for res_id in self.res_ids.iter() {
let full_path = format!("./tests/resources/{}/{}", locale, res_id);
let source = fs::read_to_string(full_path).unwrap();
let res = FluentResource::try_new(source).unwrap();
bundle.add_resource(res).unwrap();
}
Some(Ok(bundle))
}
}
impl Stream for BundleIter {
type Item = FluentBundleResult<FluentResource>;
fn poll_next(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
todo!()
}
}
impl BundleGenerator for ResourceManager {
type Resource = FluentResource;
type LocalesIter = std::vec::IntoIter<LanguageIdentifier>;
type Iter = BundleIter;
type Stream = BundleIter;
fn bundles_iter(
&self,
locales: Self::LocalesIter,
res_ids: FxHashSet<ResourceId>,
) -> Self::Iter {
BundleIter { locales, res_ids }
}
fn bundles_stream(
&self,
_locales: Self::LocalesIter,
_res_ids: FxHashSet<ResourceId>,
) -> Self::Stream {
todo!()
}
}
#[cfg(test)]
mod test {
use super::*;
use unic_langid::langid;
#[test]
fn caching() {
let res_mgr = ResourceManager::new("./tests/resources/{locale}/{res_id}".into());
let _bundle = res_mgr.get_bundle(vec![langid!("en-US")], vec!["test.ftl".into()]);
let res_1 = res_mgr
.get_resource("test.ftl", "en-US")
.expect("Could not get resource");
let _bundle2 = res_mgr.get_bundle(vec![langid!("en-US")], vec!["test.ftl".into()]);
let res_2 = res_mgr
.get_resource("test.ftl", "en-US")
.expect("Could not get resource");
assert!(
std::ptr::eq(res_1, res_2),
"The resources are cached in memory and reference the same thing."
);
}
#[test]
fn get_resource_error() {
let res_mgr = ResourceManager::new("./tests/resources/{locale}/{res_id}".into());
let _bundle = res_mgr.get_bundle(vec![langid!("en-US")], vec!["test.ftl".into()]);
let res = res_mgr.get_resource("nonexistent.ftl", "en-US");
assert!(res.is_err());
}
#[test]
fn get_bundle_error() {
let res_mgr = ResourceManager::new("./tests/resources/{locale}/{res_id}".into());
let bundle = res_mgr.get_bundle(vec![langid!("en-US")], vec!["nonexistent.ftl".into()]);
assert!(bundle.is_err());
}
#[test]
fn get_bundle_ignores_errors() {
let res_mgr = ResourceManager::new("./tests/resources/{locale}/{res_id}".into());
let bundle = res_mgr
.get_bundle(
vec![langid!("en-US")],
vec!["test.ftl".into(), "invalid.ftl".into()],
)
.expect("Could not retrieve bundle");
let mut errors = vec![];
let msg = bundle.get_message("hello-world").expect("Message exists");
let pattern = msg.value().expect("Message has a value");
let value = bundle.format_pattern(pattern, None, &mut errors);
assert_eq!(value, "Hello World");
assert!(errors.is_empty());
let mut errors = vec![];
let msg = bundle.get_message("valid-message").expect("Message exists");
let pattern = msg.value().expect("Message has a value");
let value = bundle.format_pattern(pattern, None, &mut errors);
assert_eq!(value, "This is a valid message");
assert!(errors.is_empty());
}
}