use std::{
collections::HashMap,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use fluent::{FluentMessage, FluentResource};
use intl_memoizer::concurrent::IntlLangMemoizer;
use smallvec::SmallVec;
use unic_langid::{LanguageIdentifier, langid};
use crate::{Result, error::I18NError};
type FluentBundle = fluent::bundle::FluentBundle<FluentResource, IntlLangMemoizer>;
use fluent_langneg::NegotiationStrategy;
use crate::i18n::I18NArgs;
struct InnerResources {
available_languages: Vec<LanguageIdentifier>,
bundles: HashMap<LanguageIdentifier, Arc<FluentBundle>>,
default_language: LanguageIdentifier,
strategy: NegotiationStrategy,
}
pub struct I18NResourcesBuilder {
paths: Vec<PathBuf>,
resources: Vec<(String, String)>,
default_language: LanguageIdentifier,
strategy: NegotiationStrategy,
}
impl I18NResourcesBuilder {
#[must_use]
pub fn add_path(mut self, path: impl Into<PathBuf>) -> Self {
self.paths.push(path.into());
self
}
#[must_use]
pub fn add_ftl(mut self, language: impl Into<String>, ftl: impl Into<String>) -> Self {
self.resources.push((language.into(), ftl.into()));
self
}
#[must_use]
pub fn default_language(mut self, language: LanguageIdentifier) -> Self {
self.default_language = language;
self
}
#[must_use]
pub fn negotiation_strategy(mut self, strategy: NegotiationStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn build(self) -> Result<I18NResources, I18NError> {
let mut bundles = HashMap::new();
for path in self.paths {
load_resources_from_path(&mut bundles, path)?;
}
for (language, ftl) in self.resources {
let language = LanguageIdentifier::from_str(&language)?;
let resource = FluentResource::try_new(ftl)
.map_err(|(_, errors)| I18NError::FluentParser(errors))?;
bundles
.entry(language.clone())
.or_insert_with(|| FluentBundle::new_concurrent(vec![language]))
.add_resource(resource)
.map_err(I18NError::Fluent)?;
}
Ok(I18NResources {
inner: Arc::new(InnerResources {
available_languages: bundles.keys().cloned().collect(),
bundles: bundles
.into_iter()
.map(|(key, value)| (key, Arc::new(value)))
.collect(),
default_language: self.default_language,
strategy: self.strategy,
}),
})
}
}
fn load_resources_from_path(
bundles: &mut HashMap<LanguageIdentifier, FluentBundle>,
path: impl AsRef<Path>,
) -> Result<(), I18NError> {
let path = path.as_ref();
let languages = std::fs::read_dir(path)?;
for res in languages {
let language_dir = res?;
let language = match language_dir
.path()
.file_name()
.and_then(|name| name.to_str())
.and_then(|name| LanguageIdentifier::from_str(name).ok())
{
Some(language_path) => language_path,
None => continue,
};
let resources = std::fs::read_dir(language_dir.path())?;
for res in resources {
let resource_path = res?;
tracing::debug!(path = ?resource_path.path(), "load fluent resource");
let resource = FluentResource::try_new(std::fs::read_to_string(resource_path.path())?)
.map_err(|(_, errors)| I18NError::FluentParser(errors))?;
bundles
.entry(language.clone())
.or_insert_with(|| FluentBundle::new_concurrent(vec![language.clone()]))
.add_resource(resource)
.map_err(I18NError::Fluent)?;
}
}
Ok(())
}
#[derive(Clone)]
pub struct I18NResources {
inner: Arc<InnerResources>,
}
impl I18NResources {
pub fn builder() -> I18NResourcesBuilder {
I18NResourcesBuilder {
paths: vec![],
resources: vec![],
default_language: langid!("en-US"),
strategy: NegotiationStrategy::Filtering,
}
}
pub fn negotiate_languages(&self, languages: &[impl AsRef<LanguageIdentifier>]) -> I18NBundle {
let resolved_languages = fluent_langneg::negotiate_languages(
languages,
&self.inner.available_languages,
Some(&self.inner.default_language),
self.inner.strategy,
);
I18NBundle(
resolved_languages
.into_iter()
.filter_map(|language| self.inner.bundles.get(language))
.cloned()
.collect(),
)
}
}
pub struct I18NBundle(SmallVec<[Arc<FluentBundle>; 8]>);
impl I18NBundle {
fn message(
&self,
id: impl AsRef<str>,
) -> Result<(&'_ FluentBundle, FluentMessage<'_>), I18NError> {
let id = id.as_ref();
for bundle in &self.0 {
if let Some(message) = bundle.get_message(id) {
return Ok((bundle, message));
}
}
Err(I18NError::FluentMessageNotFound { id: id.to_string() })
}
pub fn text_with_args<'a>(
&self,
id: impl AsRef<str>,
args: impl Into<I18NArgs<'a>>,
) -> Result<String, I18NError> {
let mut errors = Vec::new();
let (bundle, message) = self.message(id.as_ref())?;
let args = args.into();
let value = message.value().ok_or(I18NError::FluentNoValue)?;
let s = bundle.format_pattern(value, Some(&args.0), &mut errors);
if !errors.is_empty() {
return Err(I18NError::Fluent(errors));
}
Ok(s.into_owned())
}
pub fn text(&self, id: impl AsRef<str>) -> Result<String, I18NError> {
self.text_with_args(id, I18NArgs::default())
}
}