use std::collections::HashMap;
use std::fs::read_dir;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::path::Path;
use fluent_bundle::{FluentBundle, FluentResource, FluentValue};
use fluent_locale::negotiate_languages;
pub trait Loader {
fn lookup(
&self,
lang: &str,
text_id: &str,
args: Option<&HashMap<&str, FluentValue>>,
) -> String;
}
#[macro_export]
macro_rules! simple_loader {
($constructor:ident, $location:expr, $fallback:expr) => {
$crate::lazy_static::lazy_static! {
static ref RESOURCES: std::collections::HashMap<String, Vec<$crate::fluent_bundle::FluentResource>> = $crate::loader::build_resources($location);
static ref BUNDLES: std::collections::HashMap<String, $crate::fluent_bundle::FluentBundle<'static>> = $crate::loader::build_bundles(&&RESOURCES, None, |_bundle| {});
static ref LOCALES: Vec<&'static str> = RESOURCES.iter().map(|(l, _)| &**l).collect();
static ref FALLBACKS: std::collections::HashMap<String, Vec<String>> = $crate::loader::build_fallbacks(&LOCALES);
}
pub fn $constructor() -> $crate::loader::SimpleLoader {
$crate::loader::SimpleLoader::new(&*BUNDLES, &*FALLBACKS, $fallback.into())
}
};
($constructor:ident, $location:expr, $fallback:expr, core: $core:expr, customizer: $custom:expr) => {
$crate::lazy_static::lazy_static! {
static ref CORE_RESOURCE: $crate::fluent_bundle::FluentResource = $crate::loader::load_core_resource($core);
static ref RESOURCES: std::collections::HashMap<String, Vec<$crate::fluent_bundle::FluentResource>> = $crate::loader::build_resources($location);
static ref BUNDLES: std::collections::HashMap<String, $crate::fluent_bundle::FluentBundle<'static>> = $crate::loader::build_bundles(&*RESOURCES, Some(&CORE_RESOURCE), $custom);
static ref LOCALES: Vec<&'static str> = RESOURCES.iter().map(|(l, _)| &**l).collect();
static ref FALLBACKS: std::collections::HashMap<String, Vec<String>> = $crate::loader::build_fallbacks(&LOCALES);
}
pub fn $constructor() -> $crate::loader::SimpleLoader {
$crate::loader::SimpleLoader::new(&*BUNDLES, &*FALLBACKS, $fallback.into())
}
};
}
pub fn build_fallbacks(locales: &[&str]) -> HashMap<String, Vec<String>> {
locales
.iter()
.map(|locale| {
(
locale.to_string(),
negotiate_languages(
&[locale],
&locales,
None,
&fluent_locale::NegotiationStrategy::Filtering,
)
.into_iter()
.map(|x| x.to_string())
.collect(),
)
})
.collect()
}
pub struct SimpleLoader {
bundles: &'static HashMap<String, FluentBundle<'static>>,
fallbacks: &'static HashMap<String, Vec<String>>,
fallback: String,
}
impl SimpleLoader {
pub fn new(
bundles: &'static HashMap<String, FluentBundle<'static>>,
fallbacks: &'static HashMap<String, Vec<String>>,
fallback: String,
) -> Self {
Self {
bundles,
fallbacks,
fallback,
}
}
pub fn lookup_single_language(
&self,
lang: &str,
text_id: &str,
args: Option<&HashMap<&str, FluentValue>>,
) -> Option<String> {
if let Some(bundle) = self.bundles.get(lang) {
if bundle.has_message(text_id) {
let (value, _errors) = bundle.format(text_id, args).unwrap_or_else(|| {
panic!(
"Failed to format a message for locale {} and id {}",
lang, text_id
)
});
Some(value)
} else {
None
}
} else {
panic!("Unknown language {}", lang)
}
}
pub fn lookup_no_default_fallback(
&self,
lang: &str,
text_id: &str,
args: Option<&HashMap<&str, FluentValue>>,
) -> Option<String> {
for l in self.fallbacks.get(lang).expect("language not found") {
if let Some(val) = self.lookup_single_language(l, text_id, args) {
return Some(val);
}
}
None
}
}
impl Loader for SimpleLoader {
fn lookup(
&self,
lang: &str,
text_id: &str,
args: Option<&HashMap<&str, FluentValue>>,
) -> String {
for l in self.fallbacks.get(lang).expect("language not found") {
if let Some(val) = self.lookup_single_language(l, text_id, args) {
return val;
}
}
if lang != self.fallback {
if let Some(val) = self.lookup_single_language(&self.fallback, text_id, args) {
return val;
}
}
format!("Unknown localization {}", text_id)
}
}
fn read_from_file<P: AsRef<Path>>(filename: P) -> io::Result<FluentResource> {
let mut file = File::open(filename)?;
let mut string = String::new();
file.read_to_string(&mut string)?;
Ok(FluentResource::try_new(string).expect("File did not parse!"))
}
fn read_from_dir<P: AsRef<Path>>(dirname: P) -> io::Result<Vec<FluentResource>> {
let mut result = Vec::new();
for dir_entry in read_dir(dirname)? {
let entry = dir_entry?;
if entry.path().extension().and_then(|e| e.to_str()) != Some("ftl") {
continue;
}
let resource = read_from_file(entry.path())?;
result.push(resource);
}
Ok(result)
}
pub fn create_bundle<'a>(
lang: &str,
resources: &'a [FluentResource],
core_resource: Option<&'a FluentResource>,
customizer: &impl Fn(&mut FluentBundle),
) -> FluentBundle<'a> {
let mut bundle = FluentBundle::new(&[lang]);
if let Some(core) = core_resource {
bundle
.add_resource(core)
.expect("Failed to add core resource to bundle");
}
for res in resources {
bundle
.add_resource(res)
.expect("Failed to add FTL resources to the bundle.");
}
customizer(&mut bundle);
bundle
}
pub fn build_resources(dir: &str) -> HashMap<String, Vec<FluentResource>> {
let mut all_resources = HashMap::new();
let entries = read_dir(dir).unwrap();
for entry in entries {
let entry = entry.unwrap();
if entry.file_type().unwrap().is_dir() {
if let Ok(lang) = entry.file_name().into_string() {
let resources = read_from_dir(entry.path()).unwrap();
all_resources.insert(lang, resources);
}
}
}
all_resources
}
pub fn build_bundles<'a>(
resources: &'a HashMap<String, Vec<FluentResource>>,
core_resource: Option<&'a FluentResource>,
customizer: impl Fn(&mut FluentBundle),
) -> HashMap<String, FluentBundle<'a>> {
let mut bundles = HashMap::new();
for (ref k, ref v) in &*resources {
bundles.insert(
k.to_string(),
create_bundle(&k, &v, core_resource, &customizer),
);
}
bundles
}
pub fn load_core_resource(path: &str) -> FluentResource {
read_from_file(path).expect("cannot find core resource")
}
#[cfg(test)]
mod tests {
use super::*;
use fluent_bundle::FluentBundle;
use std::error::Error;
#[test]
fn test_load_from_dir() -> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
std::fs::write(dir.path().join("core.ftl"), "foo = bar\n".as_bytes())?;
std::fs::write(dir.path().join("other.ftl"), "bar = baz\n".as_bytes())?;
std::fs::write(dir.path().join("invalid.txt"), "baz = foo\n".as_bytes())?;
std::fs::write(dir.path().join(".binary_file.swp"), &[0, 1, 2, 3, 4, 5])?;
let result = read_from_dir(dir.path())?;
assert_eq!(2, result.len());
let mut bundle = FluentBundle::new(&["en-US"]);
for resource in &result {
bundle.add_resource(resource).unwrap();
}
assert_eq!(Some(("bar".into(), Vec::new())), bundle.format("foo", None));
assert_eq!(Some(("baz".into(), Vec::new())), bundle.format("bar", None));
assert_eq!(None, bundle.format("baz", None));
Ok(())
}
}