use crate::{I18nAssets, I18nEmbedError, LanguageLoader};
use arc_swap::ArcSwap;
pub use fluent_langneg::NegotiationStrategy;
pub use i18n_embed_impl::fluent_language_loader;
use fluent::{
bundle::FluentBundle, FluentArgs, FluentAttribute, FluentMessage, FluentResource, FluentValue,
};
use fluent_syntax::ast::{self, Pattern};
use intl_memoizer::concurrent::IntlLangMemoizer;
use parking_lot::RwLock;
use std::{borrow::Cow, collections::HashMap, fmt::Debug, iter::FromIterator, sync::Arc};
use unic_langid::LanguageIdentifier;
struct LanguageBundle {
language: LanguageIdentifier,
bundle: FluentBundle<Arc<FluentResource>, IntlLangMemoizer>,
resource: Arc<FluentResource>,
}
impl LanguageBundle {
fn new(language: LanguageIdentifier, resource: FluentResource) -> Self {
let mut bundle = FluentBundle::new_concurrent(vec![language.clone()]);
let resource = Arc::new(resource);
if let Err(errors) = bundle.add_resource(resource.clone()) {
errors.iter().for_each(|error | {
log::error!(target: "i18n_embed::fluent", "Error while adding resource to bundle: {0:?}.", error);
})
}
Self {
language,
bundle,
resource,
}
}
}
impl Debug for LanguageBundle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "LanguageBundle(language: {})", self.language)
}
}
#[derive(Debug)]
struct LanguageConfig {
language_bundles: Vec<LanguageBundle>,
language_map: HashMap<LanguageIdentifier, usize>,
}
#[derive(Debug)]
struct CurrentLanguages {
languages: Vec<LanguageIdentifier>,
indices: Vec<usize>,
}
#[derive(Debug)]
struct FluentLanguageLoaderInner {
language_config: Arc<RwLock<LanguageConfig>>,
current_languages: CurrentLanguages,
}
#[derive(Debug)]
pub struct FluentLanguageLoader {
inner: ArcSwap<FluentLanguageLoaderInner>,
domain: String,
fallback_language: unic_langid::LanguageIdentifier,
}
impl FluentLanguageLoader {
pub fn new<S: Into<String>>(
domain: S,
fallback_language: unic_langid::LanguageIdentifier,
) -> Self {
let config = LanguageConfig {
language_bundles: Vec::new(),
language_map: HashMap::new(),
};
Self {
inner: ArcSwap::new(Arc::new(FluentLanguageLoaderInner {
language_config: Arc::new(RwLock::new(config)),
current_languages: CurrentLanguages {
languages: vec![fallback_language.clone()],
indices: vec![],
},
})),
domain: domain.into(),
fallback_language,
}
}
fn current_language_impl(
&self,
inner: &FluentLanguageLoaderInner,
) -> unic_langid::LanguageIdentifier {
inner
.current_languages
.languages
.first()
.map_or_else(|| self.fallback_language.clone(), Clone::clone)
}
pub fn current_languages(&self) -> Vec<unic_langid::LanguageIdentifier> {
self.inner.load().current_languages.languages.clone()
}
pub fn get(&self, message_id: &str) -> String {
self.get_args_fluent(message_id, None)
}
pub fn get_args_concrete<'args>(
&self,
message_id: &str,
args: HashMap<&'args str, FluentValue<'args>>,
) -> String {
self.get_args_fluent(message_id, hash_map_to_fluent_args(args).as_ref())
}
pub fn get_args_fluent<'args>(
&self,
message_id: &str,
args: Option<&'args FluentArgs<'args>>,
) -> String {
let inner = self.inner.load();
let language_config = inner.language_config.read();
inner
.current_languages
.indices
.iter()
.map(|&idx| &language_config.language_bundles[idx])
.find_map(|language_bundle| language_bundle
.bundle
.get_message(message_id)
.and_then(|m: FluentMessage<'_>| m.value())
.map(|pattern: &Pattern<&str>| {
let mut errors = Vec::new();
let value = language_bundle.bundle.format_pattern(pattern, args, &mut errors);
if !errors.is_empty() {
log::error!(
target:"i18n_embed::fluent",
"Failed to format a message for language \"{}\" and id \"{}\".\nErrors\n{:?}.",
inner.current_languages.languages.first().unwrap_or(&self.fallback_language), message_id, errors
)
}
value.into()
})
)
.unwrap_or_else(|| {
log::error!(
target:"i18n_embed::fluent",
"Unable to find localization for language \"{}\" and id \"{}\".",
inner.current_languages.languages.first().unwrap_or(&self.fallback_language),
message_id
);
format!("No localization for id: \"{}\"", message_id)
})
}
pub fn get_args<'a, S, V>(&self, id: &str, args: HashMap<S, V>) -> String
where
S: Into<Cow<'a, str>> + Clone,
V: Into<FluentValue<'a>> + Clone,
{
self.get_args_fluent(id, hash_map_to_fluent_args(args).as_ref())
}
pub fn get_attr(&self, message_id: &str, attribute_id: &str) -> String {
self.get_attr_args_fluent(message_id, attribute_id, None)
}
pub fn get_attr_args_concrete<'args>(
&self,
message_id: &str,
attribute_id: &str,
args: HashMap<&'args str, FluentValue<'args>>,
) -> String {
self.get_attr_args_fluent(
message_id,
attribute_id,
hash_map_to_fluent_args(args).as_ref(),
)
}
pub fn get_attr_args_fluent<'args>(
&self,
message_id: &str,
attribute_id: &str,
args: Option<&'args FluentArgs<'args>>,
) -> String {
let inner = self.inner.load();
let language_config = inner.language_config.read();
let current_language = self.current_language_impl(&inner);
language_config.language_bundles.iter().find_map(|language_bundle| {
language_bundle
.bundle
.get_message(message_id)
.and_then(|m: FluentMessage<'_>| {
m.get_attribute(attribute_id)
.map(|a: FluentAttribute<'_>| {
a.value()
})
})
.map(|pattern: &Pattern<&str>| {
let mut errors = Vec::new();
let value = language_bundle.bundle.format_pattern(pattern, args, &mut errors);
if !errors.is_empty() {
log::error!(
target:"i18n_embed::fluent",
"Failed to format a message for language \"{}\" and id \"{}\".\nErrors\n{:?}.",
current_language, message_id, errors
)
}
value.into()
})
})
.unwrap_or_else(|| {
log::error!(
target:"i18n_embed::fluent",
"Unable to find localization for language \"{}\", message id \"{}\" and attribute id \"{}\".",
current_language,
message_id,
attribute_id
);
format!("No localization for message id: \"{message_id}\" and attribute id: \"{attribute_id}\"")
})
}
pub fn get_attr_args<'a, S, V>(
&self,
message_id: &str,
attribute_id: &str,
args: HashMap<S, V>,
) -> String
where
S: Into<Cow<'a, str>> + Clone,
V: Into<FluentValue<'a>> + Clone,
{
self.get_attr_args_fluent(
message_id,
attribute_id,
hash_map_to_fluent_args(args).as_ref(),
)
}
#[deprecated(
since = "0.13.6",
note = "Please use `select_languages(...).get(...)` instead"
)]
pub fn get_lang(&self, lang: &[&LanguageIdentifier], message_id: &str) -> String {
self.select_languages(lang).get(message_id)
}
#[deprecated(
since = "0.13.6",
note = "Please use `select_languages(...).get_args_concrete(...)` instead"
)]
pub fn get_lang_args_concrete<'source>(
&self,
lang: &[&LanguageIdentifier],
message_id: &str,
args: HashMap<&'source str, FluentValue<'source>>,
) -> String {
self.select_languages(lang)
.get_args_concrete(message_id, args)
}
#[deprecated(
since = "0.13.6",
note = "Please use `select_languages(...).get_args_fluent(...)` instead"
)]
pub fn get_lang_args_fluent<'args>(
&self,
lang: &[&LanguageIdentifier],
message_id: &str,
args: Option<&'args FluentArgs<'args>>,
) -> String {
self.select_languages(lang)
.get_args_fluent(message_id, args)
}
#[deprecated(
since = "0.13.6",
note = "Please use `select_languages(...).get_args(...)` instead"
)]
pub fn get_lang_args<'a, S, V>(
&self,
lang: &[&LanguageIdentifier],
id: &str,
args: HashMap<S, V>,
) -> String
where
S: Into<Cow<'a, str>> + Clone,
V: Into<FluentValue<'a>> + Clone,
{
self.select_languages(lang).get_args(id, args)
}
#[deprecated(
since = "0.13.6",
note = "Please use `select_languages(...).get_attr(...)` instead"
)]
pub fn get_lang_attr(
&self,
lang: &[&LanguageIdentifier],
message_id: &str,
attribute_id: &str,
) -> String {
self.select_languages(lang)
.get_attr(message_id, attribute_id)
}
#[deprecated(
since = "0.13.6",
note = "Please use `select_languages(...).get_attr_args_concrete(...)` instead"
)]
pub fn get_lang_attr_args_concrete<'source>(
&self,
lang: &[&LanguageIdentifier],
message_id: &str,
attribute_id: &str,
args: HashMap<&'source str, FluentValue<'source>>,
) -> String {
self.select_languages(lang)
.get_attr_args_concrete(message_id, attribute_id, args)
}
#[deprecated(
since = "0.13.6",
note = "Please use `select_languages(...).get_attr_args_fluent(...)` instead"
)]
pub fn get_lang_attr_args_fluent<'args>(
&self,
lang: &[&LanguageIdentifier],
message_id: &str,
attribute_id: &str,
args: Option<&'args FluentArgs<'args>>,
) -> String {
self.select_languages(lang)
.get_attr_args_fluent(message_id, attribute_id, args)
}
#[deprecated(
since = "0.13.6",
note = "Please use `lang(...).get_attr_args(...)` instead"
)]
pub fn get_lang_attr_args<'a, S, V>(
&self,
lang: &[&LanguageIdentifier],
message_id: &str,
attribute_id: &str,
args: HashMap<S, V>,
) -> String
where
S: Into<Cow<'a, str>> + Clone,
V: Into<FluentValue<'a>> + Clone,
{
self.select_languages(lang)
.get_attr_args(message_id, attribute_id, args)
}
pub fn has(&self, message_id: &str) -> bool {
let mut has_message = false;
self.inner
.load()
.language_config
.read()
.language_bundles
.iter()
.for_each(|language_bundle| {
has_message |= language_bundle.bundle.has_message(message_id)
});
has_message
}
pub fn has_attr(&self, message_id: &str, attribute_id: &str) -> bool {
self.inner
.load()
.language_config
.read()
.language_bundles
.iter()
.find_map(|bundle| {
bundle
.bundle
.get_message(message_id)
.map(|message| message.get_attribute(attribute_id).is_some())
})
.unwrap_or(false)
}
pub fn with_fluent_message<OUT, C>(&self, message_id: &str, closure: C) -> Option<OUT>
where
C: Fn(fluent::FluentMessage<'_>) -> OUT,
{
self.inner
.load()
.language_config
.read()
.language_bundles
.iter()
.find_map(|language_bundle| language_bundle.bundle.get_message(message_id))
.map(closure)
}
pub fn with_message_iter<OUT, C>(&self, language: &LanguageIdentifier, closure: C) -> OUT
where
C: Fn(&mut dyn Iterator<Item = &ast::Message<&str>>) -> OUT,
{
let inner = self.inner.load();
let config_lock = inner.language_config.read();
let mut iter = config_lock
.language_bundles
.iter()
.filter(|language_bundle| &language_bundle.language == language)
.flat_map(|language_bundle| {
language_bundle
.resource
.entries()
.filter_map(|entry| match entry {
ast::Entry::Message(message) => Some(message),
_ => None,
})
});
(closure)(&mut iter)
}
pub fn set_use_isolating(&self, value: bool) {
self.with_bundles_mut(|bundle| bundle.set_use_isolating(value));
}
pub fn with_bundles_mut<F>(&self, f: F)
where
F: Fn(&mut FluentBundle<Arc<FluentResource>, IntlLangMemoizer>),
{
for bundle in self
.inner
.load()
.language_config
.write()
.language_bundles
.as_mut_slice()
{
f(&mut bundle.bundle);
}
}
#[deprecated(since = "0.13.7", note = "Please use `select_languages(...)` instead")]
pub fn lang<LI: AsRef<LanguageIdentifier>>(&self, languages: &[LI]) -> FluentLanguageLoader {
self.select_languages(languages)
}
pub fn select_languages<LI: AsRef<LanguageIdentifier>>(
&self,
languages: &[LI],
) -> FluentLanguageLoader {
let inner = self.inner.load();
let config_lock = inner.language_config.read();
let fallback_language: Option<&unic_langid::LanguageIdentifier> = if languages
.iter()
.any(|language| language.as_ref() == &self.fallback_language)
{
None
} else {
Some(&self.fallback_language)
};
let indices = languages
.iter()
.map(|lang| lang.as_ref())
.chain(fallback_language)
.filter_map(|lang| config_lock.language_map.get(lang.as_ref()))
.cloned()
.collect();
FluentLanguageLoader {
inner: ArcSwap::new(Arc::new(FluentLanguageLoaderInner {
current_languages: CurrentLanguages {
languages: languages.iter().map(|lang| lang.as_ref().clone()).collect(),
indices,
},
language_config: self.inner.load().language_config.clone(),
})),
domain: self.domain.clone(),
fallback_language: self.fallback_language.clone(),
}
}
pub fn select_languages_negotiate<LI: AsRef<LanguageIdentifier>>(
&self,
languages: &[LI],
strategy: NegotiationStrategy,
) -> FluentLanguageLoader {
let available_languages = &self.inner.load().current_languages.languages;
let negotiated_languages = fluent_langneg::negotiate_languages(
languages,
available_languages,
Some(self.fallback_language()),
strategy,
);
self.select_languages(&negotiated_languages)
}
}
impl LanguageLoader for FluentLanguageLoader {
fn fallback_language(&self) -> &unic_langid::LanguageIdentifier {
&self.fallback_language
}
fn domain(&self) -> &str {
&self.domain
}
fn language_file_name(&self) -> String {
format!("{}.ftl", self.domain())
}
fn current_language(&self) -> unic_langid::LanguageIdentifier {
self.current_language_impl(&self.inner.load())
}
fn load_languages(
&self,
i18n_assets: &dyn I18nAssets,
language_ids: &[&unic_langid::LanguageIdentifier],
) -> Result<(), I18nEmbedError> {
if language_ids.is_empty() {
return Err(I18nEmbedError::RequestedLanguagesEmpty);
}
let mut load_language_ids: Vec<unic_langid::LanguageIdentifier> =
language_ids.iter().map(|id| (**id).clone()).collect();
if !load_language_ids.contains(&self.fallback_language) {
load_language_ids.push(self.fallback_language.clone());
}
let mut language_bundles = Vec::with_capacity(language_ids.len());
for language in &load_language_ids {
let (path, file) = self.language_file(language, i18n_assets);
if let Some(file) = file {
log::debug!(target:"i18n_embed::fluent", "Loaded language file: \"{0}\" for language: \"{1}\"", path, language);
let file_string = String::from_utf8(file.to_vec())
.map_err(|err| I18nEmbedError::ErrorParsingFileUtf8(path.clone(), err))?
.replace("\u{000D}\n", "\n");
let resource = match FluentResource::try_new(file_string) {
Ok(resource) => resource,
Err((resource, errors)) => {
errors.iter().for_each(|err| {
log::error!(target: "i18n_embed::fluent", "Error while parsing fluent language file \"{0}\": \"{1:?}\".", path, err);
});
resource
}
};
let language_bundle = LanguageBundle::new(language.clone(), resource);
language_bundles.push(language_bundle);
} else {
log::debug!(target:"i18n_embed::fluent", "Unable to find language file: \"{0}\" for language: \"{1}\"", path, language);
if language == &self.fallback_language {
return Err(I18nEmbedError::LanguageNotAvailable(path, language.clone()));
}
}
}
self.inner.swap(Arc::new(FluentLanguageLoaderInner {
current_languages: CurrentLanguages {
languages: language_ids.iter().map(|&lang| lang.to_owned()).collect(),
indices: (0..load_language_ids.len()).collect(),
},
language_config: Arc::new(RwLock::new(LanguageConfig {
language_map: language_bundles
.iter()
.enumerate()
.map(|(i, language_bundle)| (language_bundle.language.clone(), i))
.collect(),
language_bundles,
})),
}));
Ok(())
}
}
fn hash_map_to_fluent_args<'args, K, V>(map: HashMap<K, V>) -> Option<FluentArgs<'args>>
where
K: Into<Cow<'args, str>>,
V: Into<FluentValue<'args>>,
{
if map.is_empty() {
None
} else {
Some(FluentArgs::from_iter(map))
}
}