extern crate self as fluent_zero;
use std::{
borrow::Cow,
collections::HashMap,
hash::BuildHasher,
sync::{Arc, LazyLock},
};
use arc_swap::ArcSwap;
pub use fluent_bundle::{
FluentArgs, FluentResource, concurrent::FluentBundle as ConcurrentFluentBundle,
};
pub use fluent_syntax;
pub use phf;
pub use unic_langid::LanguageIdentifier;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheEntry {
Static(&'static str),
Dynamic,
}
pub struct LocaleState {
_id: LanguageIdentifier,
pub key: String,
}
static CURRENT_LANG: LazyLock<ArcSwap<LocaleState>> = LazyLock::new(|| {
let id: LanguageIdentifier = "en-US".parse().unwrap();
ArcSwap::from_pointee(LocaleState {
key: id.to_string(),
_id: id,
})
});
static FALLBACK_LANG_KEY: &str = "en-US";
pub fn set_lang(lang: LanguageIdentifier) {
let key = lang.to_string();
let new_state = LocaleState { _id: lang, key };
CURRENT_LANG.store(Arc::new(new_state));
}
pub fn get_lang() -> arc_swap::Guard<std::sync::Arc<LocaleState>> {
CURRENT_LANG.load()
}
pub trait CacheStore: Sync + Send {
fn get_entry(&self, lang: &str, key: &str) -> Option<CacheEntry>;
}
impl CacheStore for phf::Map<&'static str, &'static phf::Map<&'static str, CacheEntry>> {
fn get_entry(&self, lang: &str, key: &str) -> Option<CacheEntry> {
self.get(lang).and_then(|m| m.get(key)).copied()
}
}
pub trait BundleCollection: Sync + Send {
fn get_bundle(&self, lang: &str) -> Option<&ConcurrentFluentBundle<FluentResource>>;
}
impl BundleCollection
for phf::Map<&'static str, &'static LazyLock<ConcurrentFluentBundle<FluentResource>>>
{
fn get_bundle(&self, lang: &str) -> Option<&ConcurrentFluentBundle<FluentResource>> {
self.get(lang).map(|lazy| &***lazy)
}
}
impl<S: BuildHasher + Sync + Send> BundleCollection
for HashMap<String, ConcurrentFluentBundle<FluentResource>, S>
{
fn get_bundle(&self, lang: &str) -> Option<&ConcurrentFluentBundle<FluentResource>> {
self.get(lang)
}
}
#[must_use]
pub fn join_charsets(charsets: &[&str]) -> String {
let mut unique_chars = std::collections::BTreeSet::new();
for charset in charsets {
unique_chars.extend(charset.chars());
}
unique_chars.into_iter().collect()
}
#[inline]
fn resolve_entry<'a, B: BundleCollection + ?Sized, C: CacheStore + ?Sized>(
bundles: &'a B,
cache: &C,
lang: &str,
key: &'a str,
args: Option<&FluentArgs>,
) -> Option<Cow<'a, str>> {
match cache.get_entry(lang, key)? {
CacheEntry::Static(s) => Some(Cow::Borrowed(s)),
CacheEntry::Dynamic => {
let bundle = bundles.get_bundle(lang)?;
let msg = bundle.get_message(key)?;
let pattern = msg.value()?;
let mut errors = vec![];
Some(bundle.format_pattern(pattern, args, &mut errors))
}
}
}
#[inline]
fn lookup_core<'a, B: BundleCollection + ?Sized, C: CacheStore + ?Sized>(
bundles: &'a B,
cache: &C,
key: &'a str,
args: Option<&FluentArgs>,
) -> Cow<'a, str> {
let lang_guard = get_lang();
let current_key = &lang_guard.key;
if let Some(res) = resolve_entry(bundles, cache, current_key, key, args) {
return res;
}
if current_key != FALLBACK_LANG_KEY
&& let Some(res) = resolve_entry(bundles, cache, FALLBACK_LANG_KEY, key, args)
{
return res;
}
Cow::Borrowed(key)
}
pub fn lookup_static<'a, B: BundleCollection + ?Sized, C: CacheStore + ?Sized>(
bundles: &'a B,
cache: &C,
key: &'a str,
) -> Cow<'a, str> {
lookup_core(bundles, cache, key, None)
}
pub fn lookup_dynamic<'a, B: BundleCollection + ?Sized, C: CacheStore + ?Sized>(
bundles: &'a B,
cache: &C,
key: &'a str,
args: &FluentArgs,
) -> Cow<'a, str> {
lookup_core(bundles, cache, key, Some(args))
}
#[allow(clippy::crate_in_macro_def)]
#[macro_export]
macro_rules! t {
($key:expr) => {
$crate::lookup_static(
&crate::LOCALES,
&crate::CACHE,
$key
)
};
($key:expr, { $($k:expr => $v:expr),* $(,)? }) => {
{
let mut args = $crate::FluentArgs::new();
$( args.set($k, $v); )*
$crate::lookup_dynamic(
&crate::LOCALES,
&crate::CACHE,
$key,
&args
)
}
};
}